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