2 namespace Civi\Payment
;
4 use InvalidArgumentException
;
6 use CRM_Core_PseudoConstant
;
11 * This class provides getters and setters for arguments needed by CRM_Core_Payment methods.
13 * The setters know how to validate each setting that they are responsible for.
15 * Different methods need different settings and the concept is that by passing
16 * in a property bag we can encapsulate the params needed for a particular
17 * method call, rather than setting arguments for different methods on the main
18 * CRM_Core_Payment object.
20 * This class is also supposed to help with transition away from array key naming nightmares.
23 class PropertyBag
implements \ArrayAccess
{
24 protected $props = ['default' => []];
26 protected static $propMap = [
27 'billingStreetAddress' => TRUE,
28 'billingSupplementalAddress1' => TRUE,
29 'billingSupplementalAddress2' => TRUE,
30 'billingSupplementalAddress3' => TRUE,
31 'billingCity' => TRUE,
32 'billingPostalCode' => TRUE,
33 'billingCounty' => TRUE,
34 'billingCountry' => TRUE,
36 'contact_id' => 'contactID',
37 'contributionID' => TRUE,
38 'contribution_id' => 'contributionID',
39 'contributionRecurID' => TRUE,
40 'contribution_recur_id' => 'contributionRecurID',
42 'currencyID' => 'currency',
43 'description' => TRUE,
46 'fee_amount' => 'feeAmount',
47 'first_name' => 'firstName',
50 'invoice_id' => 'invoiceID',
51 'isBackOffice' => TRUE,
52 'is_back_office' => 'isBackOffice',
54 'is_recur' => 'isRecur',
55 'last_name' => 'lastName',
57 'paymentToken' => TRUE,
58 'payment_token' => 'paymentToken',
60 'recurFrequencyInterval' => TRUE,
61 'frequency_interval' => 'recurFrequencyInterval',
62 'recurFrequencyUnit' => TRUE,
63 'frequency_unit' => 'recurFrequencyUnit',
64 'recurProcessorID' => TRUE,
65 'transactionID' => TRUE,
66 'transaction_id' => 'transactionID',
67 'trxnResultCode' => TRUE,
71 * @var string Just for unit testing.
76 * Implements ArrayAccess::offsetExists
78 * @param mixed $offset
79 * @return bool TRUE if we have that value (on our default store)
81 public function offsetExists ($offset): bool {
82 $prop = $this->handleLegacyPropNames($offset);
83 return isset($this->props
['default'][$prop]);
87 * Implements ArrayAccess::offsetGet
89 * @param mixed $offset
92 public function offsetGet ($offset) {
93 $prop = $this->handleLegacyPropNames($offset);
94 return $this->get($prop, 'default');
98 * Implements ArrayAccess::offsetSet
100 * @param mixed $offset
101 * @param mixed $value
103 public function offsetSet($offset, $value) {
105 $prop = $this->handleLegacyPropNames($offset);
107 catch (InvalidArgumentException
$e) {
108 // We need to make a lot of noise here, we're being asked to merge in
109 // something we can't validate because we don't know what this property is.
110 // This is fine if it's something particular to a payment processor
111 // (which should be using setCustomProperty) however it could also lead to
112 // things like 'my_weirly_named_contact_id'.
113 $this->legacyWarning($e->getMessage() . " We have merged this in for now as a custom property. Please rewrite your code to use PropertyBag->setCustomProperty if it is a genuinely custom property, or a standardised setter like PropertyBag->setContactID for standard properties");
114 $this->setCustomProperty($offset, $value, 'default');
118 // Coerce legacy values that were in use but shouldn't be in our new way of doing things.
119 if ($prop === 'feeAmount' && $value === '') {
120 // At least the following classes pass in ZLS for feeAmount.
121 // CRM_Core_Payment_AuthorizeNetTest::testCreateSingleNowDated
122 // CRM_Core_Payment_AuthorizeNetTest::testCreateSinglePostDated
126 // These lines are here (and not in try block) because the catch must only
127 // catch the case when the prop is custom.
128 $setter = 'set' . ucfirst($prop);
129 $this->$setter($value, 'default');
133 * Implements ArrayAccess::offsetUnset
135 * @param mixed $offset
137 public function offsetUnset ($offset) {
138 $prop = $this->handleLegacyPropNames($offset);
139 unset($this->props
['default'][$prop]);
143 * @param string $message
145 protected function legacyWarning($message) {
146 $message = "Deprecated code: $message";
147 $this->lastWarning
= $message;
148 Civi
::log()->warning($message);
152 * @param string $prop
153 * @return string canonical name.
154 * @throws \InvalidArgumentException if prop name not known.
156 protected function handleLegacyPropNames($prop) {
157 $newName = static::$propMap[$prop] ??
NULL;
158 if ($newName === TRUE) {
159 // Good, modern name.
162 if ($newName === NULL) {
163 throw new \
InvalidArgumentException("Unknown property '$prop'.");
165 // Remaining case is legacy name that's been translated.
166 $this->legacyWarning("We have translated '$prop' to '$newName' for you, but please update your code to use the propper setters and getters.");
173 * @param mixed $prop Valid property name
174 * @param string $label e.g. 'default'
176 protected function get($prop, $label) {
177 if (isset($this->props
['default'][$prop])) {
178 return $this->props
[$label][$prop];
180 throw new \
BadMethodCallException("Property '$prop' has not been set.");
186 * @param mixed $prop Valid property name
187 * @param string $label e.g. 'default'
188 * @param mixed $value
190 * @return PropertyBag $this object so you can chain set setters.
192 protected function set($prop, $label = 'default', $value) {
193 $this->props
[$label][$prop] = $value;
200 protected function coercePseudoConstantStringToInt(string $baoName, string $field, $input) {
201 if (is_numeric($input)) {
202 // We've been given a numeric ID.
205 elseif (is_string($input)) {
206 // We've been given a named instrument.
207 $_ = (int) CRM_Core_PseudoConstant
::getKey($baoName, $field, $input);
210 throw new InvalidArgumentException("Expected an integer ID or a String name for $field.");
213 throw new InvalidArgumentException("Expected an integer greater than 0 for $field.");
220 public function has($prop, $label = 'default') {
221 // We do NOT translate legacy prop names since only new code should be
222 // using this method, and new code should be using canonical names.
223 // $prop = $this->handleLegacyPropNames($prop);
224 return isset($this->props
[$label][$prop]);
228 * This is used to merge values from an array.
229 * It's a transitional function and should not be used!
233 public function mergeLegacyInputParams($data) {
234 $this->legacyWarning("We have merged input params into the property bag for now but please rewrite code to not use this.");
235 foreach ($data as $key => $value) {
236 if ($value !== NULL) {
237 $this->offsetSet($key, $value);
243 * Throw an exception if any of the props is unset.
245 * @param array $props Array of proper property names (no legacy aliases allowed).
247 * @return PropertyBag
249 public function require(array $props) {
251 foreach ($props as $prop) {
252 if (!isset($this->props
['default'][$prop])) {
257 throw new \
InvalidArgumentException("Required properties missing: " . implode(', ', $missing));
262 // Public getters, setters.
265 * Get the monetary amount.
267 public function getAmount($label = 'default') {
268 return $this->get('amount', $label);
272 * Get the monetary amount.
274 public function setAmount($value, $label = 'default') {
275 if (!is_numeric($value)) {
276 throw new \
InvalidArgumentException("setAmount requires a numeric amount value");
279 return $this->set('amount', CRM_Utils_Money
::format($value, NULL, NULL, TRUE), $label);
283 * BillingStreetAddress getter.
287 public function getBillingStreetAddress($label = 'default') {
288 return $this->get('billingStreetAddress', $label);
292 * BillingStreetAddress setter.
294 * @param string $input
295 * @param string $label e.g. 'default'
297 public function setBillingStreetAddress($input, $label = 'default') {
298 return $this->set('billingStreetAddress', $label, (string) $input);
302 * BillingSupplementalAddress1 getter.
306 public function getBillingSupplementalAddress1($label = 'default') {
307 return $this->get('billingSupplementalAddress1', $label);
311 * BillingSupplementalAddress1 setter.
313 * @param string $input
314 * @param string $label e.g. 'default'
316 public function setBillingSupplementalAddress1($input, $label = 'default') {
317 return $this->set('billingSupplementalAddress1', $label, (string) $input);
321 * BillingSupplementalAddress2 getter.
325 public function getBillingSupplementalAddress2($label = 'default') {
326 return $this->get('billingSupplementalAddress2', $label);
330 * BillingSupplementalAddress2 setter.
332 * @param string $input
333 * @param string $label e.g. 'default'
335 public function setBillingSupplementalAddress2($input, $label = 'default') {
336 return $this->set('billingSupplementalAddress2', $label, (string) $input);
340 * BillingSupplementalAddress3 getter.
344 public function getBillingSupplementalAddress3($label = 'default') {
345 return $this->get('billingSupplementalAddress3', $label);
349 * BillingSupplementalAddress3 setter.
351 * @param string $input
352 * @param string $label e.g. 'default'
354 public function setBillingSupplementalAddress3($input, $label = 'default') {
355 return $this->set('billingSupplementalAddress3', $label, (string) $input);
359 * BillingCity getter.
363 public function getBillingCity($label = 'default') {
364 return $this->get('billingCity', $label);
368 * BillingCity setter.
370 * @param string $input
371 * @param string $label e.g. 'default'
373 public function setBillingCity($input, $label = 'default') {
374 return $this->set('billingCity', $label, (string) $input);
378 * BillingPostalCode getter.
382 public function getBillingPostalCode($label = 'default') {
383 return $this->get('billingPostalCode', $label);
387 * BillingPostalCode setter.
389 * @param string $input
390 * @param string $label e.g. 'default'
392 public function setBillingPostalCode($input, $label = 'default') {
393 return $this->set('billingPostalCode', $label, (string) $input);
397 * BillingCounty getter.
401 public function getBillingCounty($label = 'default') {
402 return $this->get('billingCounty', $label);
406 * BillingCounty setter.
408 * Nb. we can't validate this unless we have the country ID too, so we don't.
410 * @param string $input
411 * @param string $label e.g. 'default'
413 public function setBillingCounty($input, $label = 'default') {
414 return $this->set('billingCounty', $label, (string) $input);
418 * BillingCountry getter.
422 public function getBillingCountry($label = 'default') {
423 return $this->get('billingCountry', $label);
427 * BillingCountry setter.
429 * Nb. We require and we store a 2 character country code.
431 * @param string $input
432 * @param string $label e.g. 'default'
434 public function setBillingCountry($input, $label = 'default') {
435 if (!is_string($input) ||
strlen($input) !== 2) {
436 throw new \
InvalidArgumentException("setBillingCountry expects ISO 3166-1 alpha-2 country code.");
438 if (!CRM_Core_PseudoConstant
::getKey('CRM_Core_BAO_Address', 'country_id', $input)) {
439 throw new \
InvalidArgumentException("setBillingCountry expects ISO 3166-1 alpha-2 country code.");
441 return $this->set('billingCountry', $label, (string) $input);
447 public function getContactID($label = 'default'): int {
448 return $this->get('contactID', $label);
452 * @param int $contactID
453 * @param string $label
455 public function setContactID($contactID, $label = 'default') {
456 // We don't use this because it counts zero as positive: CRM_Utils_Type::validate($contactID, 'Positive');
457 if (!($contactID > 0)) {
458 throw new InvalidArgumentException("ContactID must be a positive integer");
461 return $this->set('contactID', $label, (int) $contactID);
465 * Getter for contributionID.
468 * @param string $label
470 public function getContributionID($label = 'default') {
471 return $this->get('contributionID', $label);
475 * @param int $contributionID
476 * @param string $label e.g. 'default'
478 public function setContributionID($contributionID, $label = 'default') {
479 // We don't use this because it counts zero as positive: CRM_Utils_Type::validate($contactID, 'Positive');
480 if (!($contributionID > 0)) {
481 throw new InvalidArgumentException("ContributionID must be a positive integer");
484 return $this->set('contributionID', $label, (int) $contributionID);
488 * Getter for contributionRecurID.
491 * @param string $label
493 public function getContributionRecurID($label = 'default') {
494 return $this->get('contributionRecurID', $label);
498 * @param int $contributionRecurID
499 * @param string $label e.g. 'default'
501 public function setContributionRecurID($contributionRecurID, $label = 'default') {
502 // We don't use this because it counts zero as positive: CRM_Utils_Type::validate($contactID, 'Positive');
503 if (!($contributionRecurID > 0)) {
504 throw new InvalidArgumentException("ContributionRecurID must be a positive integer");
507 return $this->set('contributionRecurID', $label, (int) $contributionRecurID);
511 * Three character currency code.
513 * https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3
515 * @param string $label e.g. 'default'
517 public function getCurrency($label = 'default') {
518 return $this->get('currency', $label);
522 * Three character currency code.
524 * @param string $value
525 * @param string $label e.g. 'default'
527 public function setCurrency($value, $label = 'default') {
528 if (!preg_match('/^[A-Z]{3}$/', $value)) {
529 throw new \
InvalidArgumentException("Attemted to setCurrency with a value that was not an ISO 3166-1 alpha 3 currency code");
531 return $this->set('currency', $label, $value);
536 * @param string $label
540 public function getDescription($label = 'default') {
541 return $this->get('description', $label);
545 * @param string $description
546 * @param string $label e.g. 'default'
548 public function setDescription($description, $label = 'default') {
549 // @todo this logic was copied from a commit that then got deleted. Is it good?
550 $uninformativeStrings = [
551 ts('Online Event Registration: '),
552 ts('Online Contribution: '),
554 $cleanedDescription = str_replace($uninformativeStrings, '', $description);
555 return $this->set('description', $label, $cleanedDescription);
563 public function getEmail($label = 'default') {
564 return $this->get('email', $label);
570 * @param string $email
571 * @param string $label e.g. 'default'
573 public function setEmail($email, $label = 'default') {
574 return $this->set('email', $label, (string) $email);
578 * Amount of money charged in fees by the payment processor.
580 * This is notified by (some) payment processers.
582 * @param string $label
586 public function getFeeAmount($label = 'default') {
587 return $this->get('feeAmount', $label);
591 * @param string $feeAmount
592 * @param string $label e.g. 'default'
594 public function setFeeAmount($feeAmount, $label = 'default') {
595 if (!is_numeric($feeAmount)) {
596 throw new \
InvalidArgumentException("feeAmount must be a number.");
598 return $this->set('feeAmount', $label, (float) $feeAmount);
606 public function getFirstName($label = 'default') {
607 return $this->get('firstName', $label);
613 * @param string $firstName
614 * @param string $label e.g. 'default'
616 public function setFirstName($firstName, $label = 'default') {
617 return $this->set('firstName', $label, (string) $firstName);
621 * Getter for invoiceID.
623 * @param string $label
625 * @return string|null
627 public function getInvoiceID($label = 'default') {
628 return $this->get('invoiceID', $label);
632 * @param string $invoiceID
633 * @param string $label e.g. 'default'
635 public function setInvoiceID($invoiceID, $label = 'default') {
636 return $this->set('invoiceID', $label, $invoiceID);
640 * Getter for isBackOffice.
642 * @param string $label
646 public function getIsBackOffice($label = 'default'):bool {
647 // @todo should this return FALSE instead of exception to keep current situation?
648 return $this->get('isBackOffice', $label);
652 * @param bool $isBackOffice
653 * @param string $label e.g. 'default'
655 public function setIsBackOffice($isBackOffice, $label = 'default') {
656 if (is_null($isBackOffice)) {
657 throw new \
InvalidArgumentException("isBackOffice must be a bool, received NULL.");
659 return $this->set('isBackOffice', $label, (bool) $isBackOffice);
663 * Getter for isRecur.
665 * @param string $label
669 public function getIsRecur($label = 'default'):bool {
670 return $this->get('isRecur', $label);
674 * @param bool $isRecur
675 * @param string $label e.g. 'default'
677 public function setIsRecur($isRecur, $label = 'default') {
678 if (is_null($isRecur)) {
679 throw new \
InvalidArgumentException("isRecur must be a bool, received NULL.");
681 return $this->set('isRecur', $label, (bool) $isRecur);
689 public function getLastName($label = 'default') {
690 return $this->get('lastName', $label);
696 * @param string $lastName
697 * @param string $label e.g. 'default'
699 public function setLastName($lastName, $label = 'default') {
700 return $this->set('lastName', $label, (string) $lastName);
704 * Getter for payment processor generated string for charging.
706 * A payment token could be a single use token (e.g generated by
707 * a client side script) or a token that permits recurring or on demand charging.
709 * The key thing is it is passed to the processor in lieu of card details to
710 * initiate a payment.
712 * Generally if a processor is going to pass in a payment token generated through
713 * javascript it would add 'payment_token' to the array it returns in it's
714 * implementation of getPaymentFormFields. This will add a hidden 'payment_token' field to
715 * the form. A good example is client side encryption where credit card details are replaced by
716 * an encrypted token using a gateway provided javascript script. In this case the javascript will
717 * remove the credit card details from the form before submitting and populate the payment_token field.
719 * A more complex example is used by paypal checkout where the payment token is generated
720 * via a pre-approval process. In that case the doPreApproval function is called on the processor
721 * class to get information to then interact with paypal via js, finally getting a payment token.
722 * (at this stage the pre-approve api is not in core but that is likely to change - we just need
723 * to think about the permissions since we don't want to expose to anonymous user without thinking
724 * through any risk of credit-card testing using it.
726 * If the token is not totally transient it would be saved to civicrm_payment_token.token.
728 * @param string $label
730 * @return string|null
732 public function getPaymentToken($label = 'default') {
733 return $this->get('paymentToken', $label);
737 * @param string $paymentToken
738 * @param string $label e.g. 'default'
740 public function setPaymentToken($paymentToken, $label = 'default') {
741 return $this->set('paymentToken', $label, $paymentToken);
749 public function getPhone($label = 'default') {
750 return $this->get('phone', $label);
756 * @param string $phone
757 * @param string $label e.g. 'default'
759 public function setPhone($phone, $label = 'default') {
760 return $this->set('phone', $label, (string) $phone);
764 * Combined with recurFrequencyUnit this gives how often billing will take place.
766 * e.g every if this is 1 and recurFrequencyUnit is 'month' then it is every 1 month.
769 public function getRecurFrequencyInterval($label = 'default') {
770 return $this->get('recurFrequencyInterval', $label);
774 * @param int $recurFrequencyInterval
775 * @param string $label e.g. 'default'
777 public function setRecurFrequencyInterval($recurFrequencyInterval, $label = 'default') {
778 if (!($recurFrequencyInterval > 0)) {
779 throw new InvalidArgumentException("recurFrequencyInterval must be a positive integer");
782 return $this->set('recurFrequencyInterval', $label, (int) $recurFrequencyInterval);
786 * Getter for recurFrequencyUnit.
787 * Combined with recurFrequencyInterval this gives how often billing will take place.
789 * e.g every if this is 'month' and recurFrequencyInterval is 1 then it is every 1 month.
792 * @param string $label
794 * @return string month|day|year
796 public function getRecurFrequencyUnit($label = 'default') {
797 return $this->get('recurFrequencyUnit', $label);
801 * @param string $recurFrequencyUnit month|day|week|year
802 * @param string $label e.g. 'default'
804 public function setRecurFrequencyUnit($recurFrequencyUnit, $label = 'default') {
805 if (!preg_match('/^day|week|month|year$/', $recurFrequencyUnit)) {
806 throw new \
InvalidArgumentException("recurFrequencyUnit must be day|week|month|year");
808 return $this->set('recurFrequencyUnit', $label, $recurFrequencyUnit);
812 * Set the unique payment processor service provided ID for a particular subscription.
814 * Nb. this is stored in civicrm_contribution_recur.processor_id and is NOT
815 * in any way related to the payment processor ID.
819 public function getRecurProcessorID($label = 'default') {
820 return $this->get('recurProcessorID', $label);
824 * Set the unique payment processor service provided ID for a particular
827 * @param string $input
828 * @param string $label e.g. 'default'
830 public function setRecurProcessorID($input, $label = 'default') {
831 if (empty($input) ||
strlen($input) > 255) {
832 throw new \
InvalidArgumentException("processorID field has max length of 255");
834 return $this->set('recurProcessorID', $label, $input);
838 * Getter for payment processor generated string for the transaction ID.
840 * Note some gateways generate a reference for the order and one for the
841 * payment. This is for the payment reference and is saved to
842 * civicrm_financial_trxn.trxn_id.
844 * @param string $label
846 * @return string|null
848 public function getTransactionID($label = 'default') {
849 return $this->get('transactionID', $label);
853 * @param string $transactionID
854 * @param string $label e.g. 'default'
856 public function setTransactionID($transactionID, $label = 'default') {
857 return $this->set('transactionID', $label, $transactionID);
861 * Getter for trxnResultCode.
863 * Additional information returned by the payment processor regarding the
866 * This would normally be saved in civicrm_financial_trxn.trxn_result_code.
869 * @param string $label
871 * @return string|null
873 public function getTrxnResultCode($label = 'default') {
874 return $this->get('trxnResultCode', $label);
878 * @param string $trxnResultCode
879 * @param string $label e.g. 'default'
881 public function setTrxnResultCode($trxnResultCode, $label = 'default') {
882 return $this->set('trxnResultCode', $label, $trxnResultCode);
885 // Custom Properties.
888 * Sometimes we may need to pass in things that are specific to the Payment
891 * @param string $prop
892 * @param string $label e.g. 'default' or 'old' or 'new'
895 * @throws InvalidArgumentException if trying to use this against a non-custom property.
897 public function getCustomProperty($prop, $label = 'default') {
898 if (isset(static::$propMap[$prop])) {
899 throw new \
InvalidArgumentException("Attempted to get '$prop' via getCustomProperty - must use using its getter.");
901 return $this->props
[$label][$prop] ??
NULL;
905 * We have to leave validation to the processor, but we can still give them a
906 * way to store their data on this PropertyBag
908 * @param string $prop
909 * @param mixed $value
910 * @param string $label e.g. 'default' or 'old' or 'new'
912 * @throws InvalidArgumentException if trying to use this against a non-custom property.
914 public function setCustomProperty($prop, $value, $label = 'default') {
915 if (isset(static::$propMap[$prop])) {
916 throw new \
InvalidArgumentException("Attempted to set '$prop' via setCustomProperty - must use using its setter.");
918 $this->props
[$label][$prop] = $value;