From cf37f34e9bf5328bb7787a5b4f71135d8fb8abee Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 19 May 2021 16:35:28 +0100 Subject: [PATCH 1/3] Stop parsing doPayment params into class _params and access directly --- CRM/Core/Payment/AuthorizeNet.php | 110 +++++++++++++++--------------- 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/CRM/Core/Payment/AuthorizeNet.php b/CRM/Core/Payment/AuthorizeNet.php index e28bdbf900a..ab04b3b2490 100644 --- a/CRM/Core/Payment/AuthorizeNet.php +++ b/CRM/Core/Payment/AuthorizeNet.php @@ -139,19 +139,16 @@ public function doPayment(&$params, $component = 'contribute') { $newParams ); } - foreach ($newParams as $field => $value) { - $this->_setParam($field, $value); - } if (!empty($params['is_recur']) && !empty($params['contributionRecurID'])) { - $this->doRecurPayment(); + $this->doRecurPayment($newParams); $params['payment_status_id'] = array_search('Pending', $statuses); $params['payment_status'] = 'Pending'; return $params; } $postFields = []; - $authorizeNetFields = $this->_getAuthorizeNetFields(); + $authorizeNetFields = $this->_getAuthorizeNetFields($newParams); // Set up our call for hook_civicrm_paymentProcessor, // since we now have our parameters as assigned for the AIM back end. @@ -213,13 +210,15 @@ public function doPayment(&$params, $component = 'contribute') { } /** + * @param array|\Civi\Payment\PropertyBag $params + * * Submit an Automated Recurring Billing subscription. */ - public function doRecurPayment() { + public function doRecurPayment($params) { $template = CRM_Core_Smarty::singleton(); - $intervalLength = $this->_getParam('frequency_interval'); - $intervalUnit = $this->_getParam('frequency_unit'); + $intervalLength = $params['frequency_interval'] ?? ''; + $intervalUnit = $params['frequency_unit'] ?? ''; if ($intervalUnit === 'week') { $intervalLength *= 7; $intervalUnit = 'days'; @@ -253,11 +252,11 @@ public function doRecurPayment() { $template->assign('apiLogin', $this->_getParam('apiLogin')); $template->assign('paymentKey', $this->_getParam('paymentKey')); - $template->assign('refId', substr($this->_getParam('invoiceID'), 0, 20)); + $template->assign('refId', substr($params['invoiceID'] ?? '', 0, 20)); //for recurring, carry first contribution id - $template->assign('invoiceNumber', $this->_getParam('contributionID')); - $firstPaymentDate = $this->_getParam('receive_date'); + $template->assign('invoiceNumber', $params['contributionID'] ?? ''); + $firstPaymentDate = $params['receive_date'] ?? ''; if (!empty($firstPaymentDate)) { //allow for post dated payment if set in form $startDate = date_create($firstPaymentDate); @@ -279,31 +278,31 @@ public function doRecurPayment() { $template->assign('startDate', $startDate->format('Y-m-d')); - $installments = $this->_getParam('installments'); + $installments = $params['installments'] ?? ''; // for open ended subscription totalOccurrences has to be 9999 $installments = empty($installments) ? 9999 : $installments; $template->assign('totalOccurrences', $installments); - $template->assign('amount', $this->_getParam('amount')); + $template->assign('amount', $params['amount'] ?? ''); - $template->assign('cardNumber', $this->_getParam('credit_card_number')); - $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT); - $exp_year = $this->_getParam('year'); + $template->assign('cardNumber', $params['credit_card_number'] ?? ''); + $exp_month = str_pad($params['month'] ?? '', 2, '0', STR_PAD_LEFT); + $exp_year = $params['year'] ?? ''; $template->assign('expirationDate', $exp_year . '-' . $exp_month); // name rather than description is used in the tpl - see http://www.authorize.net/support/ARB_guide.pdf - $template->assign('name', $this->_getParam('description', TRUE)); - - $template->assign('email', $this->_getParam('email')); - $template->assign('contactID', $this->_getParam('contactID')); - $template->assign('billingFirstName', $this->_getParam('billing_first_name')); - $template->assign('billingLastName', $this->_getParam('billing_last_name')); - $template->assign('billingAddress', $this->_getParam('street_address', TRUE)); - $template->assign('billingCity', $this->_getParam('city', TRUE)); - $template->assign('billingState', $this->_getParam('state_province')); - $template->assign('billingZip', $this->_getParam('postal_code', TRUE)); - $template->assign('billingCountry', $this->_getParam('country')); + $template->assign('name', $this->makeXmlSafe($params['description'] ?? '')); + + $template->assign('email', $params['email']) ?? ''; + $template->assign('contactID', $params['contactID'] ?? ''); + $template->assign('billingFirstName', $params['billing_first_name'] ?? ''); + $template->assign('billingLastName', $params['billing_last_name'] ?? ''); + $template->assign('billingAddress', $this->makeXmlSafe($params['street_address'] ?? '')); + $template->assign('billingCity', $this->makeXmlSafe($params['city'] ?? '')); + $template->assign('billingState', $params['state_province'] ?? ''); + $template->assign('billingZip', $this->makeXmlSafe($params['postal_code'] ?? '')); + $template->assign('billingCountry', $params['country'] ?? ''); $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl'); @@ -325,7 +324,7 @@ public function doRecurPayment() { } // update recur processor_id with subscriptionId - CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', $this->_getParam('contributionRecurID'), + CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', $params['contributionRecurID'] ?? '', 'processor_id', $responseFields['subscriptionId'] ); //only impact of assigning this here is is can be used to cancel the subscription in an automated test @@ -336,44 +335,39 @@ public function doRecurPayment() { } /** + * @param array|\Civi\Payment\PropertyBag $params * @return array */ - public function _getAuthorizeNetFields() { - //Total amount is from the form contribution field - $amount = $this->_getParam('total_amount'); - //CRM-9894 would this ever be the case?? - if (empty($amount)) { - $amount = $this->_getParam('amount'); - } + public function _getAuthorizeNetFields($params) { $fields = []; $fields['x_login'] = $this->_getParam('apiLogin'); $fields['x_tran_key'] = $this->_getParam('paymentKey'); - $fields['x_email_customer'] = $this->_getParam('emailCustomer'); - $fields['x_first_name'] = $this->_getParam('billing_first_name'); - $fields['x_last_name'] = $this->_getParam('billing_last_name'); - $fields['x_address'] = $this->_getParam('street_address'); - $fields['x_city'] = $this->_getParam('city'); - $fields['x_state'] = $this->_getParam('state_province'); - $fields['x_zip'] = $this->_getParam('postal_code'); - $fields['x_country'] = $this->_getParam('country'); - $fields['x_customer_ip'] = $this->_getParam('ip_address'); - $fields['x_email'] = $this->_getParam('email'); - $fields['x_invoice_num'] = $this->_getParam('invoiceID'); - $fields['x_amount'] = $amount; - $fields['x_currency_code'] = $this->_getParam('currencyID'); - $fields['x_description'] = $this->_getParam('description'); - $fields['x_cust_id'] = $this->_getParam('contactID'); - if ($this->_getParam('paymentType') == 'AIM') { + $fields['x_email_customer'] = $params['emailCustomer'] ?? ''; + $fields['x_first_name'] = $params['billing_first_name'] ?? ''; + $fields['x_last_name'] = $params['billing_last_name'] ?? ''; + $fields['x_address'] = $params['street_address'] ?? ''; + $fields['x_city'] = $params['city'] ?? ''; + $fields['x_state'] = $params['state_province'] ?? ''; + $fields['x_zip'] = $params['postal_code'] ?? ''; + $fields['x_country'] = $params['country'] ?? ''; + $fields['x_customer_ip'] = $params['ip_address'] ?? ''; + $fields['x_email'] = $params['email'] ?? ''; + $fields['x_invoice_num'] = $params['invoiceID'] ?? ''; + $fields['x_amount'] = $params['total_amount'] ?? $params['amount']; + $fields['x_currency_code'] = $params['currencyID'] ?? ''; + $fields['x_description'] = $params['description'] ?? ''; + $fields['x_cust_id'] = $params['contactID'] ?? ''; + if ($this->_getParam('paymentType') === 'AIM') { $fields['x_relay_response'] = 'FALSE'; // request response in CSV format $fields['x_delim_data'] = 'TRUE'; $fields['x_delim_char'] = ','; $fields['x_encap_char'] = '"'; // cc info - $fields['x_card_num'] = $this->_getParam('credit_card_number'); - $fields['x_card_code'] = $this->_getParam('cvv2'); - $exp_month = str_pad($this->_getParam('month'), 2, '0', STR_PAD_LEFT); - $exp_year = $this->_getParam('year'); + $fields['x_card_num'] = $params['credit_card_number'] ?? ''; + $fields['x_card_code'] = $params['cvv2'] ?? ''; + $exp_month = str_pad($params['month'] ?? '', 2, '0', STR_PAD_LEFT); + $exp_year = $params['year'] ?? ''; $fields['x_exp_date'] = "$exp_month/$exp_year"; } @@ -483,11 +477,15 @@ public function _substring_between(&$haystack, $start, $end) { public function _getParam($field, $xmlSafe = FALSE) { $value = CRM_Utils_Array::value($field, $this->_params, ''); if ($xmlSafe) { - $value = str_replace(['&', '"', "'", '<', '>'], '', $value); + $value = $this->makeXmlSafe($value); } return $value; } + private function makeXmlSafe($value) { + return str_replace(['&', '"', "'", '<', '>'], '', $value) ?? ''; + } + /** * Set a field to the specified value. Value must be a scalar (int, * float, string, or boolean) From 949a5c9d62ea33659ebc78da8ca8361341bc72dd Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Wed, 19 May 2021 16:51:33 +0100 Subject: [PATCH 2/3] Multiple fixes/improvements to propertyBag --- Civi/Payment/PropertyBag.php | 51 +++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/Civi/Payment/PropertyBag.php b/Civi/Payment/PropertyBag.php index 25e4a1d3698..f3ce9304fb1 100644 --- a/Civi/Payment/PropertyBag.php +++ b/Civi/Payment/PropertyBag.php @@ -26,13 +26,23 @@ class PropertyBag implements \ArrayAccess { protected static $propMap = [ 'amount' => TRUE, + 'total_amount' => 'amount', 'billingStreetAddress' => TRUE, + 'billing_street_address' => 'billingStreetAddress', + 'street_address' => 'billingStreetAddress', 'billingSupplementalAddress1' => TRUE, 'billingSupplementalAddress2' => TRUE, 'billingSupplementalAddress3' => TRUE, 'billingCity' => TRUE, + 'billing_city' => 'billingCity', + 'city' => 'billingCity', 'billingPostalCode' => TRUE, + 'billing_postal_code' => 'billingPostalCode', + 'postal_code' => 'billingPostalCode', 'billingCounty' => TRUE, + 'billingStateProvince' => TRUE, + 'billing_state_province' => 'billingStateProvince', + 'state_province' => 'billingStateProvince', 'billingCountry' => TRUE, 'contactID' => TRUE, 'contact_id' => 'contactID', @@ -174,13 +184,16 @@ public function offsetGet($offset) { } } + // These lines are here (and not in try block) because the catch must only + // catch the case when the prop is custom. + $getter = 'get' . ucfirst($prop); if (!$this->getSuppressLegacyWarnings()) { CRM_Core_Error::deprecatedFunctionWarning( "get" . ucfirst($offset) . "()", "PropertyBag array access for core property '$offset'" ); } - return $this->get($prop, 'default'); + return $this->$getter('default'); } /** @@ -258,13 +271,18 @@ protected function handleLegacyPropNames($prop, $silent = FALSE) { if ($newName === NULL && substr($prop, -2) === '-' . \CRM_Core_BAO_LocationType::getBilling() && isset(static::$propMap[substr($prop, 0, -2)]) ) { - $newName = substr($prop, 0, -2); + $billingAddressProp = substr($prop, 0, -2); + $newName = static::$propMap[$billingAddressProp] ?? NULL; + if ($newName === TRUE) { + // Good, modern name. + return $billingAddressProp; + } } if ($newName === NULL) { if ($silent) { // Only for use by offsetExists - return; + return NULL; } throw new \InvalidArgumentException("Unknown property '$prop'."); } @@ -591,6 +609,27 @@ public function setBillingCounty($input, $label = 'default') { return $this->set('billingCounty', $label, (string) $input); } + /** + * BillingStateProvince getter. + * + * @return string + */ + public function getBillingStateProvince($label = 'default') { + return $this->get('billingStateProvince', $label); + } + + /** + * BillingStateProvince setter. + * + * Nb. we can't validate this unless we have the country ID too, so we don't. + * + * @param string $input + * @param string $label e.g. 'default' + */ + public function setBillingStateProvince($input, $label = 'default') { + return $this->set('billingStateProvince', $label, (string) $input); + } + /** * BillingCountry getter. * @@ -717,6 +756,9 @@ public function setCurrency($value, $label = 'default') { * @return string */ public function getDescription($label = 'default') { + if (!$this->has('description')) { + return ''; + } return $this->get('description', $label); } @@ -804,6 +846,9 @@ public function setFirstName($firstName, $label = 'default') { * @return string|null */ public function getInvoiceID($label = 'default') { + if (!$this->has('invoiceID')) { + $this->set('invoiceID', $label, md5(uniqid(mt_rand(), TRUE))); + } return $this->get('invoiceID', $label); } From 007d84a7cd59e7cab78f977b8da1db456f9d9aec Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Fri, 7 Aug 2020 18:26:09 +0100 Subject: [PATCH 3/3] Convert event cart to use standard payment forms --- ext/eventcart/CRM/Event/Cart/BAO/Cart.php | 71 +- .../CRM/Event/Cart/BAO/EventInCart.php | 51 +- .../CRM/Event/Cart/BAO/MerParticipant.php | 120 +-- ext/eventcart/CRM/Event/Cart/Form/Cart.php | 53 +- .../Cart/Form/Checkout/ConferenceEvents.php | 13 +- .../Form/Checkout/ParticipantsAndPrices.php | 190 ++--- .../CRM/Event/Cart/Form/Checkout/Payment.php | 793 ++++++++---------- .../CRM/Event/Cart/Form/Checkout/ThankYou.php | 65 +- .../CRM/Event/Cart/Form/MerParticipant.php | 12 +- .../CRM/Event/Cart/Page/CheckoutAJAX.php | 14 +- .../CRM/Event/Cart/StateMachine/Checkout.php | 11 +- ext/eventcart/eventcart.php | 17 +- ext/eventcart/settings/Eventcart.setting.php | 60 +- .../Event/Cart/Form/Checkout/Participant.tpl | 41 +- .../Form/Checkout/ParticipantsAndPrices.tpl | 2 + .../CRM/Event/Cart/Form/Checkout/Payment.tpl | 88 +- .../CRM/Event/Cart/Form/Checkout/ThankYou.tpl | 272 +++--- ext/eventcart/xml/Menu/Eventcart.xml | 6 + .../CRM/Financial/Form/PaymentFormsTest.php | 3 +- 19 files changed, 898 insertions(+), 984 deletions(-) diff --git a/ext/eventcart/CRM/Event/Cart/BAO/Cart.php b/ext/eventcart/CRM/Event/Cart/BAO/Cart.php index 11111711547..fd142c6442c 100644 --- a/ext/eventcart/CRM/Event/Cart/BAO/Cart.php +++ b/ext/eventcart/CRM/Event/Cart/BAO/Cart.php @@ -11,6 +11,12 @@ class CRM_Event_Cart_BAO_Cart extends CRM_Event_Cart_DAO_Cart { */ public $events_in_carts = []; + /** + * The default contact ID to use when creating a participant + * @var int + */ + public $defaultParticipantContactID = NULL; + /** * @param array $params * @@ -39,6 +45,7 @@ public function add_event($event_id) { 'event_id' => $event_id, 'event_cart_id' => $this->id, ]; + /** @var \CRM_Event_Cart_BAO_EventInCart $event_in_cart */ $event_in_cart = CRM_Event_Cart_BAO_EventInCart::create($params); $event_in_cart->load_associations($this); $this->events_in_carts[$event_in_cart->event_id] = $event_in_cart; @@ -46,14 +53,14 @@ public function add_event($event_id) { } /** - * @param $participant + * @param array $participantParams */ - public function add_participant_to_cart($participant) { - $event_in_cart = $this->get_event_in_cart_by_event_id($participant->event_id); + public function add_participant_to_cart($participantParams) { + $event_in_cart = $this->get_event_in_cart_by_event_id($participantParams['event_id']); if (!$event_in_cart) { - $event_in_cart = $this->add_event($participant->event_id); + $event_in_cart = $this->add_event($participantParams['event_id']); } - $event_in_cart->add_participant($participant); + $event_in_cart->add_participant($participantParams); $event_in_cart->save(); } @@ -64,18 +71,7 @@ public function add_participant_to_cart($participant) { * @throws Exception */ public static function create($params) { - $transaction = new CRM_Core_Transaction(); - - $cart = self::add($params); - - if (is_a($cart, 'CRM_Core_Error')) { - $transaction->rollback(); - throw new CRM_Core_Exception(ts('There was an error creating an event cart')); - } - - $transaction->commit(); - - return $cart; + return self::add($params); } /** @@ -109,9 +105,9 @@ public static function find_by_params($params) { public static function find_or_create_for_current_session() { $session = CRM_Core_Session::singleton(); $event_cart_id = $session->get('event_cart_id'); - $userID = $session->get('userID'); + $userID = CRM_Core_Session::getLoggedInContactID(); $cart = FALSE; - if (!is_null($event_cart_id)) { + if (!empty($event_cart_id)) { $cart = self::find_uncompleted_by_id($event_cart_id); if ($cart && $userID) { if (!$cart->user_id) { @@ -119,7 +115,6 @@ public static function find_or_create_for_current_session() { if ($saved_cart) { $cart->adopt_participants($saved_cart->id); $saved_cart->delete(); - $cart->load_associations(); } else { $cart->user_id = $userID; @@ -140,6 +135,9 @@ public static function find_or_create_for_current_session() { } $session->set('event_cart_id', $cart->id); } + // cid can be 0 for new contact + $cart->defaultParticipantContactID = CRM_Utils_Request::retrieveValue('cid', 'Integer', CRM_Core_Session::getLoggedInContactID()); + $cart->load_associations(); return $cart; } @@ -165,10 +163,10 @@ public static function find_uncompleted_by_user_id($user_id) { * @return array */ public function get_main_events_in_carts() { - //return CRM_Event_Cart_BAO_EventInCart::find_all_by_params( array('main_conference_event_id' $all = []; + /** @var \CRM_Event_Cart_BAO_EventInCart $event_in_cart */ foreach ($this->events_in_carts as $event_in_cart) { - if (!$event_in_cart->is_child_event()) { + if (!$event_in_cart->is_child_event($event_in_cart->event_id)) { $all[] = $event_in_cart; } } @@ -219,6 +217,7 @@ public static function compare_event_dates($event_in_cart_1, $event_in_cart_2) { */ public function get_subparticipants($main_participant) { $subparticipants = []; + /** @var \CRM_Event_Cart_BAO_EventInCart $event_in_cart */ foreach ($this->events_in_carts as $event_in_cart) { if ($event_in_cart->is_child_event($main_participant->event_id)) { foreach ($event_in_cart->participants as $participant) { @@ -246,7 +245,7 @@ public function get_event_in_cart_by_event_id($event_id) { * * @return null */ - public function &get_event_in_cart_by_id($event_in_cart_id) { + public function get_event_in_cart_by_id($event_in_cart_id) { foreach ($this->events_in_carts as $event_in_cart) { if ($event_in_cart->id == $event_in_cart_id) { return $event_in_cart; @@ -272,6 +271,7 @@ public function load_associations() { } $this->associations_loaded = TRUE; $this->events_in_carts = CRM_Event_Cart_BAO_EventInCart::find_all_by_event_cart_id($this->id); + /** @var \CRM_Event_Cart_BAO_EventInCart $event_in_cart */ foreach ($this->events_in_carts as $event_in_cart) { $event_in_cart->load_associations($this); } @@ -343,4 +343,29 @@ public function adopt_participants($from_cart_id) { CRM_Core_DAO::executeQuery($sql, $params); } + /** + * Get payment processors. + * + * This differs from the option value in that we append description for + * disambiguation. + * + * @return array + * @throws \CiviCRM_API3_Exception + */ + public static function getPaymentProcessors(): array { + $results = civicrm_api3('PaymentProcessor', 'get', [ + 'is_test' => 0, + 'return' => ['id', 'name', 'description', 'domain_id'], + ]); + + $processors = []; + foreach ($results['values'] as $processorID => $details) { + $processors[$processorID] = $details['name']; + if (!empty($details['description'])) { + $processors[$processorID] .= ' : ' . $details['description']; + } + } + return $processors; + } + } diff --git a/ext/eventcart/CRM/Event/Cart/BAO/EventInCart.php b/ext/eventcart/CRM/Event/Cart/BAO/EventInCart.php index abb9bb4ce6a..55a1e5a6869 100644 --- a/ext/eventcart/CRM/Event/Cart/BAO/EventInCart.php +++ b/ext/eventcart/CRM/Event/Cart/BAO/EventInCart.php @@ -1,5 +1,7 @@ participants[$participant->id] = $participant; + public function add_participant($participantParams) { + $participantParams['cart_id'] = $participantParams['cart_id'] ?? $this->event_cart_id; + $participantParams['event_id'] = $participantParams['event_id'] ?? $this->event_id; + + $merParticipantObject = CRM_Event_Cart_BAO_MerParticipant::create($participantParams); + $this->participants[$merParticipantObject->id] = $merParticipantObject; } /** @@ -164,7 +176,7 @@ public static function part_key($participant) { } /** - * @param null $event_cart + * @param CRM_Event_Cart_BAO_Cart $event_cart */ public function load_associations($event_cart = NULL) { if ($this->assocations_loaded) { @@ -175,7 +187,7 @@ public function load_associations($event_cart = NULL) { $defaults = []; $this->event = CRM_Event_BAO_Event::retrieve($params, $defaults); - if ($event_cart != NULL) { + if ($event_cart !== NULL) { $this->event_cart = $event_cart; $this->event_cart_id = $event_cart->id; } @@ -183,10 +195,19 @@ public function load_associations($event_cart = NULL) { $this->event_cart = CRM_Event_Cart_BAO_Cart::find_by_id($this->event_cart_id); } - $participants = CRM_Event_Cart_BAO_MerParticipant::find_all_by_event_and_cart_id($this->event_id, $this->event_cart->id); - foreach ($participants as $participant) { + $this->participants = CRM_Event_Cart_BAO_MerParticipant::find_all_by_event_and_cart_id($this->event_id, $this->event_cart->id); + if (empty($this->participants)) { + $participantParams = ['event_id' => $this->event_id, 'cart_id' => $this->event_cart->id]; + if ($this->event_cart->defaultParticipantContactID) { + $participantParams['contact_id'] = $this->event_cart->defaultParticipantContactID; + } + $newParticipant = CRM_Event_Cart_BAO_MerParticipant::create($participantParams); + $this->event_cart->defaultParticipantContactID = $newParticipant->contact_id; + $this->participants[$newParticipant->id] = $newParticipant; + } + /** @var \CRM_Event_BAO_Participant $participant */ + foreach ($this->participants as $participant) { $participant->load_associations(); - $this->add_participant($participant); } } @@ -246,7 +267,7 @@ public function offsetGet($offset) { return $this->id; } if ($offset == 'main_conference_event_id') { - return $this->main_conference_event_id; + return $this->main_conference_event_id ?? NULL; } $fields = &$this->fields(); return $fields[$offset]; @@ -285,7 +306,6 @@ public function waiting_participants() { */ public static function get_registration_link($event_id) { $cart = CRM_Event_Cart_BAO_Cart::find_or_create_for_current_session(); - $cart->load_associations(); $event_in_cart = $cart->get_event_in_cart_by_event_id($event_id); if ($event_in_cart) { @@ -299,7 +319,7 @@ public static function get_registration_link($event_id) { return [ 'label' => ts("Add to Cart"), 'path' => 'civicrm/event/add_to_cart', - 'query' => "reset=1&id={$event_id}", + 'query' => "reset=1&id={$event_id}&cid={$cart->defaultParticipantContactID}", ]; } } @@ -316,13 +336,8 @@ public function is_parent_event() { * * @return bool */ - public function is_child_event($parent_event_id = NULL) { - if ($parent_event_id == NULL) { - return $this->event->parent_event_id; - } - else { - return $this->event->parent_event_id == $parent_event_id; - } + public function is_child_event($parent_event_id) { + return ($this->event->parent_event_id === $parent_event_id); } } diff --git a/ext/eventcart/CRM/Event/Cart/BAO/MerParticipant.php b/ext/eventcart/CRM/Event/Cart/BAO/MerParticipant.php index 4cdf4e341b9..3fdbeb14d01 100644 --- a/ext/eventcart/CRM/Event/Cart/BAO/MerParticipant.php +++ b/ext/eventcart/CRM/Event/Cart/BAO/MerParticipant.php @@ -30,58 +30,32 @@ class CRM_Event_Cart_BAO_MerParticipant extends CRM_Event_BAO_Participant { public $cart = NULL; /** - * XXX. - * @param null $participant + * @param array $participant */ - public function __construct($participant = NULL) { + public function __construct($participant = []) { parent::__construct(); - $a = (array) $participant; - $this->copyValues($a); + $this->copyValues($participant); - $this->email = $a['email'] ?? NULL; + $this->email = $participant['email'] ?? NULL; } /** - * @param array $params + * @param array $participantParams + * You MUST pass in event_id and cart_id * * @return CRM_Event_Cart_BAO_MerParticipant * @throws Exception */ - public static function create(&$params) { - $participantParams = [ - 'id' => $params['id'] ?? NULL, - 'role_id' => self::get_attendee_role_id(), - 'status_id' => self::get_pending_in_cart_status_id(), - 'contact_id' => $params['contact_id'], - 'event_id' => $params['event_id'], - 'cart_id' => $params['cart_id'], - ]; - $participant = CRM_Event_BAO_Participant::create($participantParams); - - if (is_a($participant, 'CRM_Core_Error')) { - throw new CRM_Core_Exception(ts('There was an error creating a cart participant')); + public static function create(&$participantParams) { + if (empty($participantParams['event_id'] || empty($participantParams['cart_id']))) { + throw new CRM_Core_Exception('MerParticipant create: Missing required params: event_id/cart_id'); } + $participantParams['contact_id'] = $participantParams['contact_id'] ?? CRM_Event_Cart_Form_Cart::find_or_create_contact(); + $participantParams['role_id'] = $participantParams['role_id'] ?? CRM_Core_PseudoConstant::getKey('CRM_Event_BAO_Participant', 'role_id', 'Attendee'); + $participantParams['status_id'] = $participantParams['status_id'] ?? CRM_Core_PseudoConstant::getKey('CRM_Event_BAO_Participant', 'status_id', 'Pending in cart'); - $mer_participant = new CRM_Event_Cart_BAO_MerParticipant($participant); - return $mer_participant; - } - - /** - * @return mixed - */ - public static function get_attendee_role_id() { - $roles = CRM_Event_PseudoConstant::participantRole(NULL, "v.label='Attendee'"); - $role_names = array_keys($roles); - return end($role_names); - } - - /** - * @return mixed - */ - public static function get_pending_in_cart_status_id() { - $status_types = CRM_Event_PseudoConstant::participantStatus(NULL, "name='Pending in cart'"); - $status_names = array_keys($status_types); - return end($status_names); + $participant = reset(civicrm_api3('Participant', 'create', $participantParams)['values']); + return new CRM_Event_Cart_BAO_MerParticipant($participant); } /** @@ -93,7 +67,14 @@ public static function find_all_by_cart_id($event_cart_id) { if ($event_cart_id == NULL) { return NULL; } - return self::find_all_by_params(['cart_id' => $event_cart_id]); + $participants = \Civi\Api4\Participant::get(FALSE) + ->addWhere('cart_id', '=', $event_cart_id) + ->execute(); + $result = []; + foreach ($participants as $participant) { + $result[] = new CRM_Event_Cart_BAO_MerParticipant($participant); + } + return $result; } /** @@ -106,22 +87,13 @@ public static function find_all_by_event_and_cart_id($event_id, $event_cart_id) if ($event_cart_id == NULL) { return NULL; } - return self::find_all_by_params(['event_id' => $event_id, 'cart_id' => $event_cart_id]); - } - - /** - * @param array $params - * - * @return array - */ - public static function find_all_by_params($params) { - $participant = new CRM_Event_BAO_Participant(); - $participant->copyValues($params); + $participants = \Civi\Api4\Participant::get(FALSE) + ->addWhere('event_id', '=', $event_id) + ->addWhere('cart_id', '=', $event_cart_id) + ->execute(); $result = []; - if ($participant->find()) { - while ($participant->fetch()) { - $result[] = new CRM_Event_Cart_BAO_MerParticipant(clone($participant)); - } + foreach ($participants as $participant) { + $result[$participant['id']] = new CRM_Event_Cart_BAO_MerParticipant($participant); } return $result; } @@ -129,16 +101,23 @@ public static function find_all_by_params($params) { /** * @param int $id * - * @return mixed + * @return \CRM_Event_Cart_BAO_MerParticipant */ public static function get_by_id($id) { - $results = self::find_all_by_params(['id' => $id]); - return array_pop($results); + $participant = \Civi\Api4\Participant::get(FALSE) + ->addWhere('id', '=', $id) + ->execute() + ->first(); + return new CRM_Event_Cart_BAO_MerParticipant($participant); } public function load_associations() { - $contact_details = CRM_Contact_BAO_Contact::getContactDetails($this->contact_id); - $this->email = $contact_details[1]; + $email = \Civi\Api4\Email::get(FALSE) + ->addWhere('contact_id', '=', $this->contact_id) + ->addOrderBy('is_primary', 'DESC') + ->execute() + ->first(); + $this->email = $email['email'] ?? NULL; } /** @@ -153,25 +132,6 @@ public function get_participant_index() { return $index + 1; } - /** - * @param $contact - * - * @return null - */ - public static function billing_address_from_contact($contact) { - foreach ($contact->address as $loc) { - if ($loc['is_billing']) { - return $loc; - } - } - foreach ($contact->address as $loc) { - if ($loc['is_primary']) { - return $loc; - } - } - return NULL; - } - /** * @return CRM_Event_Cart_Form_MerParticipant */ diff --git a/ext/eventcart/CRM/Event/Cart/Form/Cart.php b/ext/eventcart/CRM/Event/Cart/Form/Cart.php index 048c2ba5b9c..c68b207c034 100644 --- a/ext/eventcart/CRM/Event/Cart/Form/Cart.php +++ b/ext/eventcart/CRM/Event/Cart/Form/Cart.php @@ -10,15 +10,12 @@ class CRM_Event_Cart_Form_Cart extends CRM_Core_Form { */ public $cart; - public $_action; public $contact; public $event_cart_id = NULL; - public $_mode; public $participants; public function preProcess() { $this->_action = CRM_Utils_Request::retrieveValue('action', 'String'); - $this->_mode = 'live'; $this->loadCart(); $this->checkWaitingList(); @@ -49,16 +46,10 @@ public function loadCart() { public function stub_out_and_inherit() { $transaction = new CRM_Core_Transaction(); + /** @var CRM_Event_Cart_BAO_EventInCart $event_in_cart */ foreach ($this->cart->get_main_events_in_carts() as $event_in_cart) { if (empty($event_in_cart->participants)) { - $participant_params = [ - 'cart_id' => $this->cart->id, - 'event_id' => $event_in_cart->event_id, - 'contact_id' => self::find_or_create_contact(), - ]; - $participant = CRM_Event_Cart_BAO_MerParticipant::create($participant_params); - $participant->save(); - $event_in_cart->add_participant($participant); + $event_in_cart->load_associations(); } $event_in_cart->save(); } @@ -96,28 +87,6 @@ public function checkEventCapacity($event_id) { } } - /** - * @return int - * @throws \CRM_Core_Exception - */ - public function getContactID() { - $tempID = CRM_Utils_Request::retrieveValue('cid', 'Positive'); - - //check if this is a checksum authentication - $userChecksum = CRM_Utils_Request::retrieveValue('cs', 'String'); - if ($userChecksum) { - //check for anonymous user. - $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum); - if ($validUser) { - return $tempID; - } - } - - // check if the user is registered and we have a contact ID - $session = CRM_Core_Session::singleton(); - return $session->get('userID'); - } - /** * @param $fields * @@ -129,27 +98,27 @@ public static function find_contact($fields) { /** * @param array $fields + * @param int $contactID * * @return int|mixed|null */ - public static function find_or_create_contact($fields = []) { - $contact_id = self::find_contact($fields); - - if ($contact_id) { - return $contact_id; + public static function find_or_create_contact($fields = [], $contactID = NULL) { + if (!$contactID) { + $contactID = self::find_contact($fields); } + $contact_params = [ 'email-Primary' => $fields['email'] ?? NULL, 'first_name' => $fields['first_name'] ?? NULL, 'last_name' => $fields['last_name'] ?? NULL, - 'is_deleted' => CRM_Utils_Array::value('is_deleted', $fields, TRUE), + 'is_deleted' => $contactID ? FALSE : TRUE, ]; $no_fields = []; - $contact_id = CRM_Contact_BAO_Contact::createProfileContact($contact_params, $no_fields, NULL); - if (!$contact_id) { + $contactID = CRM_Contact_BAO_Contact::createProfileContact($contact_params, $no_fields, $contactID); + if (!$contactID) { CRM_Core_Session::setStatus(ts("Could not create or match a contact with that email address. Please contact the webmaster."), '', 'error'); } - return $contact_id; + return $contactID; } /** diff --git a/ext/eventcart/CRM/Event/Cart/Form/Checkout/ConferenceEvents.php b/ext/eventcart/CRM/Event/Cart/Form/Checkout/ConferenceEvents.php index 8f68053a1df..08ae5061abe 100644 --- a/ext/eventcart/CRM/Event/Cart/Form/Checkout/ConferenceEvents.php +++ b/ext/eventcart/CRM/Event/Cart/Form/Checkout/ConferenceEvents.php @@ -132,15 +132,14 @@ public function postProcess() { if (!$session_event_id) { continue; } + /** @var \CRM_Event_Cart_BAO_EventInCart $event_in_cart */ $event_in_cart = $this->cart->add_event($session_event_id); - $values = []; - CRM_Core_DAO::storeValues($this->main_participant, $values); - $values['id'] = NULL; - $values['event_id'] = $event_in_cart->event_id; - $participant = CRM_Event_Cart_BAO_MerParticipant::create($values); - $participant->save(); - $event_in_cart->add_participant($participant); + $participantParams = []; + CRM_Core_DAO::storeValues($this->main_participant, $participantParams); + $participantParams['id'] = NULL; + $participantParams['event_id'] = $event_in_cart->event_id; + $event_in_cart->add_participant($participantParams); } $this->cart->save(); } diff --git a/ext/eventcart/CRM/Event/Cart/Form/Checkout/ParticipantsAndPrices.php b/ext/eventcart/CRM/Event/Cart/Form/Checkout/ParticipantsAndPrices.php index 5067ccf33d0..24ea57345bb 100644 --- a/ext/eventcart/CRM/Event/Cart/Form/Checkout/ParticipantsAndPrices.php +++ b/ext/eventcart/CRM/Event/Cart/Form/Checkout/ParticipantsAndPrices.php @@ -1,5 +1,7 @@ replaceUserContext( + CRM_Utils_System::url('civicrm/event/cart_checkout', [ + 'qf_ParticipantsAndPrices_display' => 1, + ]) + ); $this->cid = CRM_Utils_Request::retrieve('cid', 'Positive', $this); if (!isset($this->cid) || $this->cid > 0) { @@ -41,7 +48,6 @@ public function buildQuickForm() { [ 'type' => 'upload', 'name' => ts('Continue'), - 'spacing' => '         ', 'isDefault' => TRUE, ], ] @@ -115,72 +121,43 @@ public function build_price_options($event) { */ public function validate() { parent::validate(); - if ($this->_errors) { - return FALSE; - } $this->cart->load_associations(); - $fields = $this->_submitValues; foreach ($this->cart->get_main_events_in_carts() as $event_in_cart) { - $price_set_id = CRM_Event_BAO_Event::usesPriceSet($event_in_cart->event_id); - if ($price_set_id) { - $priceField = new CRM_Price_DAO_PriceField(); - $priceField->price_set_id = $price_set_id; - $priceField->find(); - - $check = []; - - while ($priceField->fetch()) { - if (!empty($fields["event_{$event_in_cart->event_id}_price_{$priceField->id}"])) { - $check[] = $priceField->id; - } - } - - //XXX - if (empty($check)) { - $this->_errors['_qf_default'] = ts("Select at least one option from Price Levels."); - } - - $lineItem = []; - if (is_array($this->_values['fee']['fields'])) { - CRM_Price_BAO_PriceSet::processAmount($this->_values['fee']['fields'], $fields, $lineItem); - //XXX total... - if ($fields['amount'] < 0) { - $this->_errors['_qf_default'] = ts("Price Levels can not be less than zero. Please select the options accordingly"); - } - } - } - // Validate if participant is already registered if ($event_in_cart->event->allow_same_participant_emails) { continue; } foreach ($event_in_cart->participants as $mer_participant) { - $participant_fields = $fields['event'][$event_in_cart->event_id]['participant'][$mer_participant->id]; + $participant_fields = $this->_submitValues['field'][$mer_participant->id]; //TODO what to do when profile responses differ for the same contact? $contact_id = self::find_contact($participant_fields); if ($contact_id) { - $participant = new CRM_Event_BAO_Participant(); - $participant->event_id = $event_in_cart->event_id; - $participant->contact_id = $contact_id; $statusTypes = CRM_Event_PseudoConstant::participantStatus(NULL, 'is_counted = 1'); - $participant->find(); - while ($participant->fetch()) { - if (array_key_exists($participant->status_id, $statusTypes)) { - $form = $mer_participant->get_form(); - $this->_errors[$form->html_field_name('email')] = ts("The participant %1 is already registered for %2 (%3).", [ - 1 => $participant_fields['email'], - 2 => $event_in_cart->event->title, - 3 => $event_in_cart->event->start_date, - ]); - } + $participant = Participant::get(FALSE) + ->addWhere('event_id', '=', $event_in_cart->event_id) + ->addWhere('contact_id', '=', $contact_id) + ->addWhere('status_id', 'IN', array_keys($statusTypes)) + ->execute() + ->first(); + if (!empty($participant)) { + $form = $mer_participant->get_form(); + $this->_errors[$form->html_field_name('email')] = ts("The participant %1 is already registered for %2 (%3).", [ + 1 => $participant_fields['email-Primary'], + 2 => $event_in_cart->event->title, + 3 => $event_in_cart->event->start_date, + ]); } } } } - return empty($this->_errors); + if (empty($this->_errors)) { + return TRUE; + } + CRM_Core_Error::statusBounce(implode('
', $this->_errors)); + return FALSE; } /** @@ -192,26 +169,17 @@ public function setDefaultValues() { $this->loadCart(); $defaults = []; + /** @var \CRM_Event_Cart_BAO_MerParticipant $participant */ foreach ($this->cart->get_main_event_participants() as $participant) { - $form = $participant->get_form(); - if (empty($participant->email) - && ($participant->get_participant_index() == 1) - && ($this->cid != 0) - ) { - $defaults = []; - $params = ['id' => $this->cid]; - $contact = CRM_Contact_BAO_Contact::retrieve($params, $defaults); - $participant->contact_id = $this->cid; - $participant->save(); - $participant->email = self::primary_email_from_contact($contact); - } - elseif ($this->cid == 0 - && $participant->contact_id == self::getContactID() - ) { + /** @var \CRM_Event_Cart_Form_MerParticipant $merParticipantForm */ + $merParticipantForm = $participant->get_form(); + if (($this->cid == 0) && ($participant->contact_id == self::getContactID())) { + // Create a new contact $participant->email = NULL; $participant->contact_id = self::find_or_create_contact(); } - $defaults += $form->setDefaultValues(); + + $defaults += $merParticipantForm->setDefaultValues(); //Set price defaults if any foreach ($this->cart->get_main_events_in_carts() as $event_in_cart) { $event_id = $event_in_cart->event_id; @@ -246,67 +214,55 @@ public function setDefaultValues() { * Post process function. */ public function postProcess() { - if (!array_key_exists('event', $this->_submitValues)) { - return; - } - // XXX de facto primary key - $email_to_contact_id = []; - foreach ($this->_submitValues['event'] as $event_id => $participants) { - foreach ($participants['participant'] as $participant_id => $fields) { - if (array_key_exists($fields['email'], $email_to_contact_id)) { - $contact_id = $email_to_contact_id[$fields['email']]; - } - else { - $contact_id = self::find_or_create_contact($fields); - $email_to_contact_id[$fields['email']] = $contact_id; - } + $submittedValues = $this->controller->exportValues($this->_name); - $participant = $this->cart->get_event_in_cart_by_event_id($event_id)->get_participant_by_id($participant_id); - if ($participant->contact_id && $contact_id != $participant->contact_id) { - $defaults = []; - $params = ['id' => $participant->contact_id]; - $temporary_contact = CRM_Contact_BAO_Contact::retrieve($params, $defaults); + foreach ($submittedValues['field'] as $participant_id => $fields) { + $participant = Participant::get(FALSE) + ->addWhere('id', '=', $participant_id) + ->execute() + ->first(); - foreach ($this->cart->get_subparticipants($participant) as $subparticipant) { - $subparticipant->contact_id = $contact_id; - $subparticipant->save(); - } - - $participant->contact_id = $contact_id; - $participant->save(); + // Email sometimes gets passed in as eg. "email-Primary" + // Normalise it to "email" + foreach ($fields as $key => $value) { + if (substr($key, 0, 5) === 'email') { + $fields['email'] = $fields[$key]; + unset($fields[$key]); + break; + } + } - if ($temporary_contact->is_deleted) { - // ARGH a permissions check prevents us from using skipUndelete, - // so we potentially leave records pointing to this contact for now - // CRM_Contact_BAO_Contact::deleteContact($temporary_contact->id); - $temporary_contact->delete(); - } + $contact_id = self::find_or_create_contact($fields, $participant['contact_id']); + $participant = $this->cart->get_event_in_cart_by_event_id($participant['event_id'])->get_participant_by_id($participant_id); + if ($participant->contact_id && $contact_id != $participant->contact_id) { + foreach ($this->cart->get_subparticipants($participant) as $subparticipant) { + $subparticipant->contact_id = $contact_id; + $subparticipant->save(); } - //TODO security check that participant ids are already in this cart - $participant_params = [ - 'id' => $participant_id, - 'cart_id' => $this->cart->id, - 'event_id' => $event_id, - 'contact_id' => $contact_id, - //'registered_by_id' => $this->cart->user_id, - 'email' => $fields['email'], - ]; - $participant = new CRM_Event_Cart_BAO_MerParticipant($participant_params); + $participant->contact_id = $contact_id; $participant->save(); - $this->cart->add_participant_to_cart($participant); + } - if (array_key_exists('field', $this->_submitValues) && array_key_exists($participant_id, $this->_submitValues['field'])) { - $custom_fields = array_merge($participant->get_form()->get_participant_custom_data_fields()); + $participantParams = [ + 'id' => $participant_id, + 'cart_id' => $this->cart->id, + 'event_id' => $participant->event_id, + 'contact_id' => $contact_id, + 'email' => $fields['email'], + ]; + $this->cart->add_participant_to_cart($participantParams); - CRM_Contact_BAO_Contact::createProfileContact($this->_submitValues['field'][$participant_id], $custom_fields, $contact_id); + if (array_key_exists('field', $this->_submitValues) && array_key_exists($participant_id, $this->_submitValues['field'])) { + $custom_fields = $participant->get_form()->get_participant_custom_data_fields(); - CRM_Core_BAO_CustomValueTable::postProcess($this->_submitValues['field'][$participant_id], - 'civicrm_participant', - $participant_id, - 'Participant' - ); - } + CRM_Contact_BAO_Contact::createProfileContact($this->_submitValues['field'][$participant_id], $custom_fields, $contact_id); + + CRM_Core_BAO_CustomValueTable::postProcess($this->_submitValues['field'][$participant_id], + 'civicrm_participant', + $participant_id, + 'Participant' + ); } } $this->cart->save(); diff --git a/ext/eventcart/CRM/Event/Cart/Form/Checkout/Payment.php b/ext/eventcart/CRM/Event/Cart/Form/Checkout/Payment.php index 5f0aa4100cb..82c84bbdc88 100644 --- a/ext/eventcart/CRM/Event/Cart/Form/Checkout/Payment.php +++ b/ext/eventcart/CRM/Event/Cart/Form/Checkout/Payment.php @@ -1,9 +1,14 @@ $participant->id, - 'event_id' => $event->id, - 'register_date' => date('YmdHis'), - 'source' => CRM_Utils_Array::value('participant_source', $params, $this->description), - 'is_pay_later' => $this->is_pay_later, - 'fee_amount' => CRM_Utils_Array::value('amount', $params, 0), - 'fee_currency' => $params['currencyID'] ?? NULL, - ]; - - if ($participant->must_wait) { - $participant_status = 'On waitlist'; - } - elseif (!empty($params['is_pay_later'])) { - $participant_status = 'Pending from pay later'; - } - else { - $participant_status = 'Registered'; - } - $participantParams['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Event_BAO_Participant', 'status_id', $participant_status); - $participantParams['participant_status'] = CRM_Core_PseudoConstant::getLabel('CRM_Event_BAO_Participant', 'status_id', $participantParams['status_id']); - - $this->assign('isOnWaitlist', $participant->must_wait); - - $participantParams['is_test'] = 0; - if ($this->_action & CRM_Core_Action::PREVIEW || CRM_Utils_Array::value('mode', $params) == 'test') { - $participantParams['is_test'] = 1; - } - - $transaction = new CRM_Core_Transaction(); - - $participant->copyValues($participantParams); - $participant->save(); - - if (!empty($params['contributionID'])) { - $participantPaymentParams = [ - 'participant_id' => $participant->id, - 'contribution_id' => $params['contributionID'], - ]; - civicrm_api3('ParticipantPayment', 'create', $participantPaymentParams); - } - - $transaction->commit(); - - $event_values = []; - CRM_Core_DAO::storeValues($event, $event_values); - - $location = []; - if (CRM_Utils_Array::value('is_show_location', $event_values) == 1) { - $locationParams = [ - 'entity_id' => $participant->event_id, - 'entity_table' => 'civicrm_event', - ]; - $location = CRM_Core_BAO_Location::getValues($locationParams, TRUE); - CRM_Core_BAO_Address::fixAddress($location['address'][1]); - } - - list($pre_id, $post_id) = CRM_Event_Cart_Form_MerParticipant::get_profile_groups($participant->event_id); - $payer_values = [ - 'email' => '', - 'name' => '', - ]; - if ($this->payer_contact_id) { - $payer_contact_details = CRM_Contact_BAO_Contact::getContactDetails($this->payer_contact_id); - $payer_values = [ - 'email' => $payer_contact_details[1], - 'name' => $payer_contact_details[0], - ]; - } - $values = [ - 'params' => [$participant->id => $participantParams], - 'event' => $event_values, - 'location' => $location, - 'custom_pre_id' => $pre_id, - 'custom_post_id' => $post_id, - 'payer' => $payer_values, - ]; - CRM_Event_BAO_Event::sendMail($participant->contact_id, $values, $participant->id); - - return $participant; - } - - /** - * Build payment fields. + * @var array */ - public function buildPaymentFields() { - $payment_processor_id = NULL; - $can_pay_later = TRUE; - $pay_later_text = ""; - $this->pay_later_receipt = ""; - foreach ($this->cart->get_main_events_in_carts() as $event_in_cart) { - if ($payment_processor_id === NULL && $event_in_cart->event->payment_processor !== NULL) { - $payment_processor_id = $event_in_cart->event->payment_processor; - $this->financial_type_id = $event_in_cart->event->financial_type_id; - $this->_values['currency'] = $event_in_cart->event->currency; - } - else { - if ($event_in_cart->event->payment_processor !== NULL && $event_in_cart->event->payment_processor !== $payment_processor_id) { - CRM_Core_Error::statusBounce(ts('When registering for multiple events all events must use the same payment processor. ')); - } - } - if ($payment_processor_id) { - $can_pay_later = FALSE; - } - elseif ($event_in_cart->event->is_pay_later) { - //XXX - $pay_later_text = $event_in_cart->event->pay_later_text; - $this->pay_later_receipt = $event_in_cart->event->pay_later_receipt; - } - else { - CRM_Core_Error::statusBounce(ts('A payment processor must be selected for this event registration page, or the event must be configured to give users the option to pay later (contact the site administrator for assistance).')); - } - } - - if ($can_pay_later) { - $this->addElement('checkbox', 'is_pay_later', $pay_later_text); - $this->addElement('checkbox', 'payment_completed', ts('Payment Completed')); - $this->assign('pay_later_instructions', $this->pay_later_receipt); - } - else { - $this->_paymentProcessorIDs = [$payment_processor_id]; - $this->assignPaymentProcessor(FALSE); - CRM_Core_Payment_Form::buildPaymentForm($this, $this->_paymentProcessor, FALSE, FALSE); - } - $this->assign('currency', $this->getCurrency()); - } + protected $_values = []; /** * Build QuickForm. */ public function buildQuickForm() { - $this->line_items = []; - $this->sub_total = 0; - $this->_price_values = $this->getValuesForPage('ParticipantsAndPrices'); + // @todo we should replace this with an optional profile + $this->add('text', 'first_name', 'First Name', '', TRUE); + $this->add('text', 'last_name', 'Last Name', '', TRUE); + $this->add('text', 'email', 'Billing Email', '', TRUE); - // iterate over each event in cart - foreach ($this->cart->get_main_events_in_carts() as $event_in_cart) { - $this->process_event_line_item($event_in_cart); - foreach ($this->cart->get_events_in_carts_by_main_event_id($event_in_cart->event_id) as $subevent) { - $this->process_event_line_item($subevent, 'subevent'); - } - } - - $this->total = $this->sub_total; - $this->payment_required = ($this->total > 0); - $this->assign('payment_required', $this->payment_required); - $this->assign('line_items', $this->line_items); - $this->assign('sub_total', $this->sub_total); - $this->assign('total', $this->total); - $buttons = []; - $buttons[] = [ - 'name' => ts('Go Back'), - 'type' => 'back', - ]; - $buttons[] = [ - 'isDefault' => TRUE, - 'name' => ts('Complete Transaction'), - 'type' => 'next', - ]; + $this->addFormRule(['CRM_Event_Cart_Form_Checkout_Payment', 'formRule'], $this); + $this->addButtons([ + [ + 'name' => ts('Go Back'), + 'type' => 'back', + ], + [ + 'isDefault' => TRUE, + 'name' => ts('Complete Transaction'), + 'type' => 'next', + ], + ]); - if ($this->total) { - $this->add('text', 'billing_contact_email', 'Billing Email', '', TRUE); - $this->assign('collect_billing_email', TRUE); + if ($this->isPaymentRequired()) { + CRM_Core_Payment_ProcessorForm::buildQuickForm($this); + $this->addPaymentProcessorFieldsToForm(); } - $this->addButtons($buttons); - $this->addFormRule(['CRM_Event_Cart_Form_Checkout_Payment', 'formRule'], $this); - - if ($this->payment_required) { - $this->buildPaymentFields(); + // Add reCAPTCHA + if (!CRM_Core_Session::getLoggedInContactID() && is_callable(['CRM_Utils_ReCAPTCHA', 'enableCaptchaOnForm'])) { + CRM_Utils_ReCAPTCHA::enableCaptchaOnForm($this); } } @@ -216,6 +73,7 @@ public function process_event_line_item(&$event_in_cart, $class = NULL) { $cost = 0; $price_set_id = CRM_Price_BAO_PriceSet::getFor("civicrm_event", $event_in_cart->event_id); $amount_level = NULL; + $price_details = []; if ($price_set_id) { $event_price_values = []; foreach ($this->_price_values as $key => $value) { @@ -297,16 +155,7 @@ public function add_line_item($event_in_cart, $class = NULL) { * @param array $events_in_cart * @param array $params */ - public function emailReceipt($events_in_cart, $params) { - $contact_details = CRM_Contact_BAO_Contact::getContactDetails($this->payer_contact_id); - $state_province = new CRM_Core_DAO_StateProvince(); - $state_province->id = $params["billing_state_province_id-{$this->_bltID}"]; - $state_province->find(); - $state_province->fetch(); - $country = new CRM_Core_DAO_Country(); - $country->id = $params["billing_country_id-{$this->_bltID}"]; - $country->find(); - $country->fetch(); + private function sendEmailPaymentReceipt($events_in_cart, $params) { foreach ($this->line_items as & $line_item) { $location_params = ['entity_id' => $line_item['event']->id, 'entity_table' => 'civicrm_event']; $line_item['location'] = CRM_Core_BAO_Location::getValues($location_params, TRUE); @@ -314,36 +163,49 @@ public function emailReceipt($events_in_cart, $params) { } $send_template_params = [ 'table' => 'civicrm_msg_template', - 'contactId' => $this->payer_contact_id, + 'contactId' => $params['contact_id'], 'from' => current(CRM_Core_BAO_Domain::getNameAndEmail(TRUE, TRUE)), 'groupName' => 'msg_tpl_workflow_event', 'isTest' => FALSE, - 'toEmail' => $contact_details[1], - 'toName' => $contact_details[0], + 'toEmail' => $params['email'], + 'toName' => $params['first_name'] . ' ' . $params['last_name'], 'tplParams' => [ 'billing_name' => "{$params['billing_first_name']} {$params['billing_last_name']}", - 'billing_city' => $params["billing_city-{$this->_bltID}"], - 'billing_country' => $country->name, - 'billing_postal_code' => $params["billing_postal_code-{$this->_bltID}"], - 'billing_state' => $state_province->abbreviation, - 'billing_street_address' => $params["billing_street_address-{$this->_bltID}"], - 'credit_card_exp_date' => $params['credit_card_exp_date'], - 'credit_card_type' => $params['credit_card_type'], - 'credit_card_number' => "************" . substr($params['credit_card_number'], -4, 4), - // XXX cart->get_discounts - 'discounts' => $this->discounts, - 'email' => $contact_details[1], + 'billing_city' => $params["billing_city-{$this->_bltID}"] ?? '', + 'billing_postal_code' => $params["billing_postal_code-{$this->_bltID}"] ?? '', + 'billing_street_address' => $params["billing_street_address-{$this->_bltID}"] ?? '', + 'credit_card_exp_date' => $params['credit_card_exp_date'] ?? '', + 'credit_card_type' => $params['credit_card_type'] ?? '', + 'credit_card_number' => isset($params['credit_card_number']) ? "************" . substr($params['credit_card_number'], -4, 4) : '', + 'discounts' => $this->discounts ?? [], + 'email' => $params['email'], 'events_in_cart' => $events_in_cart, 'line_items' => $this->line_items, - 'name' => $contact_details[0], - 'transaction_id' => $params['trxn_id'], - 'transaction_date' => $params['trxn_date'], - 'is_pay_later' => $this->is_pay_later, + 'name' => $params['first_name'] . ' ' . $params['last_name'], + 'transaction_id' => $params['trxn_id'] ?? '', + 'transaction_date' => $params['trxn_date'] ?? '', + 'is_pay_later' => ($params['payment_status'] === 'Completed') ? FALSE : TRUE, 'pay_later_receipt' => $this->pay_later_receipt, ], 'valueName' => 'event_registration_receipt', 'PDFFilename' => ts('confirmation') . '.pdf', ]; + + if (isset($params["billing_state_province_id-{$this->_bltID}"])) { + $state_province = new CRM_Core_DAO_StateProvince(); + $state_province->id = $params["billing_state_province_id-{$this->_bltID}"]; + $state_province->find(); + $state_province->fetch(); + $send_template_params['tplParams']['billing_state'] = $state_province->abbreviation; + } + if (isset($params["billing_country_id-{$this->_bltID}"])) { + $country = new CRM_Core_DAO_Country(); + $country->id = $params["billing_country_id-{$this->_bltID}"]; + $country->find(); + $country->fetch(); + $send_template_params['tplParams']['billing_country'] = $country->name; + } + $template_params_to_copy = [ 'billing_name', 'billing_city', @@ -356,12 +218,103 @@ public function emailReceipt($events_in_cart, $params) { 'credit_card_number', ]; foreach ($template_params_to_copy as $template_param_to_copy) { - $this->set($template_param_to_copy, $send_template_params['tplParams'][$template_param_to_copy]); + $this->set($template_param_to_copy, $send_template_params['tplParams'][$template_param_to_copy] ?? ''); } CRM_Core_BAO_MessageTemplate::sendTemplate($send_template_params); } + /** + * Send a confirmation email to the participant that was registered for the event + * + * @param \CRM_Event_BAO_Participant $participant + * + * @throws \API_Exception + * @throws \CRM_Core_Exception + */ + private function sendEmailEventConfirmationReceipt($participant) { + $contactDetails = []; + $contactIds[] = $participant->contact_id; + list($currentContactDetails) = CRM_Utils_Token::getTokenDetails($contactIds, NULL, + FALSE, FALSE, NULL, [], 'CRM_Event_BAO_Participant'); + foreach ($currentContactDetails as $contactId => $contactValues) { + $contactDetails[$contactId] = $contactValues; + } + $participantRoles = CRM_Event_PseudoConstant::participantRole(); + $participantDetails = []; + $query = "SELECT * FROM civicrm_participant WHERE id = " . $participant->id; + $dao = CRM_Core_DAO::executeQuery($query); + while ($dao->fetch()) { + $participantDetails[$dao->id] = [ + 'id' => $dao->id, + 'role' => $participantRoles[$dao->role_id], + 'is_test' => $dao->is_test, + 'event_id' => $dao->event_id, + 'status_id' => $dao->status_id, + 'fee_amount' => $dao->fee_amount, + 'contact_id' => $dao->contact_id, + 'register_date' => $dao->register_date, + 'registered_by_id' => $dao->registered_by_id, + ]; + } + $domainValues = []; + if (empty($domainValues)) { + $domain = CRM_Core_BAO_Domain::getDomain(); + $tokens = [ + 'domain' => + [ + 'name', + 'phone', + 'address', + 'email', + ], + 'contact' => CRM_Core_SelectValues::contactTokens(), + ]; + foreach ($tokens['domain'] as $token) { + $domainValues[$token] = CRM_Utils_Token::getDomainTokenReplacement($token, $domain); + } + } + $eventDetails = []; + $eventParams = ['id' => $participant->event_id]; + CRM_Event_BAO_Event::retrieve($eventParams, $eventDetails); + //get default participant role. + $eventDetails['participant_role'] = $participantRoles[$eventDetails['default_role_id']] ?? NULL; + //get the location info + $locParams = [ + 'entity_id' => $participant->event_id, + 'entity_table' => 'civicrm_event', + ]; + $eventDetails['location'] = CRM_Core_BAO_Location::getValues($locParams, TRUE); + $toEmail = $contactDetails[$participant->contact_id]['email'] ?? NULL; + if ($toEmail) { + //take a receipt from as event else domain. + $receiptFrom = $domainValues['name'] . ' <' . $domainValues['email'] . '>'; + if (!empty($eventDetails['confirm_from_name']) && !empty($eventDetails['confirm_from_email'])) { + $receiptFrom = $eventDetails['confirm_from_name'] . ' <' . $eventDetails['confirm_from_email'] . '>'; + } + $participantName = $contactDetails[$participant->contact_id]['display_name']; + $tplParams = [ + 'event' => $eventDetails, + 'participant' => $participantDetails[$participant->id], + 'participantID' => $participant->id, + 'participant_status' => 'Registered', + ]; + + $sendTemplateParams = [ + 'groupName' => 'msg_tpl_workflow_event', + 'valueName' => 'event_online_receipt', + 'contactId' => $participantDetails[$participant->id]['contact_id'], + 'tplParams' => $tplParams, + 'from' => $receiptFrom, + 'toName' => $participantName, + 'toEmail' => $toEmail, + 'cc' => $eventDetails['cc_confirm'] ?? NULL, + 'bcc' => $eventDetails['bcc_confirm'] ?? NULL, + ]; + CRM_Core_BAO_MessageTemplate::sendTemplate($sendTemplateParams); + } + } + /** * Apply form rules. * @@ -374,7 +327,7 @@ public function emailReceipt($events_in_cart, $params) { public static function formRule($fields, $files, $form) { $errors = []; - if ($form->payment_required && empty($form->_submitValues['is_pay_later'])) { + if ($form->isPaymentRequired() && empty($form->_submitValues['is_pay_later'])) { CRM_Core_Form::validateMandatoryFields($form->_fields, $fields, $errors); // validate payment instrument values (e.g. credit card number) @@ -388,112 +341,149 @@ public static function formRule($fields, $files, $form) { * Pre-process form. */ public function preProcess() { - $params = $this->_submitValues; - $this->is_pay_later = CRM_Utils_Array::value('is_pay_later', $params, FALSE) && !CRM_Utils_Array::value('payment_completed', $params); - + CRM_Core_Session::singleton()->replaceUserContext( + CRM_Utils_System::url('civicrm/event/cart_checkout', [ + 'reset' => 1, + 'qf_Payment_display' => 1, + ]) + ); parent::preProcess(); + + $this->paymentPropertyBag = new \Civi\Payment\PropertyBag(); + $this->setPaymentMode(); + + // Setup payment processors if required + if ($this->isPaymentRequired()) { + $this->_paymentProcessorIDs = \Civi::settings()->get('eventcart_payment_processors'); + $this->setIsPayLater(\Civi::settings()->get('eventcart_paylater')); + if ($this->isPayLater()) { + $this->setPayLaterLabel(\Civi::settings()->get('eventcart_paylater_text')); + // Add the pay later processor to the list of available processor IDs + $this->_paymentProcessorIDs[] = 0; + } + + $this->assignPaymentProcessor($this->isPayLater()); + // This is required to set the "default" payment processor for the form. + // Otherwise the first (default) processor will not load correctly on the page. + if (isset($this->_submitValues['payment_processor_id'])) { + $this->_paymentProcessor = $this->_paymentProcessors[$this->_submitValues['payment_processor_id']]; + } + else { + $this->_paymentProcessor = reset($this->_paymentProcessors); + } + $this->_defaults['payment_processor_id'] = $this->_paymentProcessor['id']; + } } /** * Post process form. */ public function postProcess() { - $trxnDetails = NULL; $params = $this->_submitValues; + $transaction = new CRM_Core_Transaction(); - $main_participants = $this->cart->get_main_event_participants(); + $contactID = $this->getContactID(); + $ctype = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $contactID, 'contact_type'); - $transaction = new CRM_Core_Transaction(); + $fields = []; - foreach ($main_participants as $participant) { - $defaults = []; - $ids = ['contact_id' => $participant->contact_id]; - $contact = CRM_Contact_BAO_Contact::retrieve($ids, $defaults); - $contact->is_deleted = 0; - $contact->save(); + $fields['email-Primary'] = 1; + $this->_params = $params; + // now set the values for the billing location. + foreach (array_keys($this->_fields) as $name) { + $fields[$name] = 1; } - $trxn_prefix = 'VR'; - if (array_key_exists('billing_contact_email', $params)) { - $this->payer_contact_id = self::find_or_create_contact([ - 'email' => $params['billing_contact_email'], - 'first_name' => $params['billing_first_name'], - 'last_name' => $params['billing_last_name'], - 'is_deleted' => FALSE, - ]); - - $ctype = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', - $this->payer_contact_id, - 'contact_type' - ); - $billing_fields = [ - "billing_first_name" => 1, - "billing_middle_name" => 1, - "billing_last_name" => 1, - "billing_street_address-{$this->_bltID}" => 1, - "billing_city-{$this->_bltID}" => 1, - "billing_state_province_id-{$this->_bltID}" => 1, - "billing_postal_code-{$this->_bltID}" => 1, - "billing_country_id-{$this->_bltID}" => 1, - "address_name-{$this->_bltID}" => 1, - "email-{$this->_bltID}" => 1, - ]; + $fields["address_name-{$this->_bltID}"] = 1; - $params["address_name-{$this->_bltID}"] = CRM_Utils_Array::value('billing_first_name', $params) . ' ' . CRM_Utils_Array::value('billing_middle_name', $params) . ' ' . CRM_Utils_Array::value('billing_last_name', $params); + list($hasBillingField, $addressParams) = CRM_Contribute_BAO_Contribution::getPaymentProcessorReadyAddressParams($this->_params, $this->_bltID); + $fields = $this->formatParamsForPaymentProcessor($fields); - $params["email-{$this->_bltID}"] = $params['billing_contact_email']; + if ($hasBillingField) { + $addressParams = array_merge($this->_params, $addressParams); + // CRM-18277 don't let this get passed in because we don't want contribution source to override contact source. + // Ideally we wouldn't just randomly merge everything into addressParams but just pass in a relevant array. + // Note this source field is covered by a unit test. + if (isset($addressParams['source'])) { + unset($addressParams['source']); + } + //here we are setting up the billing contact - if different from the member they are already created + // but they will get billing details assigned CRM_Contact_BAO_Contact::createProfileContact( $params, - $billing_fields, - $this->payer_contact_id, + $fields, + $contactID, NULL, NULL, - $ctype, - TRUE + $ctype ); - - $params['contact_id'] = $this->payer_contact_id; } - $params['now'] = date('YmdHis'); - $params['invoiceID'] = md5(uniqid(rand(), TRUE)); - $params['amount'] = $this->total; - $params['financial_type_id'] = $this->financial_type_id; - if ($this->payment_required && empty($params['is_pay_later'])) { - $trxnDetails = $this->make_payment($params); - $params['trxn_id'] = $trxnDetails['trxn_id']; - $params['trxn_date'] = $trxnDetails['trxn_date']; - $params['currencyID'] = $trxnDetails['currency']; + $params['contact_id'] = $contactID; + $params['currency'] = $this->paymentPropertyBag->getCurrency(); + + if (empty($params['is_pay_later'])) { + if ($this->isPaymentRequired()) { + $contributionID = $this->createOrder($params)['id']; + $this->paymentPropertyBag->setContactID($contactID); + $this->paymentPropertyBag->setContributionID($contributionID); + $this->paymentPropertyBag->mergeLegacyInputParams($params); + + $payment = Civi\Payment\System::singleton()->getById($params['payment_processor_id']); + try { + $result = $payment->doPayment($this->paymentPropertyBag); + $completedStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($result['payment_status_id'] == $completedStatusId) { + civicrm_api3('Payment', 'create', [ + 'contribution_id' => $this->paymentPropertyBag->getContributionID(), + 'total_amount' => $this->paymentPropertyBag->getAmount(), + 'is_send_contribution_notification' => 0, + 'fee_amount' => $result['fee_amount'] ?? 0, + 'payment_processor_id' => $this->_paymentProcessor['id'], + ]); + } + $params['trxn_id'] = $result['trxn_id'] ?? ''; + $params['trxn_date'] = date('YmdHis'); + $params['payment_status'] = $result['payment_status']; + } + catch (\Civi\Payment\Exception\PaymentProcessorException $e) { + Civi::log()->error('Payment processor exception: ' . $e->getMessage()); + CRM_Core_Session::singleton()->setStatus($e->getMessage()); + CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/event/cart_checkout', "_qf_Payment_display=1&qfKey={$this->controller->_key}", TRUE, NULL, FALSE)); + } + } + + // Free events are not included in the "Order" so we need to mark them as "Registered" here. + // For now we just set all participants (paid and free) to "Registered" even though the paid ones will have been + // set by a successful payment above + $participantIDs = []; + foreach ($this->cart->get_main_event_participants() as $participant) { + $participantIDs[] = $participant->id; + } + Participant::update(FALSE) + ->addValue('status_id:name', 'Registered') + ->addWhere('id', 'IN', $participantIDs) + ->execute(); } + $this->cart->completed = TRUE; $this->cart->save(); - $this->set('last_event_cart_id', $this->cart->id); + $transaction->commit(); - $contribution_statuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name'); - $params['payment_instrument_id'] = NULL; - if (!empty($params['is_pay_later'])) { - $params['payment_instrument_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Check'); - $trxn_prefix = 'CK'; - } - else { - $params['payment_instrument_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Credit Card'); - } - if ($this->is_pay_later && empty($params['payment_completed'])) { - $params['contribution_status_id'] = array_search('Pending', $contribution_statuses); - } - else { - $params['contribution_status_id'] = array_search('Completed', $contribution_statuses); - $params['participant_status'] = 'Registered'; - $params['is_pay_later'] = 0; - } - if ($trxnDetails == NULL) { - $params['trxn_id'] = $trxn_prefix . strftime("%Y%m%d%H%M%S"); - $params['trxn_date'] = $params['now']; + // Send the payment receipt + $this->sendEmailPaymentReceipt($this->cart->events_in_carts, $params); + // Send registration confirmation receipts to each participant + foreach ($this->cart->get_main_event_participants() as $participant) { + $this->sendEmailEventConfirmationReceipt($participant); } - if ($this->payment_required) { - $this->emailReceipt($this->cart->events_in_carts, $params); - } + $session = CRM_Core_Session::singleton(); + $session->set('last_event_cart_id', $this->cart->id); + $session->set('contributionID', $contributionID ?? NULL); + } + + private function createOrder($params) { + $params['invoiceID'] = md5(uniqid(rand(), TRUE)); // n.b. we need to process the subparticipants before main event // participants so that session attendance can be included in the email @@ -504,135 +494,45 @@ public function postProcess() { } $this->all_participants = array_merge($this->all_participants, $main_participants); - $this->sub_trxn_index = 0; foreach ($this->all_participants as $mer_participant) { $event_in_cart = $this->cart->get_event_in_cart_by_event_id($mer_participant->event_id); - $this->sub_trxn_index += 1; + $lineItem = []; + $lineItem['params'] = [ + 'event_id' => $event_in_cart->event_id, + 'contact_id' => $mer_participant->contact_id, + 'role_id' => $mer_participant->role_id, + 'participant_id' => $mer_participant->id, + 'participant_status_id' => 'Pending in cart', + ]; - unset($params['contributionID']); - if ($mer_participant->must_wait) { - $this->registerParticipant($params, $mer_participant, $event_in_cart->event); - } - else { - $params['amount'] = $mer_participant->cost - $mer_participant->discount_amount; - - if ($event_in_cart->event->financial_type_id && $mer_participant->cost) { - $params['financial_type_id'] = $event_in_cart->event->financial_type_id; - $params['participant_contact_id'] = $mer_participant->contact_id; - $contribution = $this->record_contribution($mer_participant, $params, $event_in_cart->event); - // Record civicrm_line_item - CRM_Price_BAO_LineItem::processPriceSet($mer_participant->id, $mer_participant->price_details, $contribution, $entity_table = 'civicrm_participant'); + // @todo maybe handle waitList: $mer_participant->must_wait + foreach ($mer_participant->price_details as $priceSetID => $priceSetFieldValues) { + foreach ($priceSetFieldValues as $priceSetFieldValueID => $priceSetFieldValueDetail) { + $priceSetFieldValueDetail['entity_table'] = 'civicrm_participant'; + $lineItem['line_item'][] = $priceSetFieldValueDetail; } - $this->registerParticipant($params, $mer_participant, $event_in_cart->event); } + $lineItems[] = $lineItem; } - $this->trxn_id = $params['trxn_id']; - $this->trxn_date = $params['trxn_date']; - $this->saveDataToSession(); - $transaction->commit(); - } - /** - * Make payment. - * - * @param array $params - * - * @return array - * @throws Exception - */ - public function make_payment(&$params) { - $params = $this->prepareParamsForPaymentProcessor($params); - $params['currencyID'] = CRM_Core_Config::singleton()->defaultCurrency; - - $payment = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor); - CRM_Core_Payment_Form::mapParams($this->_bltID, $params, $params, TRUE); - - try { - $result = $payment->doPayment($params); - } - catch (\Civi\Payment\Exception\PaymentProcessorException $e) { - CRM_Core_Error::displaySessionError($result); - CRM_Utils_System::redirect(CRM_Utils_System::url('civicrm/event/cart_checkout', "_qf_Payment_display=1&qfKey={$this->controller->_key}", TRUE, NULL, FALSE)); - } - - $trxnDetails = [ - 'trxn_id' => $result['trxn_id'], - 'trxn_date' => $result['now'], - 'currency' => $result['currencyID'] ?? NULL, + $orderParams = [ + 'contact_id' => $this->getContactID(), + 'total_amount' => $this->total, + 'financial_type_id' => 'Event fee', + 'contribution_status_id' => 'Pending', + 'currency' => $params['currency'], + 'line_items' => $lineItems, ]; - return $trxnDetails; - } - - /** - * Record contribution. - * - * @param CRM_Event_BAO_Participant $mer_participant - * @param array $params - * @param CRM_Event_BAO_Event $event - * - * @return object - * @throws Exception - */ - public function record_contribution(&$mer_participant, &$params, $event) { - if ($this->payer_contact_id) { - $payer = $this->payer_contact_id; - } - elseif (self::getContactID()) { - $payer = self::getContactID(); - } - else { - $payer = $params['participant_contact_id']; - } - - $contribParams = [ - 'contact_id' => $payer, - 'financial_type_id' => $params['financial_type_id'], - 'receive_date' => $params['now'], - 'total_amount' => $params['amount'], - 'amount_level' => $mer_participant->fee_level, - 'net_amount' => $params['amount'], - 'invoice_id' => "{$params['invoiceID']}-{$this->sub_trxn_index}", - 'trxn_id' => "{$params['trxn_id']}-{$this->sub_trxn_index}", - 'currency' => $params['currencyID'] ?? NULL, - 'source' => $event->title, - 'is_pay_later' => CRM_Utils_Array::value('is_pay_later', $params, 0), - 'contribution_status_id' => $params['contribution_status_id'], - 'payment_instrument_id' => $params['payment_instrument_id'], - 'check_number' => $params['check_number'] ?? NULL, - 'skipLineItem' => 1, - ]; - - if (is_array($this->_paymentProcessor)) { - $contribParams['payment_processor'] = $this->_paymentProcessor['id']; - } - - $contribution = CRM_Contribute_BAO_Contribution::add($contribParams); - $mer_participant->contribution_id = $contribution->id; - $params['contributionID'] = $contribution->id; - - return $contribution; + $result = civicrm_api3('Order', 'create', $orderParams); + return reset($result['values']); } /** * Save data to session. */ public function saveDataToSession() { - $session_line_items = []; - foreach ($this->line_items as $line_item) { - $session_line_item = []; - $session_line_item['amount'] = $line_item['amount']; - $session_line_item['cost'] = $line_item['cost']; - $session_line_item['event_id'] = $line_item['event']->id; - $session_line_items[] = $session_line_item; - } - $this->set('line_items', $session_line_items); - $this->set('payment_required', $this->payment_required); - $this->set('is_pay_later', $this->is_pay_later); - $this->set('pay_later_receipt', $this->pay_later_receipt); - $this->set('trxn_id', $this->trxn_id); - $this->set('trxn_date', $this->trxn_date); - $this->set('total', $this->total); + } /** @@ -641,52 +541,32 @@ public function saveDataToSession() { * @return array */ public function setDefaultValues() { - $defaults = parent::setDefaultValues(); - - $default_country = new CRM_Core_DAO_Country(); - $default_country->iso_code = CRM_Core_BAO_Country::defaultContactCountry(); - $default_country->find(TRUE); - $defaults["billing_country_id-{$this->_bltID}"] = $default_country->id; - - if (self::getContactID()) { - $params = ['id' => self::getContactID()]; - $contact = CRM_Contact_BAO_Contact::retrieve($params, $defaults); - - foreach ($contact->email as $email) { - if ($email['is_billing']) { - $defaults["billing_contact_email"] = $email['email']; - } - } - if (empty($defaults['billing_contact_email'])) { - foreach ($contact->email as $email) { - if ($email['is_primary']) { - $defaults["billing_contact_email"] = $email['email']; - } - } - } - - $defaults["billing_first_name"] = $contact->first_name; - $defaults["billing_middle_name"] = $contact->middle_name; - $defaults["billing_last_name"] = $contact->last_name; - - $billing_address = CRM_Event_Cart_BAO_MerParticipant::billing_address_from_contact($contact); - - if ($billing_address != NULL) { - $defaults["billing_street_address-{$this->_bltID}"] = $billing_address['street_address']; - $defaults["billing_city-{$this->_bltID}"] = $billing_address['city']; - $defaults["billing_postal_code-{$this->_bltID}"] = $billing_address['postal_code']; - $defaults["billing_state_province_id-{$this->_bltID}"] = $billing_address['state_province_id']; - $defaults["billing_country_id-{$this->_bltID}"] = $billing_address['country_id']; - } - } - - $defaults["source"] = $this->description; - - return $defaults; + $contactID = $this->getContactID(); + CRM_Core_Payment_Form::setDefaultValues($this, $contactID); + + // @todo replace this with a profile + $contact = Contact::get(FALSE) + ->addSelect('first_name', 'last_name') + ->addWhere('id', '=', $contactID) + ->execute() + ->first(); + $this->_defaults['first_name'] = $contact['first_name'] ?? ''; + $this->_defaults['last_name'] = $contact['last_name'] ?? ''; + $email = \Civi\Api4\Email::get(FALSE) + ->addSelect('email') + ->addWhere('contact_id', '=', $contactID) + ->addOrderBy('is_billing', 'DESC') + ->addOrderBy('is_primary', 'DESC') + ->execute() + ->first(); + $this->_defaults['email'] = $email['email'] ?? ''; + + return $this->_defaults; } /** * Apply discount. + * @fixme Check this still works! * * @param string $discountCode * @param array $price_set_amount @@ -700,7 +580,7 @@ protected function apply_discount($discountCode, &$price_set_amount, &$cost, $ev $extensions = civicrm_api3('Extension', 'get', [ 'full_name' => 'org.civicrm.module.cividiscount', ]); - if (empty($extensions['id']) || ($extensions['values'][$extensions['id']]['status'] !== 'installed')) { + if (empty($extensions['count']) || ($extensions['values'][$extensions['id']]['status'] !== 'installed')) { return FALSE; } @@ -756,4 +636,39 @@ protected function apply_discount($discountCode, &$price_set_amount, &$cost, $ev return $stat; } + public function isPaymentRequired() { + if (!isset(\Civi::$statics[__CLASS__]['is_payment_required'])) { + $this->setPaymentParameters(); + \Civi::$statics[__CLASS__]['is_payment_required'] = ($this->total > 0 ? TRUE : FALSE); + $this->assign('payment_required', \Civi::$statics[__CLASS__]['is_payment_required']); + } + return \Civi::$statics[__CLASS__]['is_payment_required']; + } + + private function setPaymentParameters() { + $this->line_items = []; + $this->sub_total = 0; + $this->_price_values = $this->getValuesForPage('ParticipantsAndPrices'); + + // iterate over each event in cart + foreach ($this->cart->get_main_events_in_carts() as $event_in_cart) { + $this->process_event_line_item($event_in_cart); + foreach ($this->cart->get_events_in_carts_by_main_event_id($event_in_cart->event_id) as $subevent) { + $this->process_event_line_item($subevent, 'subevent'); + } + } + + $this->total = $this->sub_total; + $this->assign('line_items', $this->line_items); + $this->assign('sub_total', $this->sub_total); + $this->assign('total', $this->total); + + // @fixme: This will always use the currency of the "last" event. + // That's probably ok because they should all use the same currency. + $currency = $event_in_cart->event->currency ?? CRM_Core_Config::singleton()->defaultCurrency; + $this->assign('currency', $currency); + $this->paymentPropertyBag->setCurrency($currency); + $this->paymentPropertyBag->setAmount($this->total); + } + } diff --git a/ext/eventcart/CRM/Event/Cart/Form/Checkout/ThankYou.php b/ext/eventcart/CRM/Event/Cart/Form/Checkout/ThankYou.php index 991e3724e99..e7cf91afdfd 100644 --- a/ext/eventcart/CRM/Event/Cart/Form/Checkout/ThankYou.php +++ b/ext/eventcart/CRM/Event/Cart/Form/Checkout/ThankYou.php @@ -1,5 +1,7 @@ contributionID) { + $lineItems = \Civi\Api4\LineItem::get() + ->addWhere('contribution_id', '=', $this->contributionID) + ->execute() + ->indexBy('entity_id'); + } foreach ($this->cart->events_in_carts as $event_in_cart) { $event_in_cart->load_location(); - } - $line_items = $this->get('line_items'); - foreach ($line_items as $line_item) { - $event_in_cart = $this->cart->get_event_in_cart_by_event_id($line_item['event_id']); $not_waiting_participants = []; foreach ($event_in_cart->not_waiting_participants() as $participant) { - $not_waiting_participants[] = [ + $not_waiting_participants[$participant->id] = [ 'display_name' => CRM_Contact_BAO_Contact::displayName($participant->contact_id), ]; } $waiting_participants = []; foreach ($event_in_cart->waiting_participants() as $participant) { - $waiting_participants[] = [ + $waiting_participants[$participant->id] = [ 'display_name' => CRM_Contact_BAO_Contact::displayName($participant->contact_id), ]; } - $line_item['event'] = $event_in_cart->event; - $line_item['num_participants'] = count($not_waiting_participants); - $line_item['participants'] = $not_waiting_participants; - $line_item['num_waiting_participants'] = count($waiting_participants); - $line_item['waiting_participants'] = $waiting_participants; - $line_item['location'] = $event_in_cart->location; - $line_item['class'] = $event_in_cart->event->parent_event_id ? 'subevent' : NULL; - - $this->sub_total += $line_item['amount']; - $this->line_items[] = $line_item; + $lineItemForDisplay['event'] = $event_in_cart->event; + $lineItemForDisplay['num_participants'] = count($not_waiting_participants); + $lineItemForDisplay['participants'] = $not_waiting_participants; + $lineItemForDisplay['num_waiting_participants'] = count($waiting_participants); + $lineItemForDisplay['waiting_participants'] = $waiting_participants; + $lineItemForDisplay['location'] = $event_in_cart->location; + $lineItemForDisplay['class'] = $event_in_cart->event->parent_event_id ? 'subevent' : NULL; + $lineItemForDisplay['line_total'] = 0; + foreach ($not_waiting_participants as $participantID => $_) { + $lineItemForDisplay['line_total'] += $lineItems[$participantID]['line_total'] ?? 0; + $lineItemForDisplay['unit_price'] = $lineItems[$participantID]['unit_price'] ?? 0; + } + $this->sub_total += $lineItemForDisplay['line_total']; + $this->line_items[] = $lineItemForDisplay; } $this->assign('line_items', $this->line_items); } public function buildQuickForm() { - $defaults = []; - $ids = []; $template_params_to_copy = [ 'billing_name', 'billing_city', @@ -67,17 +79,22 @@ public function buildQuickForm() { $this->assign('payment_required', $this->get('payment_required')); $this->assign('is_pay_later', $this->get('is_pay_later')); $this->assign('pay_later_receipt', $this->get('pay_later_receipt')); + if (!empty($this->contributionID)) { + $currency = Contribution::get(FALSE) + ->addWhere('id', '=', $this->contributionID) + ->execute() + ->first()['currency']; + } + $this->assign('currency', $currency ?? CRM_Core_Config::singleton()->defaultCurrency); $this->assign('sub_total', $this->sub_total); - $this->assign('total', $this->get('total')); - // XXX Configure yourself - //$this->assign( 'site_name', "" ); - //$this->assign( 'site_contact', "" ); + $this->assign('total', $this->get('total') ?? $this->sub_total); } public function preProcess() { - $this->event_cart_id = $this->get('last_event_cart_id'); + $session = CRM_Core_Session::singleton(); + $this->event_cart_id = $session->get('last_event_cart_id'); + $this->contributionID = $session->get('contributionID'); $this->loadCart(); - //$this->loadParticipants( ); } } diff --git a/ext/eventcart/CRM/Event/Cart/Form/MerParticipant.php b/ext/eventcart/CRM/Event/Cart/Form/MerParticipant.php index 6a4d5c07d8c..5578eec4dbc 100644 --- a/ext/eventcart/CRM/Event/Cart/Form/MerParticipant.php +++ b/ext/eventcart/CRM/Event/Cart/Form/MerParticipant.php @@ -2,7 +2,6 @@ /** * Class CRM_Event_Cart_Form_MerParticipant - * @fixme What is a MerParticipant! */ class CRM_Event_Cart_Form_MerParticipant extends CRM_Core_Form { @@ -12,10 +11,16 @@ class CRM_Event_Cart_Form_MerParticipant extends CRM_Core_Form { public $participant = NULL; /** - * @param null|object $participant + * @param array|\CRM_Event_BAO_Participant $participant */ public function __construct($participant) { parent::__construct(); + if (is_array($participant)) { + $participantObject = new CRM_Event_BAO_Participant(); + $participantObject->id = $participant['id']; + $participantObject->find(); + $participant = $participantObject; + } $this->participant = $participant; } @@ -23,9 +28,6 @@ public function __construct($participant) { * @param \CRM_Core_Form $form */ public function appendQuickForm(&$form) { - $textarea_size = ['size' => 30, 'maxlength' => 60]; - $form->add('text', $this->email_field_name(), ts('Email Address'), $textarea_size, TRUE); - list($custom_fields_pre, $custom_fields_post) = $this->get_participant_custom_data_fields(); foreach ($custom_fields_pre as $key => $field) { diff --git a/ext/eventcart/CRM/Event/Cart/Page/CheckoutAJAX.php b/ext/eventcart/CRM/Event/Cart/Page/CheckoutAJAX.php index 19c7b8559b3..9357a0ec9f5 100644 --- a/ext/eventcart/CRM/Event/Cart/Page/CheckoutAJAX.php +++ b/ext/eventcart/CRM/Event/Cart/Page/CheckoutAJAX.php @@ -6,21 +6,17 @@ class CRM_Event_Cart_Page_CheckoutAJAX { public function add_participant_to_cart() { - $transaction = new CRM_Core_Transaction(); - $cart_id = CRM_Utils_Request::retrieve('cart_id', 'Integer'); - $event_id = CRM_Utils_Request::retrieve('event_id', 'Integer'); - + $cart_id = CRM_Utils_Request::retrieveValue('cart_id', 'Integer', NULL, TRUE); $cart = CRM_Event_Cart_BAO_Cart::find_by_id($cart_id); - $params_array = [ + $transaction = new CRM_Core_Transaction(); + $participantParams = [ 'cart_id' => $cart->id, 'contact_id' => CRM_Event_Cart_Form_Cart::find_or_create_contact(), - 'event_id' => $event_id, + 'event_id' => CRM_Utils_Request::retrieveValue('event_id', 'Integer', NULL, TRUE), ]; - //XXX security? - $participant = CRM_Event_Cart_BAO_MerParticipant::create($params_array); - $participant->save(); + $participant = CRM_Event_Cart_BAO_MerParticipant::create($participantParams); $form = new CRM_Core_Form(); $pform = new CRM_Event_Cart_Form_MerParticipant($participant); diff --git a/ext/eventcart/CRM/Event/Cart/StateMachine/Checkout.php b/ext/eventcart/CRM/Event/Cart/StateMachine/Checkout.php index bff2b8adb04..282c5da880c 100644 --- a/ext/eventcart/CRM/Event/Cart/StateMachine/Checkout.php +++ b/ext/eventcart/CRM/Event/Cart/StateMachine/Checkout.php @@ -7,18 +7,17 @@ class CRM_Event_Cart_StateMachine_Checkout extends CRM_Core_StateMachine { /** * @param object $controller - * @param const|int $action + * @param int $action */ public function __construct($controller, $action = CRM_Core_Action::NONE) { parent::__construct($controller, $action); $cart = CRM_Event_Cart_BAO_Cart::find_or_create_for_current_session(); $cart->load_associations(); - if ($cart->is_empty()) { + if (empty($cart->events_in_carts) && empty(CRM_Core_Session::singleton()->get('last_event_cart_id'))) { CRM_Core_Error::statusBounce(ts("You don't have any events in you cart. Please add some events."), CRM_Utils_System::url('civicrm/event')); } - $pages = []; $pages['CRM_Event_Cart_Form_Checkout_ParticipantsAndPrices'] = NULL; foreach ($cart->events_in_carts as $event_in_cart) { if ($event_in_cart->is_parent_event()) { @@ -30,9 +29,9 @@ public function __construct($controller, $action = CRM_Core_Action::NONE) { } } } - $pages["CRM_Event_Cart_Form_Checkout_Payment"] = NULL; - $pages["CRM_Event_Cart_Form_Checkout_ThankYou"] = NULL; - $this->addSequentialPages($pages, $action); + $pages['CRM_Event_Cart_Form_Checkout_Payment'] = NULL; + $pages['CRM_Event_Cart_Form_Checkout_ThankYou'] = NULL; + $this->addSequentialPages($pages); } } diff --git a/ext/eventcart/eventcart.php b/ext/eventcart/eventcart.php index 9fac71ca984..40daa7f1536 100644 --- a/ext/eventcart/eventcart.php +++ b/ext/eventcart/eventcart.php @@ -2,7 +2,7 @@ require_once 'eventcart.civix.php'; // phpcs:disable -use CRM_Eventcart_ExtensionUtil as E; +use CRM_Event_Cart_ExtensionUtil as E; // phpcs:enable /** @@ -131,3 +131,18 @@ function eventcart_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { function eventcart_civicrm_entityTypes(&$entityTypes) { _eventcart_civix_civicrm_entityTypes($entityTypes); } + +/** + * Implements hook_civicrm_navigationMenu(). + */ +function eventcart_civicrm_navigationMenu(&$menu) { + _eventcart_civix_insert_navigation_menu($menu, 'Administer/CiviEvent', array( + 'label' => E::ts('Event Cart Settings'), + 'name' => 'eventcart_settings', + 'url' => 'civicrm/admin/setting/eventcart', + 'permission' => 'administer CiviCRM', + 'operator' => 'OR', + 'separator' => 0, + )); + _eventcart_civix_navigationMenu($menu); +} diff --git a/ext/eventcart/settings/Eventcart.setting.php b/ext/eventcart/settings/Eventcart.setting.php index d52892d25cf..8f7768e70b2 100644 --- a/ext/eventcart/settings/Eventcart.setting.php +++ b/ext/eventcart/settings/Eventcart.setting.php @@ -21,11 +21,11 @@ return [ 'enable_cart' => [ 'name' => 'enable_cart', - 'group_name' => 'Event Preferences', - 'settings_pages' => ['event' => ['weight' => 10]], - 'group' => 'event', + 'group_name' => 'Event Cart Preferences', + 'settings_pages' => ['eventcart' => ['weight' => 10]], + 'group' => 'eventcart', 'type' => 'Boolean', - 'quick_form_type' => 'CheckBox', + 'html_type' => 'checkbox', 'default' => '0', 'add' => '4.1', 'title' => ts('Use Shopping Cart Style Event Registration'), @@ -35,4 +35,56 @@ 'help_text' => '', 'documentation_link' => ['page' => 'CiviEvent Cart Checkout', 'resource' => 'wiki'], ], + 'eventcart_payment_processors' => [ + 'name' => 'eventcart_payment_processors', + 'group_name' => 'Event Cart Preferences', + 'group' => 'eventcart', + 'type' => 'Array', + 'add' => '5.28', + 'is_domain' => 1, + 'is_contact' => 0, + 'description' => ts('Payment processors that will be available for the checkout'), + 'help_text' => '', + 'title' => ts('Payment processors for event cart checkout'), + 'html_type' => 'select', + 'default' => [], + 'html_attributes' => [ + 'class' => 'crm-select2', + 'multiple' => TRUE, + ], + 'pseudoconstant' => [ + 'callback' => 'CRM_Event_Cart_BAO_Cart::getPaymentProcessors', + ], + 'settings_pages' => ['eventcart' => ['weight' => 15]], + ], + 'eventcart_paylater' => [ + 'name' => 'eventcart_paylater', + 'group_name' => 'Event Cart Preferences', + 'settings_pages' => ['eventcart' => ['weight' => 20]], + 'group' => 'eventcart', + 'type' => 'Boolean', + 'html_type' => 'checkbox', + 'default' => '0', + 'add' => '5.28', + 'title' => ts('Allow pay later for event cart'), + 'is_domain' => 1, + 'is_contact' => 0, + 'description' => ts('Enable the pay later option for the event cart checkout'), + 'help_text' => '', + ], + 'eventcart_paylater_text' => [ + 'name' => 'eventcart_paylater_text', + 'group_name' => 'Event Cart Preferences', + 'settings_pages' => ['eventcart' => ['weight' => 25]], + 'group' => 'eventcart', + 'type' => 'Text', + 'html_type' => 'text', + 'default' => 'Pay later', + 'add' => '5.28', + 'title' => ts('The text to display for pay later option'), + 'is_domain' => 1, + 'is_contact' => 0, + 'description' => ts('This is the text that will be displayed on the payment processor selector'), + 'help_text' => '', + ], ]; diff --git a/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/Participant.tpl b/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/Participant.tpl index 7f6ef84078f..ea5011620a1 100644 --- a/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/Participant.tpl +++ b/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/Participant.tpl @@ -1,31 +1,30 @@ - {assign var=event_id value=$participant->event_id} - {assign var=participant_id value=$participant->id} -
- - {assign var=name value="event[`$event_id`][participant][`$participant_id`][number]"} - {$custom.$name} - +{assign var=event_id value=$participant->event_id} +{assign var=participant_id value=$participant->id} +
+ + {assign var=name value="event[`$event_id`][participant][`$participant_id`][number]"} + {$custom.$name} +
- {assign var=pre value="event[`$event_id`][participant][`$participant_id`][customPre]"} -
- {include file="CRM/UF/Form/Block.tpl" fields=$custom.$pre form=$form.field.$participant_id} -
+ {assign var=pre value="event[`$event_id`][participant][`$participant_id`][customPre]"} +
+ {include file="CRM/UF/Form/Block.tpl" fields=$custom.$pre form=$form.field.$participant_id} +
- {$form.event.$event_id.participant.$participant_id.email.label} + {$form.event.$event_id.participant.$participant_id.email.label}
- {$form.event.$event_id.participant.$participant_id.email.html} + {$form.event.$event_id.participant.$participant_id.email.html}
- {assign var=post value="event[`$event_id`][participant][`$participant_id`][customPost]"} -
-
- {include file="CRM/UF/Form/Block.tpl" fields=$custom.$post form=$form.field.$participant_id} -
+ {assign var=post value="event[`$event_id`][participant][`$participant_id`][customPost]"} +
+
+ {include file="CRM/UF/Form/Block.tpl" fields=$custom.$post form=$form.field.$participant_id} +
- - {ts}Delete{/ts} {$form->name} -
+ {ts}Delete{/ts} {$form->name} +
diff --git a/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/ParticipantsAndPrices.tpl b/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/ParticipantsAndPrices.tpl index 5b465d625a8..db8e9dc951f 100644 --- a/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/ParticipantsAndPrices.tpl +++ b/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/ParticipantsAndPrices.tpl @@ -88,3 +88,5 @@ //]]> {/literal} + +{include file="CRM/Form/validate.tpl"} diff --git a/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/Payment.tpl b/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/Payment.tpl index fba3a4770c4..c88e9fe1633 100644 --- a/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/Payment.tpl +++ b/ext/eventcart/templates/CRM/Event/Cart/Form/Checkout/Payment.tpl @@ -88,71 +88,59 @@  {$total|crmMoney:$currency|string_format:"%10s"} - + -{if $payment_required == true} - {if $form.is_pay_later.label} -
-
{$form.is_pay_later.label}
-
{$form.is_pay_later.html} -
-
-
-