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