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