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