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