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