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