CustomerGateway.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. <?php
  2. namespace Braintree;
  3. use InvalidArgumentException;
  4. /**
  5. * Braintree CustomerGateway module
  6. * Creates and manages Customers
  7. *
  8. * <b>== More information ==</b>
  9. *
  10. * For more detailed information on Customers, see {@link https://developers.braintreepayments.com/reference/response/customer/php https://developers.braintreepayments.com/reference/response/customer/php}
  11. *
  12. * @package Braintree
  13. * @category Resources
  14. */
  15. class CustomerGateway
  16. {
  17. private $_gateway;
  18. private $_config;
  19. private $_http;
  20. public function __construct($gateway)
  21. {
  22. $this->_gateway = $gateway;
  23. $this->_config = $gateway->config;
  24. $this->_config->assertHasAccessTokenOrKeys();
  25. $this->_http = new Http($gateway->config);
  26. }
  27. public function all()
  28. {
  29. $path = $this->_config->merchantPath() . '/customers/advanced_search_ids';
  30. $response = $this->_http->post($path);
  31. $pager = [
  32. 'object' => $this,
  33. 'method' => 'fetch',
  34. 'methodArgs' => [[]]
  35. ];
  36. return new ResourceCollection($response, $pager);
  37. }
  38. public function fetch($query, $ids)
  39. {
  40. $criteria = [];
  41. foreach ($query as $term) {
  42. $criteria[$term->name] = $term->toparam();
  43. }
  44. $criteria["ids"] = CustomerSearch::ids()->in($ids)->toparam();
  45. $path = $this->_config->merchantPath() . '/customers/advanced_search';
  46. $response = $this->_http->post($path, ['search' => $criteria]);
  47. return Util::extractattributeasarray(
  48. $response['customers'],
  49. 'customer'
  50. );
  51. }
  52. /**
  53. * Creates a customer using the given +attributes+. If <tt>:id</tt> is not passed,
  54. * the gateway will generate it.
  55. *
  56. * <code>
  57. * $result = Customer::create(array(
  58. * 'first_name' => 'John',
  59. * 'last_name' => 'Smith',
  60. * 'company' => 'Smith Co.',
  61. * 'email' => 'john@smith.com',
  62. * 'website' => 'www.smithco.com',
  63. * 'fax' => '419-555-1234',
  64. * 'phone' => '614-555-1234'
  65. * ));
  66. * if($result->success) {
  67. * echo 'Created customer ' . $result->customer->id;
  68. * } else {
  69. * echo 'Could not create customer, see result->errors';
  70. * }
  71. * </code>
  72. *
  73. * @access public
  74. * @param array $attribs
  75. * @return Braintree_Result_Successful|Braintree_Result_Error
  76. */
  77. public function create($attribs = [])
  78. {
  79. Util::verifyKeys(self::createSignature(), $attribs);
  80. return $this->_doCreate('/customers', ['customer' => $attribs]);
  81. }
  82. /**
  83. * attempts the create operation assuming all data will validate
  84. * returns a Customer object instead of a Result
  85. *
  86. * @access public
  87. * @param array $attribs
  88. * @return Customer
  89. * @throws Exception\ValidationError
  90. */
  91. public function createNoValidate($attribs = [])
  92. {
  93. $result = $this->create($attribs);
  94. return Util::returnObjectOrThrowException(__CLASS__, $result);
  95. }
  96. /**
  97. * create a customer from a TransparentRedirect operation
  98. *
  99. * @deprecated since version 2.3.0
  100. * @access public
  101. * @param array $attribs
  102. * @return Customer
  103. */
  104. public function createFromTransparentRedirect($queryString)
  105. {
  106. trigger_error("DEPRECATED: Please use TransparentRedirectRequest::confirm", E_USER_NOTICE);
  107. $params = TransparentRedirect::parseAndValidateQueryString(
  108. $queryString
  109. );
  110. return $this->_doCreate(
  111. '/customers/all/confirm_transparent_redirect_request',
  112. ['id' => $params['id']]
  113. );
  114. }
  115. /**
  116. *
  117. * @deprecated since version 2.3.0
  118. * @access public
  119. * @param none
  120. * @return string
  121. */
  122. public function createCustomerUrl()
  123. {
  124. trigger_error("DEPRECATED: Please use TransparentRedirectRequest::url", E_USER_NOTICE);
  125. return $this->_config->baseUrl() . $this->_config->merchantPath() .
  126. '/customers/all/create_via_transparent_redirect_request';
  127. }
  128. /**
  129. * creates a full array signature of a valid create request
  130. * @return array gateway create request format
  131. */
  132. public static function createSignature()
  133. {
  134. $creditCardSignature = CreditCardGateway::createSignature();
  135. unset($creditCardSignature[array_search('customerId', $creditCardSignature)]);
  136. $signature = [
  137. 'id', 'company', 'email', 'fax', 'firstName',
  138. 'lastName', 'phone', 'website', 'deviceData',
  139. 'deviceSessionId', 'fraudMerchantId', 'paymentMethodNonce',
  140. ['riskData' =>
  141. ['customerBrowser', 'customerIp', 'customer_browser', 'customer_ip']
  142. ],
  143. ['creditCard' => $creditCardSignature],
  144. ['customFields' => ['_anyKey_']],
  145. ['options' => [
  146. ['paypal' => [
  147. 'payee_email',
  148. 'payeeEmail',
  149. 'order_id',
  150. 'orderId',
  151. 'custom_field',
  152. 'customField',
  153. 'description',
  154. 'amount',
  155. ['shipping' =>
  156. [
  157. 'firstName', 'lastName', 'company', 'countryName',
  158. 'countryCodeAlpha2', 'countryCodeAlpha3', 'countryCodeNumeric',
  159. 'extendedAddress', 'locality', 'postalCode', 'region',
  160. 'streetAddress'],
  161. ],
  162. ]]
  163. ]],
  164. ];
  165. return $signature;
  166. }
  167. /**
  168. * creates a full array signature of a valid update request
  169. * @return array update request format
  170. */
  171. public static function updateSignature()
  172. {
  173. $creditCardSignature = CreditCardGateway::updateSignature();
  174. foreach($creditCardSignature AS $key => $value) {
  175. if(is_array($value) and array_key_exists('options', $value)) {
  176. array_push($creditCardSignature[$key]['options'], 'updateExistingToken');
  177. }
  178. }
  179. $signature = [
  180. 'id', 'company', 'email', 'fax', 'firstName',
  181. 'lastName', 'phone', 'website', 'deviceData',
  182. 'deviceSessionId', 'fraudMerchantId', 'paymentMethodNonce', 'defaultPaymentMethodToken',
  183. ['creditCard' => $creditCardSignature],
  184. ['customFields' => ['_anyKey_']],
  185. ['options' => [
  186. ['paypal' => [
  187. 'payee_email',
  188. 'payeeEmail',
  189. ['shipping' =>
  190. [
  191. 'firstName', 'lastName', 'company', 'countryName',
  192. 'countryCodeAlpha2', 'countryCodeAlpha3', 'countryCodeNumeric',
  193. 'extendedAddress', 'locality', 'postalCode', 'region',
  194. 'streetAddress'],
  195. ],
  196. ]],
  197. ]],
  198. ];
  199. return $signature;
  200. }
  201. /**
  202. * find a customer by id
  203. *
  204. * @access public
  205. * @param string id customer Id
  206. * @param string associationFilterId association filter Id
  207. * @return Customer|boolean The customer object or false if the request fails.
  208. * @throws Exception\NotFound
  209. */
  210. public function find($id, $associationFilterId = null)
  211. {
  212. $this->_validateId($id);
  213. try {
  214. $queryParams = '';
  215. if ($associationFilterId) {
  216. $queryParams = '?association_filter_id=' . $associationFilterId;
  217. }
  218. $path = $this->_config->merchantPath() . '/customers/' . $id . $queryParams;
  219. $response = $this->_http->get($path);
  220. return Customer::factory($response['customer']);
  221. } catch (Exception\NotFound $e) {
  222. throw new Exception\NotFound(
  223. 'customer with id ' . $id . ' not found'
  224. );
  225. }
  226. }
  227. /**
  228. * credit a customer for the passed transaction
  229. *
  230. * @access public
  231. * @param int $customerId
  232. * @param array $transactionAttribs
  233. * @return Result\Successful|Result\Error
  234. */
  235. public function credit($customerId, $transactionAttribs)
  236. {
  237. $this->_validateId($customerId);
  238. return Transaction::credit(
  239. array_merge($transactionAttribs,
  240. ['customerId' => $customerId]
  241. )
  242. );
  243. }
  244. /**
  245. * credit a customer, assuming validations will pass
  246. *
  247. * returns a Transaction object on success
  248. *
  249. * @access public
  250. * @param int $customerId
  251. * @param array $transactionAttribs
  252. * @return Transaction
  253. * @throws Exception\ValidationError
  254. */
  255. public function creditNoValidate($customerId, $transactionAttribs)
  256. {
  257. $result = $this->credit($customerId, $transactionAttribs);
  258. return Util::returnObjectOrThrowException('Braintree\Transaction', $result);
  259. }
  260. /**
  261. * delete a customer by id
  262. *
  263. * @param string $customerId
  264. */
  265. public function delete($customerId)
  266. {
  267. $this->_validateId($customerId);
  268. $path = $this->_config->merchantPath() . '/customers/' . $customerId;
  269. $this->_http->delete($path);
  270. return new Result\Successful();
  271. }
  272. /**
  273. * create a new sale for a customer
  274. *
  275. * @param string $customerId
  276. * @param array $transactionAttribs
  277. * @return Result\Successful|Result\Error
  278. * @see Transaction::sale()
  279. */
  280. public function sale($customerId, $transactionAttribs)
  281. {
  282. $this->_validateId($customerId);
  283. return Transaction::sale(
  284. array_merge($transactionAttribs,
  285. ['customerId' => $customerId]
  286. )
  287. );
  288. }
  289. /**
  290. * create a new sale for a customer, assuming validations will pass
  291. *
  292. * returns a Transaction object on success
  293. * @access public
  294. * @param string $customerId
  295. * @param array $transactionAttribs
  296. * @return Transaction
  297. * @throws Exception\ValidationsFailed
  298. * @see Transaction::sale()
  299. */
  300. public function saleNoValidate($customerId, $transactionAttribs)
  301. {
  302. $result = $this->sale($customerId, $transactionAttribs);
  303. return Util::returnObjectOrThrowException('Braintree\Transaction', $result);
  304. }
  305. /**
  306. * Returns a ResourceCollection of customers matching the search query.
  307. *
  308. * If <b>query</b> is a string, the search will be a basic search.
  309. * If <b>query</b> is a hash, the search will be an advanced search.
  310. * For more detailed information and examples, see {@link https://developers.braintreepayments.com/reference/request/customer/search/php https://developers.braintreepayments.com/reference/request/customer/search/php}
  311. *
  312. * @param mixed $query search query
  313. * @return ResourceCollection
  314. * @throws InvalidArgumentException
  315. */
  316. public function search($query)
  317. {
  318. $criteria = [];
  319. foreach ($query as $term) {
  320. $result = $term->toparam();
  321. if(is_null($result) || empty($result)) {
  322. throw new InvalidArgumentException('Operator must be provided');
  323. }
  324. $criteria[$term->name] = $term->toparam();
  325. }
  326. $path = $this->_config->merchantPath() . '/customers/advanced_search_ids';
  327. $response = $this->_http->post($path, ['search' => $criteria]);
  328. $pager = [
  329. 'object' => $this,
  330. 'method' => 'fetch',
  331. 'methodArgs' => [$query]
  332. ];
  333. return new ResourceCollection($response, $pager);
  334. }
  335. /**
  336. * updates the customer record
  337. *
  338. * if calling this method in static context, customerId
  339. * is the 2nd attribute. customerId is not sent in object context.
  340. *
  341. * @access public
  342. * @param string $customerId (optional)
  343. * @param array $attributes
  344. * @return Result\Successful|Result\Error
  345. */
  346. public function update($customerId, $attributes)
  347. {
  348. Util::verifyKeys(self::updateSignature(), $attributes);
  349. $this->_validateId($customerId);
  350. return $this->_doUpdate(
  351. 'put',
  352. '/customers/' . $customerId,
  353. ['customer' => $attributes]
  354. );
  355. }
  356. /**
  357. * update a customer record, assuming validations will pass
  358. *
  359. * if calling this method in static context, customerId
  360. * is the 2nd attribute. customerId is not sent in object context.
  361. * returns a Customer object on success
  362. *
  363. * @access public
  364. * @param string $customerId
  365. * @param array $attributes
  366. * @return Customer
  367. * @throws Exception\ValidationsFailed
  368. */
  369. public function updateNoValidate($customerId, $attributes)
  370. {
  371. $result = $this->update($customerId, $attributes);
  372. return Util::returnObjectOrThrowException(__CLASS__, $result);
  373. }
  374. /**
  375. *
  376. * @deprecated since version 2.3.0
  377. * @access public
  378. * @return string
  379. */
  380. public function updateCustomerUrl()
  381. {
  382. trigger_error("DEPRECATED: Please use TransparentRedirectRequest::url", E_USER_NOTICE);
  383. return $this->_config->baseUrl() . $this->_config->merchantPath() .
  384. '/customers/all/update_via_transparent_redirect_request';
  385. }
  386. /**
  387. * update a customer from a TransparentRedirect operation
  388. *
  389. * @deprecated since version 2.3.0
  390. * @access public
  391. * @param string $queryString
  392. * @return object
  393. */
  394. public function updateFromTransparentRedirect($queryString)
  395. {
  396. trigger_error("DEPRECATED: Please use TransparentRedirectRequest::confirm", E_USER_NOTICE);
  397. $params = TransparentRedirect::parseAndValidateQueryString(
  398. $queryString
  399. );
  400. return $this->_doUpdate(
  401. 'post',
  402. '/customers/all/confirm_transparent_redirect_request',
  403. ['id' => $params['id']]
  404. );
  405. }
  406. /* instance methods */
  407. /**
  408. * sets instance properties from an array of values
  409. *
  410. * @ignore
  411. * @access protected
  412. * @param array $customerAttribs array of customer data
  413. * @return void
  414. */
  415. protected function _initialize($customerAttribs)
  416. {
  417. // set the attributes
  418. $this->_attributes = $customerAttribs;
  419. // map each address into its own object
  420. $addressArray = [];
  421. if (isset($customerAttribs['addresses'])) {
  422. foreach ($customerAttribs['addresses'] AS $address) {
  423. $addressArray[] = Address::factory($address);
  424. }
  425. }
  426. $this->_set('addresses', $addressArray);
  427. // map each creditCard into its own object
  428. $creditCardArray = [];
  429. if (isset($customerAttribs['creditCards'])) {
  430. foreach ($customerAttribs['creditCards'] AS $creditCard) {
  431. $creditCardArray[] = CreditCard::factory($creditCard);
  432. }
  433. }
  434. $this->_set('creditCards', $creditCardArray);
  435. // map each coinbaseAccount into its own object
  436. $coinbaseAccountArray = [];
  437. if (isset($customerAttribs['coinbaseAccounts'])) {
  438. foreach ($customerAttribs['coinbaseAccounts'] AS $coinbaseAccount) {
  439. $coinbaseAccountArray[] = CoinbaseAccount::factory($coinbaseAccount);
  440. }
  441. }
  442. $this->_set('coinbaseAccounts', $coinbaseAccountArray);
  443. // map each paypalAccount into its own object
  444. $paypalAccountArray = [];
  445. if (isset($customerAttribs['paypalAccounts'])) {
  446. foreach ($customerAttribs['paypalAccounts'] AS $paypalAccount) {
  447. $paypalAccountArray[] = PayPalAccount::factory($paypalAccount);
  448. }
  449. }
  450. $this->_set('paypalAccounts', $paypalAccountArray);
  451. // map each applePayCard into its own object
  452. $applePayCardArray = [];
  453. if (isset($customerAttribs['applePayCards'])) {
  454. foreach ($customerAttribs['applePayCards'] AS $applePayCard) {
  455. $applePayCardArray[] = ApplePayCard::factory($applePayCard);
  456. }
  457. }
  458. $this->_set('applePayCards', $applePayCardArray);
  459. // map each androidPayCard into its own object
  460. $androidPayCardArray = [];
  461. if (isset($customerAttribs['androidPayCards'])) {
  462. foreach ($customerAttribs['androidPayCards'] AS $androidPayCard) {
  463. $androidPayCardArray[] = AndroidPayCard::factory($androidPayCard);
  464. }
  465. }
  466. $this->_set('androidPayCards', $androidPayCardArray);
  467. $this->_set('paymentMethods', array_merge($this->creditCards, $this->paypalAccounts, $this->applePayCards, $this->coinbaseAccounts, $this->androidPayCards));
  468. }
  469. /**
  470. * returns a string representation of the customer
  471. * @return string
  472. */
  473. public function __toString()
  474. {
  475. return __CLASS__ . '[' .
  476. Util::attributesToString($this->_attributes) .']';
  477. }
  478. /**
  479. * returns false if comparing object is not a Customer,
  480. * or is a Customer with a different id
  481. *
  482. * @param object $otherCust customer to compare against
  483. * @return boolean
  484. */
  485. public function isEqual($otherCust)
  486. {
  487. return !($otherCust instanceof Customer) ? false : $this->id === $otherCust->id;
  488. }
  489. /**
  490. * returns an array containt all of the customer's payment methods
  491. *
  492. * @return array
  493. */
  494. public function paymentMethods()
  495. {
  496. return $this->paymentMethods;
  497. }
  498. /**
  499. * returns the customer's default payment method
  500. *
  501. * @return CreditCard|PayPalAccount|ApplePayCard|AndroidPayCard
  502. */
  503. public function defaultPaymentMethod()
  504. {
  505. $defaultPaymentMethods = array_filter($this->paymentMethods, 'Braintree\\Customer::_defaultPaymentMethodFilter');
  506. return current($defaultPaymentMethods);
  507. }
  508. public static function _defaultPaymentMethodFilter($paymentMethod)
  509. {
  510. return $paymentMethod->isDefault();
  511. }
  512. /* private class properties */
  513. /**
  514. * @access protected
  515. * @var array registry of customer data
  516. */
  517. protected $_attributes = [
  518. 'addresses' => '',
  519. 'company' => '',
  520. 'creditCards' => '',
  521. 'email' => '',
  522. 'fax' => '',
  523. 'firstName' => '',
  524. 'id' => '',
  525. 'lastName' => '',
  526. 'phone' => '',
  527. 'createdAt' => '',
  528. 'updatedAt' => '',
  529. 'website' => '',
  530. ];
  531. /**
  532. * sends the create request to the gateway
  533. *
  534. * @ignore
  535. * @param string $subPath
  536. * @param array $params
  537. * @return mixed
  538. */
  539. public function _doCreate($subPath, $params)
  540. {
  541. $fullPath = $this->_config->merchantPath() . $subPath;
  542. $response = $this->_http->post($fullPath, $params);
  543. return $this->_verifyGatewayResponse($response);
  544. }
  545. /**
  546. * verifies that a valid customer id is being used
  547. * @ignore
  548. * @param string customer id
  549. * @throws InvalidArgumentException
  550. */
  551. private function _validateId($id = null) {
  552. if (is_null($id)) {
  553. throw new InvalidArgumentException(
  554. 'expected customer id to be set'
  555. );
  556. }
  557. if (!preg_match('/^[0-9A-Za-z_-]+$/', $id)) {
  558. throw new InvalidArgumentException(
  559. $id . ' is an invalid customer id.'
  560. );
  561. }
  562. }
  563. /* private class methods */
  564. /**
  565. * sends the update request to the gateway
  566. *
  567. * @ignore
  568. * @param string $subPath
  569. * @param array $params
  570. * @return mixed
  571. */
  572. private function _doUpdate($httpVerb, $subPath, $params)
  573. {
  574. $fullPath = $this->_config->merchantPath() . $subPath;
  575. $response = $this->_http->$httpVerb($fullPath, $params);
  576. return $this->_verifyGatewayResponse($response);
  577. }
  578. /**
  579. * generic method for validating incoming gateway responses
  580. *
  581. * creates a new Customer object and encapsulates
  582. * it inside a Result\Successful object, or
  583. * encapsulates a Errors object inside a Result\Error
  584. * alternatively, throws an Unexpected exception if the response is invalid.
  585. *
  586. * @ignore
  587. * @param array $response gateway response values
  588. * @return Result\Successful|Result\Error
  589. * @throws Exception\Unexpected
  590. */
  591. private function _verifyGatewayResponse($response)
  592. {
  593. if (isset($response['customer'])) {
  594. // return a populated instance of Customer
  595. return new Result\Successful(
  596. Customer::factory($response['customer'])
  597. );
  598. } else if (isset($response['apiErrorResponse'])) {
  599. return new Result\Error($response['apiErrorResponse']);
  600. } else {
  601. throw new Exception\Unexpected(
  602. "Expected customer or apiErrorResponse"
  603. );
  604. }
  605. }
  606. }
  607. class_alias('Braintree\CustomerGateway', 'Braintree_CustomerGateway');