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