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