Source of file OrderStep.php
Size: 46,548 Bytes - Last Modified: 2021-12-23T10:39:35+00:00
/var/www/docs.ssmods.com/process/src/src/Model/Process/OrderStep.php
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502 | <?php namespace Sunnysideup\Ecommerce\Model\Process; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Core\Config\Config; use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter; use SilverStripe\Forms\GridField\GridFieldDeleteAction; use SilverStripe\Forms\HeaderField; use SilverStripe\Forms\HTMLEditor\HTMLEditorField; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\ReadonlyField; use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\TextField; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Security\Member; use SilverStripe\Security\Permission; use SilverStripe\Security\Security; use Sunnysideup\Ecommerce\Api\ClassHelpers; use Sunnysideup\Ecommerce\Config\EcommerceConfig; use Sunnysideup\Ecommerce\Email\OrderErrorEmail; use Sunnysideup\Ecommerce\Email\OrderInvoiceEmail; use Sunnysideup\Ecommerce\Forms\Fields\EcommerceCMSButtonField; use Sunnysideup\Ecommerce\Interfaces\EditableEcommerceObject; use Sunnysideup\Ecommerce\Model\Extensions\EcommerceRole; use Sunnysideup\Ecommerce\Model\Order; use Sunnysideup\Ecommerce\Model\Process\OrderSteps\OrderStepArchived; use Sunnysideup\Ecommerce\Model\Process\OrderSteps\OrderStepCreated; use Sunnysideup\Ecommerce\Model\Process\OrderSteps\OrderStepSubmitted; use Sunnysideup\Ecommerce\Pages\OrderConfirmationPage; /** * @description: see OrderStep.md * * @authors: Nicolaas [at] Sunny Side Up .co.nz * @package: ecommerce * @sub-package: model */ class OrderStep extends DataObject implements EditableEcommerceObject { /** * @var string */ protected $emailClassName = OrderInvoiceEmail::class; // Order Status Logs /** * The OrderStatusLog that is relevant to the particular step. * * @var string */ protected $relevantLogEntryClassName = ''; /** * @var array */ private static $order_steps_to_include = [ 'step1' => OrderStepCreated::class, 'step2' => OrderStepSubmitted::class, 'step3' => OrderStepArchived::class, ]; /** * @var int */ private static $number_of_days_to_send_update_email = 10; /** * standard SS variable. * * @return array */ private static $table_name = 'OrderStep'; private static $db = [ 'Name' => 'Varchar(50)', 'Code' => 'Varchar(50)', 'Description' => 'Text', 'EmailSubject' => 'Varchar(200)', 'CustomerMessage' => 'HTMLText', //customer privileges 'CustomerCanEdit' => 'Boolean', 'CustomerCanCancel' => 'Boolean', 'CustomerCanPay' => 'Boolean', //What to show the customer... 'ShowAsUncompletedOrder' => 'Boolean', 'ShowAsInProcessOrder' => 'Boolean', 'ShowAsCompletedOrder' => 'Boolean', 'HideStepFromCustomer' => 'Boolean', //sorting index 'Sort' => 'Int', 'DeferTimeInSeconds' => 'Int', 'DeferFromSubmitTime' => 'Boolean', ]; /** * standard SS variable. * * @return array */ private static $indexes = [ 'Code' => true, 'Sort' => true, ]; /** * standard SS variable. * * @return array */ private static $has_many = [ 'Orders' => Order::class, 'OrderEmailRecords' => OrderEmailRecord::class, 'OrderProcessQueueEntries' => OrderProcessQueue::class, ]; /** * standard SS variable. * * @return array */ private static $field_labels = [ 'Sort' => 'Sorting Index', 'CustomerCanEdit' => 'Customer can edit order', 'CustomerCanPay' => 'Customer can pay order', 'CustomerCanCancel' => 'Customer can cancel order', ]; /** * standard SS variable. * * @return array */ private static $summary_fields = [ 'NameAndDescription' => 'Step', 'ShowAsSummary' => 'Phase', 'Orders.Count' => 'Orders', ]; /** * standard SS variable. * * @return array */ private static $casting = [ 'Title' => 'Varchar', 'CustomerCanEditNice' => 'Varchar', 'CustomerCanPayNice' => 'Varchar', 'CustomerCanCancelNice' => 'Varchar', 'ShowAsUncompletedOrderNice' => 'Varchar', 'ShowAsInProcessOrderNice' => 'Varchar', 'ShowAsCompletedOrderNice' => 'Varchar', 'HideStepFromCustomerNice' => 'Varchar', 'HasCustomerMessageNice' => 'Varchar', 'ShowAsSummary' => 'HTMLText', 'NameAndDescription' => 'HTMLText', ]; /** * standard SS variable. * * @return array */ private static $searchable_fields = [ 'Name' => [ 'title' => 'Name', 'filter' => 'PartialMatchFilter', ], 'Code' => [ 'title' => 'Code', 'filter' => 'PartialMatchFilter', ], ]; /** * standard SS variable. * * @return string */ private static $singular_name = 'Order Step'; /** * standard SS variable. * * @return string */ private static $plural_name = 'Order Steps'; /** * Standard SS variable. * * @var string */ private static $description = 'A step that any order goes through.'; /** * SUPER IMPORTANT TO KEEP ORDER! * standard SS variable. * * @return string */ private static $default_sort = '"Sort" ASC'; private static $_last_order_step_cache; /** * IMPORTANT:: MUST HAVE Code must be defined!!! * standard SS variable. * * @return array */ private static $defaults = [ 'CustomerCanEdit' => 0, 'CustomerCanCancel' => 0, 'CustomerCanPay' => 1, 'ShowAsUncompletedOrder' => 0, 'ShowAsInProcessOrder' => 0, 'ShowAsCompletedOrder' => 0, 'Code' => 'ORDERSTEP', ]; /** * casted variable. */ public function Title(): string { return $this->getTitle(); } public function getTitle(): string { return (string) $this->Name; } /** * casted variable. */ public function CustomerCanEditNice(): string { return $this->getCustomerCanEditNice(); } public function getCustomerCanEditNice(): string { return $this->yesOrNoNiceHelper($this->CustomerCanEdit); } /** * casted variable. */ public function CustomerCanPayNice(): string { return $this->getCustomerCanPayNice(); } public function getCustomerCanPayNice(): string { return $this->yesOrNoNiceHelper($this->CustomerCanPay); } /** * casted variable. */ public function CustomerCanCancelNice(): string { return $this->getCustomerCanCancelNice(); } public function getCustomerCanCancelNice(): string { return $this->yesOrNoNiceHelper($this->CustomerCanCancel); } public function ShowAsUncompletedOrderNice(): string { return $this->getShowAsUncompletedOrderNice(); } public function getShowAsUncompletedOrderNice(): string { return $this->yesOrNoNiceHelper($this->ShowAsUncompletedOrder); } /** * casted variable. */ public function ShowAsInProcessOrderNice(): string { return $this->getShowAsInProcessOrderNice(); } public function getShowAsInProcessOrderNice(): string { return $this->yesOrNoNiceHelper($this->ShowAsInProcessOrder); } /** * casted variable. */ public function ShowAsCompletedOrderNice(): string { return $this->getShowAsCompletedOrderNice(); } public function getShowAsCompletedOrderNice(): string { return $this->yesOrNoNiceHelper($this->ShowAsCompletedOrder); } /** * do not show in steps at all. */ public function HideFromEveryone(): bool { return false; } /** * casted variable. */ public function HideStepFromCustomerNice(): string { return $this->getHideStepFromCustomerNice(); } public function getHideStepFromCustomerNice(): string { return $this->yesOrNoNiceHelper($this->HideStepFromCustomer); } public function i18n_singular_name() { return _t('OrderStep.ORDERSTEP', 'Order Step'); } public function i18n_plural_name() { return _t('OrderStep.ORDERSTEPS', 'Order Steps'); } /** * returns all the order steps * that the admin should / can edit.... * * @return \SilverStripe\ORM\DataList */ public static function admin_manageable_steps() { $lastStep = OrderStep::last_order_step(); return OrderStep::get()->filter(['ShowAsInProcessOrder' => 1])->exclude(['ID' => $lastStep->ID]); } /** * returns all the order steps * that the admin should / can edit.... * * @return \SilverStripe\ORM\DataList */ public static function non_admin_manageable_steps() { $lastStep = OrderStep::last_order_step(); return OrderStep::get()->filterAny(['ShowAsInProcessOrder' => 0, 'ID' => $lastStep->ID]); } /** * @param bool $noCacheValues * * @return OrderStep */ public static function last_order_step($noCacheValues = false) { if (! self::$_last_order_step_cache || $noCacheValues) { self::$_last_order_step_cache = OrderStep::get()->Last(); } return self::$_last_order_step_cache; } /** * return StatusIDs (orderstep IDs) from orders that are bad.... * (basically StatusID values that do not exist). * * @return array */ public static function bad_order_step_ids() { $badorderStatus = Order::get() ->leftJoin('OrderStep', '"OrderStep"."ID" = "Order"."StatusID"') ->where('"OrderStep"."ID" IS NULL AND "StatusID" > 0') ->column('StatusID') ; if (is_array($badorderStatus)) { return array_unique(array_values($badorderStatus)); } return [-1]; } /** * turns code into ID. */ public static function get_status_id_from_code(string $code): int { $otherStatus = DataObject::get_one( OrderStep::class, ['Code' => $code] ); if ($otherStatus) { return (int) $otherStatus->ID; } return 0; } /** * @return array */ public static function get_codes_for_order_steps_to_include() { $newArray = []; $array = EcommerceConfig::get(OrderStep::class, 'order_steps_to_include'); if (is_array($array) && count($array)) { foreach ($array as $className) { $code = singleton($className)->getMyCode(); $newArray[$className] = strtoupper($code); } } return $newArray; } /** * returns a list of ordersteps that have not been created yet. * * @return array */ public static function get_not_created_codes_for_order_steps_to_include() { $array = EcommerceConfig::get(OrderStep::class, 'order_steps_to_include'); if (is_array($array) && count($array)) { foreach ($array as $className) { $obj = DataObject::get_one($className); if ($obj) { unset($array[$className]); } } } return $array; } /** * @return string */ public function getMyCode() { $array = Config::inst()->get($this->ClassName, 'defaults', Config::UNINHERITED); if (! isset($array['Code'])) { user_error($this->ClassName . ' does not have a default code specified'); } return $array['Code']; } /** * standard SS method. */ public function populateDefaults() { $this->Description = $this->myDescription(); return parent::populateDefaults(); } /** * @return \SilverStripe\Forms\FieldList */ public function getCMSFields() { $fields = parent::getCMSFields(); //replacing $queueField = $fields->dataFieldByName('OrderProcessQueueEntries'); if ($queueField) { $config = $queueField->getConfig(); $config->removeComponentsByType(GridFieldAddExistingAutocompleter::class); $config->removeComponentsByType(GridFieldDeleteAction::class); } $fields->removeFieldFromTab('Root', 'OrderProcessQueueEntries'); if ($this->canBeDefered()) { if ($this->DeferTimeInSeconds) { $fields->addFieldToTab( 'Root.Queue', HeaderField::create( 'WhenWillThisRun', $this->humanReadeableDeferTimeInSeconds() ) ); } $fields->addFieldToTab( 'Root.Queue', $deferTimeInSecondsField = TextField::create( 'DeferTimeInSeconds', _t('OrderStep.DeferTimeInSeconds', 'Seconds in queue') ) ->setDescription( _t( 'OrderStep.TIME_EXPLANATION', '86,400 seconds is one day ... <br />To make it easier, you can also enter things like <em>1 week</em>, <em>3 hours</em>, or <em>7 minutes</em>. <br />Non-second entries will automatically be converted to seconds.' ) ) ); if ($this->DeferTimeInSeconds) { $fields->addFieldToTab( 'Root.Queue', $deferTimeInSecondsField = CheckboxField::create( 'DeferFromSubmitTime', _t('OrderStep.DeferFromSubmitTime', 'Calculated from submit time?') ) ->setDescription( _t( 'OrderStep.DeferFromSubmitTime_HELP', 'The time in the queue can be calculated from the moment the current orderstep starts or from the moment the order was submitted (in this case, check the box above) ' ) ) ); } $fields->addFieldToTab( 'Root.Queue', $queueField ); } if ($this->hasCustomerMessage()) { $rightTitle = _t( 'OrderStep.EXPLAIN_ORDER_NUMBER_IN_SUBJECT', 'You can use [OrderNumber] as a tag that will be replaced with the actual Order Number.' ); $fields->addFieldToTab( 'Root.CustomerMessage', TextField::create('EmailSubject', _t('OrderStep.EMAILSUBJECT', 'Email Subject')) ->setDescription($rightTitle) ); $testEmailLink = $this->testEmailLink(); if ($testEmailLink) { $fields->addFieldToTab( 'Root.CustomerMessage', new LiteralField( 'testEmailLink', '<h3> <a href="' . $testEmailLink . '" data-popup="true" target"_blank" onclick="emailPrompt(this, event);"> ' . _t('OrderStep.VIEW_EMAIL_EXAMPLE', 'Test Email') . ' </a> </h3> <script language="javascript"> function emailPrompt(caller, event) { event.preventDefault(); var href = jQuery(caller).attr("href"); var email = prompt("Enter an email address to receive a copy of this example in your inbox, leave blank to view in the browser"); if (email) { href += "&send=" + email; } window.open(href); }; </script>' ) ); } $fields->addFieldToTab('Root.CustomerMessage', $htmlEditorField = new HTMLEditorField('CustomerMessage', _t('OrderStep.CUSTOMERMESSAGE', 'Customer Message (if any)'))); $htmlEditorField->setRows(3); } else { $fields->removeFieldFromTab('Root', 'OrderEmailRecords'); $fields->removeFieldFromTab('Root.Main', 'EmailSubject'); $fields->removeFieldFromTab('Root.Main', 'CustomerMessage'); } //adding if (! $this->exists() || ! $this->isDefaultStatusOption()) { $fields->removeFieldFromTab('Root.Main', 'Code'); $fields->addFieldToTab('Root.Main', new DropdownField('ClassName', _t('OrderStep.TYPE', 'Type'), self::get_not_created_codes_for_order_steps_to_include()), 'Name'); } if ($this->isDefaultStatusOption()) { $fields->replaceField('Code', $fields->dataFieldByName('Code')->performReadonlyTransformation()); } //headers $fields->addFieldToTab('Root.Main', new HeaderField('WARNING1', _t('OrderStep.CAREFUL', 'CAREFUL! please edit details below with care'), 2), 'Description'); $fields->addFieldToTab('Root.Main', new HeaderField('WARNING2', _t('OrderStep.CUSTOMERCANCHANGE', 'What can be changed during this step?'), 3), 'CustomerCanEdit'); $fields->addFieldToTab('Root.Main', new HeaderField('WARNING5', _t('OrderStep.ORDERGROUPS', 'Order groups for customer?'), 3), 'ShowAsUncompletedOrder'); $fields->addFieldToTab('Root.Main', new HeaderField('HideStepFromCustomerHeader', _t('OrderStep.HIDE_STEP_FROM_CUSTOMER_HEADER', 'Customer Interaction'), 3), 'HideStepFromCustomer'); $fields->addFieldToTab('Root.Main', new HeaderField('DeferHeader', _t('OrderStep.DEFER_HEADER', 'Delay'), 3), 'DeferTimeInSeconds'); //final cleanup $fields->removeFieldFromTab('Root.Main', 'Sort'); $fields->addFieldToTab('Root.Main', new TextareaField('Description', _t('OrderStep.DESCRIPTION', 'Explanation for internal use only')), 'WARNING1'); return $fields; } /** * link to edit the record. * * @param null|string $action - e.g. edit * * @return string */ public function CMSEditLink($action = null) { $sanitisedClassName = ClassHelpers::sanitise_class_name(OrderStep::class); return 'admin/shop/' . $sanitisedClassName . '/EditForm/field/' . $sanitisedClassName . '/item/' . $this->ID . '/edit'; } /** * tells the order to display itself with an alternative display page. * in that way, orders can be displayed differently for certain steps * for example, in a print step, the order can be displayed in a * PRINT ONLY format. * * When the method return null, the order is displayed using the standard display page * * @see Order::DisplayPage * * @return null|object (Page) */ public function AlternativeDisplayPage() { return null; } /** * A form that can be used by the Customer to progress step! * * @return null|\SilverStripe\Forms\Form (CustomerOrderStepForm) */ public function CustomerOrderStepForm(Controller $controller, string $name, Order $order) { return null; } /** * Allows the opportunity for the Order Step to add any fields to Order::getCMSFields. * * @return \SilverStripe\Forms\FieldList */ public function addOrderStepFields(FieldList $fields, Order $order, ?bool $nothingToDo = false) { if ($nothingToDo) { $text = _t( 'OrderStep.NOTHING_TO_DO', 'Orders should not be stuck in this step and an order being here may indicate an error. Please check the Process Tab and see if the order is being queued for the next step. In case it is stuck here for a long time, you can move it along using the field below. This is not recommended.' ); $fields->addFieldsToTab( 'Root.Next', [ ReadonlyField::create('StatusIDNotice', 'Info', $text), DropdownField::create( 'StatusID', 'Select Status - NOT RECOMMENDED', OrderStep::get()->map() ), ] ); } $this->addQuickLogEntryButton($fields, $order); return $fields; } public function addQuickLogEntryButton($fields, $order) { if ($this->relevantLogEntryClassName) { $log = $this->relevantLogEntryClassName::get()->filter(['OrderID' => $order->ID])->Last(); if ($log) { $link = $log->CMSEditLink(); $title = _t('Order.EDIT', 'Edit') . ' ' . $log->i18n_singular_name(); $fields->addFieldsToTab( 'Root.Next', [ EcommerceCMSButtonField::create( 'AddEditNoteButton', $link, $title ), ] ); } } } /** * @return \SilverStripe\ORM\ValidationResult */ public function validate() { $result = parent::validate(); $anotherOrderStepWithSameNameOrCode = OrderStep::get() ->filter( [ 'Name' => $this->Name, 'Code' => strtoupper($this->Code), ] ) ->exclude(['ID' => (int) $this->ID]) ->First() ; if ($anotherOrderStepWithSameNameOrCode) { $result->addError(_t('OrderStep.ORDERSTEPALREADYEXISTS', 'An order status with this name already exists. Please change the name and try again.')); } return $result; } // moving between statusses... /** *initStep: * makes sure the step is ready to run.... (e.g. check if the order is ready to be emailed as receipt). * should be able to run this function many times to check if the step is ready. * * @see Order::doNextStatus * * @return bool - true if the current step is ready to be run... */ public function initStep(Order $order): bool { user_error('Please implement the initStep method in a subclass (' . __CLASS__ . ') of OrderStep', E_USER_WARNING); return true; } /** *doStep: * should only be able to run this function once * (init stops you from running it twice - in theory....) * runs the actual step. * * @see Order::doNextStatus * * @return bool - true if run correctly */ public function doStep(Order $order): bool { user_error('Please implement the initStep method in a subclass (' . __CLASS__ . ') of OrderStep', E_USER_WARNING); return true; } /** * nextStep: * returns the next step (after it checks if everything is in place for the next step to run...). * * @see Order::doNextStatus * * @return null|OrderStep (next step OrderStep object) */ public function nextStep(Order $order) { $sort = (int) $this->Sort; if (! $sort) { $sort = 0; } $where = '"OrderStep"."Sort" > ' . $sort; $nextOrderStepObject = DataObject::get_one( OrderStep::class, $where ); if ($nextOrderStepObject) { return $nextOrderStepObject; } return null; } // Boolean checks /** * Checks if a step has passed (been completed) in comparison to the current step. * * @param bool $orIsEqualTo if set to true, this method will return TRUE if the step being checked is the current one * @param mixed $code * * @return bool */ public function hasPassed($code, $orIsEqualTo = false) { $otherStatus = DataObject::get_one( OrderStep::class, ['Code' => $code] ); if ($otherStatus) { if ($otherStatus->Sort < $this->Sort) { return true; } if ($orIsEqualTo && $otherStatus->Code === $this->Code) { return true; } } else { user_error("could not find {$code} in OrderStep", E_USER_NOTICE); } return false; } /** * @param string $code * * @return bool */ public function hasPassedOrIsEqualTo($code) { return $this->hasPassed($code, true); } /** * @param string $code * * @return bool */ public function hasNotPassed($code) { return (bool) ! $this->hasPassed($code, true); } /** * Opposite of hasPassed. */ public function isBefore(string $code): bool { return ! (bool) $this->hasPassed($code); } /** * returns the email class used for emailing the * customer during a specific step (IF ANY!). * * @return string */ public function getEmailClassName() { return $this->emailClassName; } /** * sets the email class used for emailing the * customer during a specific step (IF ANY!). */ public function setEmailClassName(string $s): self { $this->emailClassName = $s; return $this; } /** * Has an email been sent to the customer for this * order step. *"-10 days". * * @param bool $checkDateOfOrder * * @return bool */ public function hasBeenSent(Order $order, ?bool $checkDateOfOrder = true) { //if it has been more than a XXX days since the order was last edited (submitted) then we do not send emails as //this would be embarrasing. if ($checkDateOfOrder) { $lastEditedValue = ($log = $order->SubmissionLog()) ? $log->LastEdited : $order->LastEdited; if ((strtotime($lastEditedValue) < strtotime('-' . EcommerceConfig::get(OrderStep::class, 'number_of_days_to_send_update_email') . ' days'))) { return true; } } $exists = OrderEmailRecord::get() ->filter( [ 'OrderID' => $order->ID, 'OrderStepID' => $this->ID, 'Result' => 1, ] ) ->exists() ; if ($exists) { return true; } $count = OrderEmailRecord::get() ->filter( [ 'OrderID' => $order->ID, 'OrderStepID' => $this->ID, ] ) ->count() ; //tried it twice - abandon to avoid being stuck in a loop! return $count >= 2; } /** * Formatted answer for "hasCustomerMessage". */ public function HasCustomerMessageNice(): string { return $this->getHasCustomerMessageNice(); } public function getHasCustomerMessageNice(): string { return $this->hasCustomerMessage() ? _t('OrderStep.YES', 'Yes') : _t('OrderStep.NO', 'No'); } public function CalculatedEmailSubject(?Order $order = null): string { return (string) $this->EmailSubject; } public function CalculatedCustomerMessage(?Order $order = null): string { return (string) $this->CustomerMessage; } /** * Formatted answer for "hasCustomerMessage". */ public function ShowAsSummary() { return $this->getShowAsSummary(); } public function getShowAsSummary(): DBHTMLText { $v = '<strong>'; if ($this->ShowAsUncompletedOrder) { $v .= _t('OrderStep.UNCOMPLETED', 'Uncompleted'); } elseif ($this->ShowAsInProcessOrder) { $v .= _t('OrderStep.INPROCESS', 'In process'); } elseif ($this->ShowAsCompletedOrder) { $v .= _t('OrderStep.COMPLETED', 'Completed'); } $v .= '</strong>'; $canArray = []; if ($this->CustomerCanEdit) { $canArray[] = _t('OrderStep.EDITABLE', 'edit'); } if ($this->CustomerCanPay) { $canArray[] = _t('OrderStep.PAY', 'pay'); } if ($this->CustomerCanCancel) { $canArray[] = _t('OrderStep.CANCEL', 'cancel'); } if (count($canArray)) { $v .= '<br />' . _t('OrderStep.CUSTOMER_CAN', 'Customer Can') . ': ' . implode(', ', $canArray) . ''; } if ($this->hasCustomerMessage()) { $v .= '<br />' . _t('OrderStep.CUSTOMER_MESSAGES', 'Includes message to customer'); } if ($this->DeferTimeInSeconds) { $v .= '<br />' . $this->humanReadeableDeferTimeInSeconds(); } // @return DBHTMLText return DBField::create_field('HTMLText', $v); } /** * Formatted answer for "hasCustomerMessage". * * @return string */ public function NameAndDescription() { return $this->getNameAndDescription(); } public function getNameAndDescription() { $v = '<strong>' . $this->Name . '</strong><br /><em>' . $this->Description . '</em>'; return DBField::create_field('HTMLText', $v); } /** * This allows you to set the time to something other than the standard DeferTimeInSeconds * value based on the order provided. * * @param Order $order (optional) * * @return int */ public function CalculatedDeferTimeInSeconds(?Order $order = null) { return $this->DeferTimeInSeconds; } public function getRelevantLogEntryClassName(): string { return $this->relevantLogEntryClassName; } public function setRelevantLogEntryClassName(string $s): self { $this->relevantLogEntryClassName = $s; return $this; } /** * returns the OrderStatusLog that is relevant to this step. * * @return null|OrderStatusLog */ public function RelevantLogEntry(Order $order) { if ($this->getRelevantLogEntryClassName()) { return $this->RelevantLogEntries($order)->Last(); } return null; } /** * returns the OrderStatusLogs that are relevant to this step. * It is important that getRelevantLogEntryClassName returns * a specific enough ClassName and not a base class name. * * @return null|\SilverStripe\ORM\DataList */ public function RelevantLogEntries(Order $order) { $className = $this->getRelevantLogEntryClassName(); if ($className) { return $className::get()->filter( [ 'OrderID' => $order->ID, ] ); } return null; } // Silverstripe Standard Data Object Methods /** * Standard SS method * These are only created programmatically. * * @param \SilverStripe\Security\Member $member * @param mixed $context * * @return bool */ public function canCreate($member = null, $context = []) { return false; } /** * Standard SS method. * * @param \SilverStripe\Security\Member $member * @param mixed $context * * @return bool */ public function canView($member = null, $context = []) { if (! $member) { $member = Security::getCurrentUser(); } $extended = $this->extendedCan(__FUNCTION__, $member); if (null !== $extended) { return $extended; } if (Permission::checkMember($member, Config::inst()->get(EcommerceRole::class, 'admin_permission_code'))) { return true; } return parent::canEdit($member); } /** * the default for this is TRUE, but for completed order steps. * * we do not allow this. * * @param Order $order * @param Member $member optional */ public function canOverrideCanViewForOrder($order, $member = null): bool { //return true if the order can have customer input // orders recently saved can also be views return $this->CustomerCanEdit || $this->CustomerCanCancel || $this->CustomerCanPay; } /** * standard SS method. * * @param null|mixed $member * @param mixed $context * * @return bool */ public function canEdit($member = null, $context = []) { if (! $member) { $member = Security::getCurrentUser(); } $extended = $this->extendedCan(__FUNCTION__, $member); if (null !== $extended) { return $extended; } if (Permission::checkMember($member, Config::inst()->get(EcommerceRole::class, 'admin_permission_code'))) { return true; } return parent::canEdit($member); } /** * Standard SS method. * * @param \SilverStripe\Security\Member $member * * @return bool */ public function canDelete($member = null) { //cant delete last status if there are orders with this status $nextOrderStepObject = $this->NextOrderStep(); if ($nextOrderStepObject) { //do nothing } else { $exists = Order::get() ->filter(['StatusID' => (int) $this->ID]) ->exists() ; if ($exists) { return false; } } if ($this->isDefaultStatusOption()) { return false; } if (! $member) { $member = Security::getCurrentUser(); } $extended = $this->extendedCan(__FUNCTION__, $member); if (null !== $extended) { return $extended; } if (Permission::checkMember($member, Config::inst()->get(EcommerceRole::class, 'admin_permission_code'))) { return true; } return parent::canEdit($member); } /** * standard SS method * USED TO BE: Unpaid,Query,Paid,Processing,Sent,Complete,AdminCancelled,MemberCancelled,Cart. */ public function requireDefaultRecords() { parent::requireDefaultRecords(); $this->checkValidityOfOrderSteps(); } protected function yesOrNoNiceHelper(?bool $bool): string { return $bool ? _t('OrderStep.YES', 'Yes') : _t('OrderStep.NO', 'No'); } /** * standard SS method. */ protected function onBeforeWrite() { parent::onBeforeWrite(); //make sure only one of three conditions applies ... if ($this->ShowAsUncompletedOrder) { $this->ShowAsInProcessOrder = false; $this->ShowAsCompletedOrder = false; } elseif ($this->ShowAsInProcessOrder) { $this->ShowAsUncompletedOrder = false; $this->ShowAsCompletedOrder = false; } elseif ($this->ShowAsCompletedOrder) { $this->ShowAsUncompletedOrder = false; $this->ShowAsInProcessOrder = false; } if (! $this->canBeDefered()) { $this->DeferTimeInSeconds = 0; $this->DeferFromSubmitTime = 0; } elseif (is_numeric($this->DeferTimeInSeconds)) { $this->DeferTimeInSeconds = (int) $this->DeferTimeInSeconds; } else { $this->DeferTimeInSeconds = strtotime('+' . $this->DeferTimeInSeconds); if ($this->DeferTimeInSeconds > 0) { $this->DeferTimeInSeconds -= time(); } } $this->Code = strtoupper($this->Code); } /** * move linked orders to the next status * standard SS method. */ protected function onBeforeDelete() { $ordersWithThisStatus = Order::get()->filter(['StatusID' => $this->ID]); if ($ordersWithThisStatus->exists()) { $bestOrderStep = $this->NextOrderStep(); //backup if ($bestOrderStep && $bestOrderStep->exists()) { //do nothing } else { $bestOrderStep = $this->PreviousOrderStep(); } if ($bestOrderStep) { foreach ($ordersWithThisStatus as $orderWithThisStatus) { $orderWithThisStatus->StatusID = $bestOrderStep->ID; $orderWithThisStatus->write(); } } } parent::onBeforeDelete(); } /** * standard SS method. */ protected function onAfterDelete() { parent::onAfterDelete(); $this->checkValidityOfOrderSteps(); } /** * @return bool */ protected function isDefaultStatusOption() { return in_array($this->Code, self::get_codes_for_order_steps_to_include(), true); } /** * return true if done already or mailed successfully now. * * @param order $order * @param string $subject * @param string $message * @param bool $resend * @param bool|string $adminOnlyOrToEmail you can set to false = send to customer, true: send to admin, or email = send to email * @param string $emailClassName * * @return bool; */ protected function sendEmailForStep( $order, $subject, $message = '', $resend = false, $adminOnlyOrToEmail = false, $emailClassName = '' ): bool { if (false === $this->hasBeenSent($order) || true === boolval($resend)) { if (! $subject) { $subject = $this->CalculatedEmailSubject($order); } $useAlternativeEmail = $adminOnlyOrToEmail && filter_var($adminOnlyOrToEmail, FILTER_VALIDATE_EMAIL); //this is NOT an admin EMAIL if ($this->hasCustomerMessage() || $useAlternativeEmail) { if (! $emailClassName) { $emailClassName = $this->getEmailClassName(); } $outcome = $order->sendEmail( $emailClassName, $subject, $message, $resend, $adminOnlyOrToEmail ); //ADMIN ONLY .... } else { if (! $emailClassName) { $emailClassName = OrderErrorEmail::class; } //looks like we are sending an error, but we are just using this for notification $message = _t('OrderStep.THISMESSAGENOTSENTTOCUSTOMER', 'NOTE: This message was not sent to the customer.') . '<br /><br /><br /><br />' . $message; $outcome = $order->sendAdminNotification( $emailClassName, $subject, $message, $resend ); } return $outcome || Director::isDev(); } return true; } /** * returns a link that can be used to test * the email being sent during this step * this method returns NULL if no email * is being sent OR if there is no suitable Order * to test with... */ protected function testEmailLink(): ?string { if ($this->getEmailClassName()) { $order = DataObject::get_one( Order::class, ['StatusID' => $this->ID], $cacheDataObjectGetOne = true, 'RAND() ASC' ); if (! $order) { $order = Order::get() ->where('"OrderStep"."Sort" >= ' . $this->Sort) ->sort('IF("OrderStep"."Sort" > ' . $this->Sort . ', 0, 1) ASC, "OrderStep"."Sort" ASC, RAND() ASC') ->innerJoin('OrderStep', '"OrderStep"."ID" = "Order"."StatusID"') ->first() ; } if ($order) { return OrderConfirmationPage::get_email_link( $order->ID, $this->getEmailClassName(), $actuallySendEmail = false, $alternativeOrderStepID = $this->ID ); } } return null; } /** * For some ordersteps this returns true... * * @return bool */ protected function hasCustomerMessage() { return false; } protected function humanReadeableDeferTimeInSeconds(): ?string { if ($this->canBeDefered()) { $field = DBField::create_field('DBDatetime', strtotime('+ ' . $this->DeferTimeInSeconds . ' seconds')); $descr0 = _t('OrderStep.THE', 'The') . ' ' . '<span style="color: #338DC1">' . $this->getTitle() . '</span>'; $descr1 = _t('OrderStep.DELAY_VALUE', 'Order Step, for any order, will run'); $descr2 = $field->ago(); $descr3 = $this->DeferFromSubmitTime ? _t('OrderStep.FROM_ORDER_SUBMIT_TIME', 'from the order being submitted') : _t('OrderStep.FROM_START_OF_ORDSTEP', 'from the order arriving on this step'); return $descr0 . ' ' . $descr1 . ' <span style="color: #338DC1">' . $descr2 . '</span> ' . $descr3 . '.'; } return null; // $dtF = new \DateTime('@0'); // $dtT = new \DateTime("@".$this->DeferTimeInSeconds); // // return $dtF->diff($dtT)->format('%a days, %h hours, %i minutes and %s seconds'); } /** * can this order step be delayed? * in general, if there is a customer message * we should be able to delay it. * * This method can be overridden in any orderstep * * @return bool */ protected function canBeDefered() { return $this->hasCustomerMessage(); } protected function NextOrderStep() { return OrderStep::get() ->filter(['Sort:GreaterThan' => $this->Sort]) ->First() ; } protected function PreviousOrderStep() { return OrderStep::get() ->filter(['Sort:LessThan' => $this->Sort]) ->First() ; } protected function checkValidityOfOrderSteps() { $orderStepsToInclude = EcommerceConfig::get(OrderStep::class, 'order_steps_to_include'); $codesToInclude = self::get_codes_for_order_steps_to_include(); $indexNumber = 0; if ($orderStepsToInclude && count($orderStepsToInclude)) { if ($codesToInclude && count($codesToInclude)) { foreach ($codesToInclude as $className => $code) { $className = (string) $className; $code = strtoupper($code); $filter = ['ClassName' => $className]; $indexNumber += 10; $itemCountExists = OrderStep::get()->filter($filter)->exists(); if ($itemCountExists) { //always reset code $obj = DataObject::get_one( OrderStep::class, $filter, $cacheDataObjectGetOne = false ); if ($obj->Code !== $code) { $obj->Code = $code; $obj->write(); } //replace default description $parentObj = singleton(OrderStep::class); if ($obj->Description === $parentObj->myDescription()) { $obj->Description = $obj->myDescription(); $obj->write(); } //check sorting order if ($obj->Sort !== $indexNumber) { $obj->Sort = $indexNumber; $obj->write(); } } else { $oldObjects = OrderStep::get()->filterAny(['Code' => $code]); foreach ($oldObjects as $oldObject) { DB::alteration_message('DELETING ' . $oldObject->Title . ' as this now appears obsolete', 'deleted'); $oldObject->delete(); } $obj = $className::create($filter); $obj->Code = $code; $obj->Description = $obj->myDescription(); $obj->Sort = $indexNumber; $obj->write(); DB::alteration_message("Created \"{$code}\" as {$className}.", 'created'); } $obj = DataObject::get_one( OrderStep::class, $filter, $cacheDataObjectGetOne = false ); if (! $obj) { user_error("There was an error in creating the {$code} OrderStep"); } } } } $steps = OrderStep::get(); foreach ($steps as $step) { if (! $step->Description) { $step->Description = $step->myDescription(); $step->write(); } } } /** * Explains the current order step. * * @return string */ protected function myDescription() { return _t('OrderStep.DESCRIPTION', 'No description has been provided for this step.'); } } |