Merge pull request #19019 from eileenmcnaughton/total_fail
[civicrm-core.git] / Civi / Payment / PropertyBag.php
1 <?php
2 namespace Civi\Payment;
3
4 use InvalidArgumentException;
5 use CRM_Core_Error;
6 use CRM_Core_PseudoConstant;
7
8 /**
9 * @class
10 *
11 * This class provides getters and setters for arguments needed by CRM_Core_Payment methods.
12 *
13 * The setters know how to validate each setting that they are responsible for.
14 *
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.
19 *
20 * This class is also supposed to help with transition away from array key naming nightmares.
21 *
22 */
23 class PropertyBag implements \ArrayAccess {
24
25 protected $props = ['default' => []];
26
27 protected static $propMap = [
28 'billingStreetAddress' => TRUE,
29 'billingSupplementalAddress1' => TRUE,
30 'billingSupplementalAddress2' => TRUE,
31 'billingSupplementalAddress3' => TRUE,
32 'billingCity' => TRUE,
33 'billingPostalCode' => TRUE,
34 'billingCounty' => TRUE,
35 'billingCountry' => TRUE,
36 'contactID' => TRUE,
37 'contact_id' => 'contactID',
38 'contributionID' => TRUE,
39 'contribution_id' => 'contributionID',
40 'contributionRecurID' => TRUE,
41 'contribution_recur_id' => 'contributionRecurID',
42 'currency' => TRUE,
43 'currencyID' => 'currency',
44 'description' => TRUE,
45 'email' => TRUE,
46 'feeAmount' => TRUE,
47 'fee_amount' => 'feeAmount',
48 'first_name' => 'firstName',
49 'firstName' => TRUE,
50 'invoiceID' => TRUE,
51 'invoice_id' => 'invoiceID',
52 'isBackOffice' => TRUE,
53 'is_back_office' => 'isBackOffice',
54 'isRecur' => TRUE,
55 'is_recur' => 'isRecur',
56 'last_name' => 'lastName',
57 'lastName' => TRUE,
58 'paymentToken' => TRUE,
59 'payment_token' => 'paymentToken',
60 'phone' => TRUE,
61 'recurFrequencyInterval' => TRUE,
62 'frequency_interval' => 'recurFrequencyInterval',
63 'recurFrequencyUnit' => TRUE,
64 'frequency_unit' => 'recurFrequencyUnit',
65 'subscriptionId' => 'recurProcessorID',
66 'recurProcessorID' => TRUE,
67 'transactionID' => TRUE,
68 'transaction_id' => 'transactionID',
69 'trxnResultCode' => TRUE,
70 'isNotifyProcessorOnCancelRecur' => TRUE,
71 ];
72
73
74 /**
75 * @var bool
76 * Temporary, internal variable to help ease transition to PropertyBag.
77 * Used by cast() to suppress legacy warnings.
78 */
79 protected $suppressLegacyWarnings = FALSE;
80
81 /**
82 * Get the property bag.
83 *
84 * This allows us to swap a 'might be an array might be a property bag'
85 * variable for a known PropertyBag.
86 *
87 * @param \Civi\Payment\PropertyBag|array $params
88 *
89 * @return \Civi\Payment\PropertyBag
90 */
91 public static function cast($params) {
92 if ($params instanceof self) {
93 return $params;
94 }
95 $propertyBag = new self();
96 $propertyBag->mergeLegacyInputParams($params);
97 return $propertyBag;
98 }
99
100 /**
101 * Just for unit testing.
102 *
103 * @var string
104 */
105 public $lastWarning;
106
107 /**
108 * Implements ArrayAccess::offsetExists
109 *
110 * @param mixed $offset
111 * @return bool TRUE if we have that value (on our default store)
112 */
113 public function offsetExists ($offset): bool {
114 $prop = $this->handleLegacyPropNames($offset, TRUE);
115 // If there's no prop, assume it's a custom property.
116 $prop = $prop ?? $offset;
117 return array_key_exists($prop, $this->props['default']);
118 }
119
120 /**
121 * Implements ArrayAccess::offsetGet
122 *
123 * @param mixed $offset
124 * @return mixed
125 */
126 public function offsetGet($offset) {
127 try {
128 $prop = $this->handleLegacyPropNames($offset);
129 }
130 catch (InvalidArgumentException $e) {
131
132 CRM_Core_Error::deprecatedFunctionWarning(
133 "proper getCustomProperty('$offset') for non-core properties. "
134 . $e->getMessage(),
135 "PropertyBag array access to get '$offset'"
136 );
137
138 try {
139 return $this->getCustomProperty($offset, 'default');
140 }
141 catch (BadMethodCallException $e) {
142 CRM_Core_Error::deprecatedFunctionWarning(
143 "proper setCustomProperty('$offset', \$value) to store the value (since it is not a core value), then access it with getCustomProperty('$offset'). NULL is returned but in future an exception will be thrown."
144 . $e->getMessage(),
145 "PropertyBag array access to get unset property '$offset'"
146 );
147 return NULL;
148 }
149 }
150
151 CRM_Core_Error::deprecatedFunctionWarning(
152 "get" . ucfirst($offset) . "()",
153 "PropertyBag array access for core property '$offset'"
154 );
155 return $this->get($prop, 'default');
156 }
157
158 /**
159 * Implements ArrayAccess::offsetSet
160 *
161 * @param mixed $offset
162 * @param mixed $value
163 */
164 public function offsetSet($offset, $value) {
165 try {
166 $prop = $this->handleLegacyPropNames($offset);
167 }
168 catch (InvalidArgumentException $e) {
169 // We need to make a lot of noise here, we're being asked to merge in
170 // something we can't validate because we don't know what this property is.
171 // This is fine if it's something particular to a payment processor
172 // (which should be using setCustomProperty) however it could also lead to
173 // things like 'my_weirly_named_contact_id'.
174 //
175 // From 5.28 we suppress this when using PropertyBag::cast() to ease transition.
176 if (!$this->suppressLegacyWarnings) {
177 CRM_Core_Error::deprecatedFunctionWarning(
178 "proper setCustomProperty('$offset', \$value) for non-core properties. "
179 . $e->getMessage(),
180 "PropertyBag array access to set '$offset'"
181 );
182 }
183 $this->setCustomProperty($offset, $value, 'default');
184 return;
185 }
186
187 // Coerce legacy values that were in use but shouldn't be in our new way of doing things.
188 if ($prop === 'feeAmount' && $value === '') {
189 // At least the following classes pass in ZLS for feeAmount.
190 // CRM_Core_Payment_AuthorizeNetTest::testCreateSingleNowDated
191 // CRM_Core_Payment_AuthorizeNetTest::testCreateSinglePostDated
192 $value = 0;
193 }
194
195 // These lines are here (and not in try block) because the catch must only
196 // catch the case when the prop is custom.
197 $setter = 'set' . ucfirst($prop);
198 if (!$this->suppressLegacyWarnings) {
199 CRM_Core_Error::deprecatedFunctionWarning(
200 "$setter()",
201 "PropertyBag array access to set core property '$offset'"
202 );
203 }
204 $this->$setter($value, 'default');
205 }
206
207 /**
208 * Implements ArrayAccess::offsetUnset
209 *
210 * @param mixed $offset
211 */
212 public function offsetUnset ($offset) {
213 $prop = $this->handleLegacyPropNames($offset);
214 unset($this->props['default'][$prop]);
215 }
216
217 /**
218 * @param string $prop
219 * @param bool $silent if TRUE return NULL instead of throwing an exception. This is because offsetExists should be safe and not throw exceptions.
220 * @return string canonical name.
221 * @throws \InvalidArgumentException if prop name not known.
222 */
223 protected function handleLegacyPropNames($prop, $silent = FALSE) {
224 $newName = static::$propMap[$prop] ?? NULL;
225 if ($newName === TRUE) {
226 // Good, modern name.
227 return $prop;
228 }
229 // Handling for legacy addition of billing details.
230 if ($newName === NULL && substr($prop, -2) === '-' . \CRM_Core_BAO_LocationType::getBilling()
231 && isset(static::$propMap[substr($prop, 0, -2)])
232 ) {
233 $newName = substr($prop, 0, -2);
234 }
235
236 if ($newName === NULL) {
237 if ($silent) {
238 // Only for use by offsetExists
239 return;
240 }
241 throw new \InvalidArgumentException("Unknown property '$prop'.");
242 }
243 // Remaining case is legacy name that's been translated.
244 if (!$this->suppressLegacyWarnings) {
245 CRM_Core_Error::deprecatedFunctionWarning("Canonical property name '$newName'", "Legacy property name '$prop'");
246 }
247
248 return $newName;
249 }
250
251 /**
252 * Internal getter.
253 *
254 * @param mixed $prop Valid property name
255 * @param string $label e.g. 'default'
256 *
257 * @return mixed
258 */
259 protected function get($prop, $label) {
260 if (array_key_exists($prop, $this->props[$label] ?? [])) {
261 return $this->props[$label][$prop];
262 }
263 throw new \BadMethodCallException("Property '$prop' has not been set.");
264 }
265
266 /**
267 * Internal setter.
268 *
269 * @param mixed $prop Valid property name
270 * @param string $label e.g. 'default'
271 * @param mixed $value
272 *
273 * @return PropertyBag $this object so you can chain set setters.
274 */
275 protected function set($prop, $label = 'default', $value) {
276 $this->props[$label][$prop] = $value;
277 return $this;
278 }
279
280 /**
281 * DRY code.
282 */
283 protected function coercePseudoConstantStringToInt(string $baoName, string $field, $input) {
284 if (is_numeric($input)) {
285 // We've been given a numeric ID.
286 $_ = (int) $input;
287 }
288 elseif (is_string($input)) {
289 // We've been given a named instrument.
290 $_ = (int) CRM_Core_PseudoConstant::getKey($baoName, $field, $input);
291 }
292 else {
293 throw new InvalidArgumentException("Expected an integer ID or a String name for $field.");
294 }
295 if (!($_ > 0)) {
296 throw new InvalidArgumentException("Expected an integer greater than 0 for $field.");
297 }
298 return $_;
299 }
300
301 /**
302 */
303 public function has($prop, $label = 'default') {
304 // We do NOT translate legacy prop names since only new code should be
305 // using this method, and new code should be using canonical names.
306 // $prop = $this->handleLegacyPropNames($prop);
307 return isset($this->props[$label][$prop]);
308 }
309
310 /**
311 * This is used to merge values from an array.
312 * It's a transitional, internal function and should not be used!
313 *
314 * @param array $data
315 */
316 public function mergeLegacyInputParams($data) {
317 // Suppress legacy warnings for merging an array of data as this
318 // suits our migration plan at this moment. Future behaviour may differ.
319 // @see https://github.com/civicrm/civicrm-core/pull/17643
320 $this->suppressLegacyWarnings = TRUE;
321 foreach ($data as $key => $value) {
322 if ($value !== NULL && $value !== '') {
323 $this->offsetSet($key, $value);
324 }
325 }
326 $this->suppressLegacyWarnings = FALSE;
327 }
328
329 /**
330 * Throw an exception if any of the props is unset.
331 *
332 * @param array $props Array of proper property names (no legacy aliases allowed).
333 *
334 * @return PropertyBag
335 */
336 public function require(array $props) {
337 $missing = [];
338 foreach ($props as $prop) {
339 if (!isset($this->props['default'][$prop])) {
340 $missing[] = $prop;
341 }
342 }
343 if ($missing) {
344 throw new \InvalidArgumentException("Required properties missing: " . implode(', ', $missing));
345 }
346 return $this;
347 }
348
349 // Public getters, setters.
350
351 /**
352 * Get a property by its name (but still using its getter).
353 *
354 * @param string $prop valid property name, like contactID
355 * @param bool $allowUnset If TRUE, return the default value if the property is
356 * not set - normal behaviour would be to throw an exception.
357 * @param mixed $default
358 * @param string $label e.g. 'default' or 'old' or 'new'
359 *
360 * @return mixed
361 */
362 public function getter($prop, $allowUnset = FALSE, $default = NULL, $label = 'default') {
363
364 if ((static::$propMap[$prop] ?? NULL) === TRUE) {
365 // This is a standard property that will have a getter method.
366 $getter = 'get' . ucfirst($prop);
367 return (!$allowUnset || $this->has($prop, $label))
368 ? $this->$getter($label)
369 : $default;
370 }
371
372 // This is not a property name we know, but they could be requesting a
373 // custom property.
374 return (!$allowUnset || $this->has($prop, $label))
375 ? $this->getCustomProperty($prop, $label)
376 : $default;
377 }
378
379 /**
380 * Set a property by its name (but still using its setter).
381 *
382 * @param string $prop valid property name, like contactID
383 * @param mixed $value
384 * @param string $label e.g. 'default' or 'old' or 'new'
385 *
386 * @return mixed
387 */
388 public function setter($prop, $value = NULL, $label = 'default') {
389 if ((static::$propMap[$prop] ?? NULL) === TRUE) {
390 // This is a standard property.
391 $setter = 'set' . ucfirst($prop);
392 return $this->$setter($value, $label);
393 }
394 // We don't allow using the setter for custom properties.
395 throw new \BadMethodCallException("Cannot use generic setter with non-standard properties; you must use setCustomProperty for custom properties.");
396 }
397
398 /**
399 * Get the monetary amount.
400 */
401 public function getAmount($label = 'default') {
402 return $this->get('amount', $label);
403 }
404
405 /**
406 * Set the monetary amount.
407 *
408 * - We expect to be called with a string amount with optional decimals using
409 * a '.' as the decimal point (not a ',').
410 *
411 * - We're ok with floats/ints being passed in, too, but we'll cast them to a
412 * string.
413 *
414 * - Negatives are fine.
415 *
416 * @see https://github.com/civicrm/civicrm-core/pull/18219
417 *
418 * @param string|float|int $value
419 * @param string $label
420 */
421 public function setAmount($value, $label = 'default') {
422 if (!is_numeric($value)) {
423 throw new \InvalidArgumentException("setAmount requires a numeric amount value");
424 }
425 return $this->set('amount', $label, filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION));
426 }
427
428 /**
429 * BillingStreetAddress getter.
430 *
431 * @return string
432 */
433 public function getBillingStreetAddress($label = 'default') {
434 return $this->get('billingStreetAddress', $label);
435 }
436
437 /**
438 * BillingStreetAddress setter.
439 *
440 * @param string $input
441 * @param string $label e.g. 'default'
442 */
443 public function setBillingStreetAddress($input, $label = 'default') {
444 return $this->set('billingStreetAddress', $label, (string) $input);
445 }
446
447 /**
448 * BillingSupplementalAddress1 getter.
449 *
450 * @return string
451 */
452 public function getBillingSupplementalAddress1($label = 'default') {
453 return $this->get('billingSupplementalAddress1', $label);
454 }
455
456 /**
457 * BillingSupplementalAddress1 setter.
458 *
459 * @param string $input
460 * @param string $label e.g. 'default'
461 */
462 public function setBillingSupplementalAddress1($input, $label = 'default') {
463 return $this->set('billingSupplementalAddress1', $label, (string) $input);
464 }
465
466 /**
467 * BillingSupplementalAddress2 getter.
468 *
469 * @return string
470 */
471 public function getBillingSupplementalAddress2($label = 'default') {
472 return $this->get('billingSupplementalAddress2', $label);
473 }
474
475 /**
476 * BillingSupplementalAddress2 setter.
477 *
478 * @param string $input
479 * @param string $label e.g. 'default'
480 */
481 public function setBillingSupplementalAddress2($input, $label = 'default') {
482 return $this->set('billingSupplementalAddress2', $label, (string) $input);
483 }
484
485 /**
486 * BillingSupplementalAddress3 getter.
487 *
488 * @return string
489 */
490 public function getBillingSupplementalAddress3($label = 'default') {
491 return $this->get('billingSupplementalAddress3', $label);
492 }
493
494 /**
495 * BillingSupplementalAddress3 setter.
496 *
497 * @param string $input
498 * @param string $label e.g. 'default'
499 */
500 public function setBillingSupplementalAddress3($input, $label = 'default') {
501 return $this->set('billingSupplementalAddress3', $label, (string) $input);
502 }
503
504 /**
505 * BillingCity getter.
506 *
507 * @return string
508 */
509 public function getBillingCity($label = 'default') {
510 return $this->get('billingCity', $label);
511 }
512
513 /**
514 * BillingCity setter.
515 *
516 * @param string $input
517 * @param string $label e.g. 'default'
518 *
519 * @return \Civi\Payment\PropertyBag
520 */
521 public function setBillingCity($input, $label = 'default') {
522 return $this->set('billingCity', $label, (string) $input);
523 }
524
525 /**
526 * BillingPostalCode getter.
527 *
528 * @return string
529 */
530 public function getBillingPostalCode($label = 'default') {
531 return $this->get('billingPostalCode', $label);
532 }
533
534 /**
535 * BillingPostalCode setter.
536 *
537 * @param string $input
538 * @param string $label e.g. 'default'
539 */
540 public function setBillingPostalCode($input, $label = 'default') {
541 return $this->set('billingPostalCode', $label, (string) $input);
542 }
543
544 /**
545 * BillingCounty getter.
546 *
547 * @return string
548 */
549 public function getBillingCounty($label = 'default') {
550 return $this->get('billingCounty', $label);
551 }
552
553 /**
554 * BillingCounty setter.
555 *
556 * Nb. we can't validate this unless we have the country ID too, so we don't.
557 *
558 * @param string $input
559 * @param string $label e.g. 'default'
560 */
561 public function setBillingCounty($input, $label = 'default') {
562 return $this->set('billingCounty', $label, (string) $input);
563 }
564
565 /**
566 * BillingCountry getter.
567 *
568 * @return string
569 */
570 public function getBillingCountry($label = 'default') {
571 return $this->get('billingCountry', $label);
572 }
573
574 /**
575 * BillingCountry setter.
576 *
577 * Nb. We require and we store a 2 character country code.
578 *
579 * @param string $input
580 * @param string $label e.g. 'default'
581 */
582 public function setBillingCountry($input, $label = 'default') {
583 if (!is_string($input) || strlen($input) !== 2) {
584 throw new \InvalidArgumentException("setBillingCountry expects ISO 3166-1 alpha-2 country code.");
585 }
586 if (!CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_Address', 'country_id', $input)) {
587 throw new \InvalidArgumentException("setBillingCountry expects ISO 3166-1 alpha-2 country code.");
588 }
589 return $this->set('billingCountry', $label, (string) $input);
590 }
591
592 /**
593 * @return int
594 */
595 public function getContactID($label = 'default'): int {
596 return $this->get('contactID', $label);
597 }
598
599 /**
600 * @param int $contactID
601 * @param string $label
602 */
603 public function setContactID($contactID, $label = 'default') {
604 // We don't use this because it counts zero as positive: CRM_Utils_Type::validate($contactID, 'Positive');
605 if (!($contactID > 0)) {
606 throw new InvalidArgumentException("ContactID must be a positive integer");
607 }
608
609 return $this->set('contactID', $label, (int) $contactID);
610 }
611
612 /**
613 * Getter for contributionID.
614 *
615 * @return int|null
616 * @param string $label
617 */
618 public function getContributionID($label = 'default') {
619 return $this->get('contributionID', $label);
620 }
621
622 /**
623 * @param int $contributionID
624 * @param string $label e.g. 'default'
625 */
626 public function setContributionID($contributionID, $label = 'default') {
627 // We don't use this because it counts zero as positive: CRM_Utils_Type::validate($contactID, 'Positive');
628 if (!($contributionID > 0)) {
629 throw new InvalidArgumentException("ContributionID must be a positive integer");
630 }
631
632 return $this->set('contributionID', $label, (int) $contributionID);
633 }
634
635 /**
636 * Getter for contributionRecurID.
637 *
638 * @return int|null
639 * @param string $label
640 */
641 public function getContributionRecurID($label = 'default') {
642 return $this->get('contributionRecurID', $label);
643 }
644
645 /**
646 * @param int $contributionRecurID
647 * @param string $label e.g. 'default'
648 *
649 * @return \Civi\Payment\PropertyBag
650 */
651 public function setContributionRecurID($contributionRecurID, $label = 'default') {
652 // We don't use this because it counts zero as positive: CRM_Utils_Type::validate($contactID, 'Positive');
653 if (!($contributionRecurID > 0)) {
654 throw new InvalidArgumentException('ContributionRecurID must be a positive integer');
655 }
656
657 return $this->set('contributionRecurID', $label, (int) $contributionRecurID);
658 }
659
660 /**
661 * Three character currency code.
662 *
663 * https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3
664 *
665 * @param string $label e.g. 'default'
666 */
667 public function getCurrency($label = 'default') {
668 return $this->get('currency', $label);
669 }
670
671 /**
672 * Three character currency code.
673 *
674 * @param string $value
675 * @param string $label e.g. 'default'
676 */
677 public function setCurrency($value, $label = 'default') {
678 if (!preg_match('/^[A-Z]{3}$/', $value)) {
679 throw new \InvalidArgumentException("Attemted to setCurrency with a value that was not an ISO 3166-1 alpha 3 currency code");
680 }
681 return $this->set('currency', $label, $value);
682 }
683
684 /**
685 *
686 * @param string $label
687 *
688 * @return string
689 */
690 public function getDescription($label = 'default') {
691 return $this->get('description', $label);
692 }
693
694 /**
695 * @param string $description
696 * @param string $label e.g. 'default'
697 */
698 public function setDescription($description, $label = 'default') {
699 // @todo this logic was copied from a commit that then got deleted. Is it good?
700 $uninformativeStrings = [
701 ts('Online Event Registration: '),
702 ts('Online Contribution: '),
703 ];
704 $cleanedDescription = str_replace($uninformativeStrings, '', $description);
705 return $this->set('description', $label, $cleanedDescription);
706 }
707
708 /**
709 * Email getter.
710 *
711 * @return string
712 */
713 public function getEmail($label = 'default') {
714 return $this->get('email', $label);
715 }
716
717 /**
718 * Email setter.
719 *
720 * @param string $email
721 * @param string $label e.g. 'default'
722 */
723 public function setEmail($email, $label = 'default') {
724 return $this->set('email', $label, (string) $email);
725 }
726
727 /**
728 * Amount of money charged in fees by the payment processor.
729 *
730 * This is notified by (some) payment processers.
731 *
732 * @param string $label
733 *
734 * @return float
735 */
736 public function getFeeAmount($label = 'default') {
737 return $this->get('feeAmount', $label);
738 }
739
740 /**
741 * @param string $feeAmount
742 * @param string $label e.g. 'default'
743 */
744 public function setFeeAmount($feeAmount, $label = 'default') {
745 if (!is_numeric($feeAmount)) {
746 throw new \InvalidArgumentException("feeAmount must be a number.");
747 }
748 return $this->set('feeAmount', $label, (float) $feeAmount);
749 }
750
751 /**
752 * First name
753 *
754 * @return string
755 */
756 public function getFirstName($label = 'default') {
757 return $this->get('firstName', $label);
758 }
759
760 /**
761 * First name setter.
762 *
763 * @param string $firstName
764 * @param string $label e.g. 'default'
765 */
766 public function setFirstName($firstName, $label = 'default') {
767 return $this->set('firstName', $label, (string) $firstName);
768 }
769
770 /**
771 * Getter for invoiceID.
772 *
773 * @param string $label
774 *
775 * @return string|null
776 */
777 public function getInvoiceID($label = 'default') {
778 return $this->get('invoiceID', $label);
779 }
780
781 /**
782 * @param string $invoiceID
783 * @param string $label e.g. 'default'
784 */
785 public function setInvoiceID($invoiceID, $label = 'default') {
786 return $this->set('invoiceID', $label, $invoiceID);
787 }
788
789 /**
790 * Getter for isBackOffice.
791 *
792 * @param string $label
793 *
794 * @return bool|null
795 */
796 public function getIsBackOffice($label = 'default'):bool {
797 // @todo should this return FALSE instead of exception to keep current situation?
798 return $this->get('isBackOffice', $label);
799 }
800
801 /**
802 * @param bool $isBackOffice
803 * @param string $label e.g. 'default'
804 */
805 public function setIsBackOffice($isBackOffice, $label = 'default') {
806 if (is_null($isBackOffice)) {
807 throw new \InvalidArgumentException("isBackOffice must be a bool, received NULL.");
808 }
809 return $this->set('isBackOffice', $label, (bool) $isBackOffice);
810 }
811
812 /**
813 * Getter for isRecur.
814 *
815 * @param string $label
816 *
817 * @return bool
818 */
819 public function getIsRecur($label = 'default'):bool {
820 if (!$this->has('isRecur')) {
821 return FALSE;
822 }
823 return $this->get('isRecur', $label);
824 }
825
826 /**
827 * @param bool $isRecur
828 * @param string $label e.g. 'default'
829 */
830 public function setIsRecur($isRecur, $label = 'default') {
831 if (is_null($isRecur)) {
832 throw new \InvalidArgumentException("isRecur must be a bool, received NULL.");
833 }
834 return $this->set('isRecur', $label, (bool) $isRecur);
835 }
836
837 /**
838 * Set whether the user has selected to notify the processor of a cancellation request.
839 *
840 * When cancelling the user may be presented with an option to notify the processor. The payment
841 * processor can take their response, if present, into account.
842 *
843 * @param bool $value
844 * @param string $label e.g. 'default'
845 *
846 * @return \Civi\Payment\PropertyBag
847 */
848 public function setIsNotifyProcessorOnCancelRecur($value, $label = 'default') {
849 return $this->set('isNotifyProcessorOnCancelRecur', $label, (bool) $value);
850 }
851
852 /**
853 * Get whether the user has selected to notify the processor of a cancellation request.
854 *
855 * When cancelling the user may be presented with an option to notify the processor. The payment
856 * processor can take their response, if present, into account.
857 *
858 * @param string $label e.g. 'default'
859 *
860 * @return \Civi\Payment\PropertyBag
861 */
862 public function getIsNotifyProcessorOnCancelRecur($label = 'default') {
863 return $this->get('isNotifyProcessorOnCancelRecur', $label);
864 }
865
866 /**
867 * Last name
868 *
869 * @return string
870 */
871 public function getLastName($label = 'default') {
872 return $this->get('lastName', $label);
873 }
874
875 /**
876 * Last name setter.
877 *
878 * @param string $lastName
879 * @param string $label e.g. 'default'
880 */
881 public function setLastName($lastName, $label = 'default') {
882 return $this->set('lastName', $label, (string) $lastName);
883 }
884
885 /**
886 * Getter for payment processor generated string for charging.
887 *
888 * A payment token could be a single use token (e.g generated by
889 * a client side script) or a token that permits recurring or on demand charging.
890 *
891 * The key thing is it is passed to the processor in lieu of card details to
892 * initiate a payment.
893 *
894 * Generally if a processor is going to pass in a payment token generated through
895 * javascript it would add 'payment_token' to the array it returns in it's
896 * implementation of getPaymentFormFields. This will add a hidden 'payment_token' field to
897 * the form. A good example is client side encryption where credit card details are replaced by
898 * an encrypted token using a gateway provided javascript script. In this case the javascript will
899 * remove the credit card details from the form before submitting and populate the payment_token field.
900 *
901 * A more complex example is used by paypal checkout where the payment token is generated
902 * via a pre-approval process. In that case the doPreApproval function is called on the processor
903 * class to get information to then interact with paypal via js, finally getting a payment token.
904 * (at this stage the pre-approve api is not in core but that is likely to change - we just need
905 * to think about the permissions since we don't want to expose to anonymous user without thinking
906 * through any risk of credit-card testing using it.
907 *
908 * If the token is not totally transient it would be saved to civicrm_payment_token.token.
909 *
910 * @param string $label
911 *
912 * @return string|null
913 */
914 public function getPaymentToken($label = 'default') {
915 return $this->get('paymentToken', $label);
916 }
917
918 /**
919 * @param string $paymentToken
920 * @param string $label e.g. 'default'
921 */
922 public function setPaymentToken($paymentToken, $label = 'default') {
923 return $this->set('paymentToken', $label, $paymentToken);
924 }
925
926 /**
927 * Phone getter.
928 *
929 * @return string
930 */
931 public function getPhone($label = 'default') {
932 return $this->get('phone', $label);
933 }
934
935 /**
936 * Phone setter.
937 *
938 * @param string $phone
939 * @param string $label e.g. 'default'
940 */
941 public function setPhone($phone, $label = 'default') {
942 return $this->set('phone', $label, (string) $phone);
943 }
944
945 /**
946 * Combined with recurFrequencyUnit this gives how often billing will take place.
947 *
948 * e.g every if this is 1 and recurFrequencyUnit is 'month' then it is every 1 month.
949 * @return int|null
950 */
951 public function getRecurFrequencyInterval($label = 'default') {
952 return $this->get('recurFrequencyInterval', $label);
953 }
954
955 /**
956 * @param int $recurFrequencyInterval
957 * @param string $label e.g. 'default'
958 */
959 public function setRecurFrequencyInterval($recurFrequencyInterval, $label = 'default') {
960 if (!($recurFrequencyInterval > 0)) {
961 throw new InvalidArgumentException("recurFrequencyInterval must be a positive integer");
962 }
963
964 return $this->set('recurFrequencyInterval', $label, (int) $recurFrequencyInterval);
965 }
966
967 /**
968 * Getter for recurFrequencyUnit.
969 * Combined with recurFrequencyInterval this gives how often billing will take place.
970 *
971 * e.g every if this is 'month' and recurFrequencyInterval is 1 then it is every 1 month.
972 *
973 *
974 * @param string $label
975 *
976 * @return string month|day|year
977 */
978 public function getRecurFrequencyUnit($label = 'default') {
979 return $this->get('recurFrequencyUnit', $label);
980 }
981
982 /**
983 * @param string $recurFrequencyUnit month|day|week|year
984 * @param string $label e.g. 'default'
985 */
986 public function setRecurFrequencyUnit($recurFrequencyUnit, $label = 'default') {
987 if (!preg_match('/^day|week|month|year$/', $recurFrequencyUnit)) {
988 throw new \InvalidArgumentException("recurFrequencyUnit must be day|week|month|year");
989 }
990 return $this->set('recurFrequencyUnit', $label, $recurFrequencyUnit);
991 }
992
993 /**
994 * Set the unique payment processor service provided ID for a particular subscription.
995 *
996 * Nb. this is stored in civicrm_contribution_recur.processor_id and is NOT
997 * in any way related to the payment processor ID.
998 *
999 * @param string $label
1000 *
1001 * @return string|null
1002 */
1003 public function getRecurProcessorID($label = 'default') {
1004 return $this->get('recurProcessorID', $label);
1005 }
1006
1007 /**
1008 * Set the unique payment processor service provided ID for a particular
1009 * subscription.
1010 *
1011 * See https://github.com/civicrm/civicrm-core/pull/17292 for discussion
1012 * of how this function accepting NULL fits with standard / planned behaviour.
1013 *
1014 * @param string|null $input
1015 * @param string $label e.g. 'default'
1016 *
1017 * @return \Civi\Payment\PropertyBag
1018 */
1019 public function setRecurProcessorID($input, $label = 'default') {
1020 if ($input === '') {
1021 $input = NULL;
1022 }
1023 if (strlen($input) > 255 || in_array($input, [FALSE, 0], TRUE)) {
1024 throw new \InvalidArgumentException('processorID field has max length of 255');
1025 }
1026 return $this->set('recurProcessorID', $label, $input);
1027 }
1028
1029 /**
1030 * Getter for payment processor generated string for the transaction ID.
1031 *
1032 * Note some gateways generate a reference for the order and one for the
1033 * payment. This is for the payment reference and is saved to
1034 * civicrm_financial_trxn.trxn_id.
1035 *
1036 * @param string $label
1037 *
1038 * @return string|null
1039 */
1040 public function getTransactionID($label = 'default') {
1041 return $this->get('transactionID', $label);
1042 }
1043
1044 /**
1045 * @param string $transactionID
1046 * @param string $label e.g. 'default'
1047 */
1048 public function setTransactionID($transactionID, $label = 'default') {
1049 return $this->set('transactionID', $label, $transactionID);
1050 }
1051
1052 /**
1053 * Getter for trxnResultCode.
1054 *
1055 * Additional information returned by the payment processor regarding the
1056 * payment outcome.
1057 *
1058 * This would normally be saved in civicrm_financial_trxn.trxn_result_code.
1059 *
1060 *
1061 * @param string $label
1062 *
1063 * @return string|null
1064 */
1065 public function getTrxnResultCode($label = 'default') {
1066 return $this->get('trxnResultCode', $label);
1067 }
1068
1069 /**
1070 * @param string $trxnResultCode
1071 * @param string $label e.g. 'default'
1072 */
1073 public function setTrxnResultCode($trxnResultCode, $label = 'default') {
1074 return $this->set('trxnResultCode', $label, $trxnResultCode);
1075 }
1076
1077 // Custom Properties.
1078
1079 /**
1080 * Sometimes we may need to pass in things that are specific to the Payment
1081 * Processor.
1082 *
1083 * @param string $prop
1084 * @param string $label e.g. 'default' or 'old' or 'new'
1085 * @return mixed
1086 *
1087 * @throws InvalidArgumentException if trying to use this against a non-custom property.
1088 */
1089 public function getCustomProperty($prop, $label = 'default') {
1090 if (isset(static::$propMap[$prop])) {
1091 throw new \InvalidArgumentException("Attempted to get '$prop' via getCustomProperty - must use using its getter.");
1092 }
1093
1094 if (!array_key_exists($prop, $this->props[$label] ?? [])) {
1095 throw new \BadMethodCallException("Property '$prop' has not been set.");
1096 }
1097 return $this->props[$label][$prop] ?? NULL;
1098 }
1099
1100 /**
1101 * We have to leave validation to the processor, but we can still give them a
1102 * way to store their data on this PropertyBag
1103 *
1104 * @param string $prop
1105 * @param mixed $value
1106 * @param string $label e.g. 'default' or 'old' or 'new'
1107 *
1108 * @throws InvalidArgumentException if trying to use this against a non-custom property.
1109 */
1110 public function setCustomProperty($prop, $value, $label = 'default') {
1111 if (isset(static::$propMap[$prop])) {
1112 throw new \InvalidArgumentException("Attempted to set '$prop' via setCustomProperty - must use using its setter.");
1113 }
1114 $this->props[$label][$prop] = $value;
1115 }
1116
1117 }