From 5a72aba76a93c7817f73321377fa10cc54220c53 Mon Sep 17 00:00:00 2001 From: Joe Cohen Date: Tue, 31 Dec 2024 00:26:19 -0600 Subject: [PATCH] migrate oxxy pay --- src/Gateways/Femsa/Charges.php | 323 +++++++++++++++++++++++ src/Gateways/Femsa/Customers.php | 86 ++++++ src/Gateways/Femsa/Events.php | 37 +++ src/Gateways/Femsa/FemsaGateway.php | 390 ++++++++++++++++++++++++++++ src/Gateways/Femsa/Recipients.php | 117 +++++++++ src/Gateways/Femsa/Webhooks.php | 88 +++++++ tests/Unit/AbstractTestCase.php | 2 +- tests/Unit/FemsaGatewayTest.php | 282 ++++++++++++++++++++ 8 files changed, 1324 insertions(+), 1 deletion(-) create mode 100644 src/Gateways/Femsa/Charges.php create mode 100644 src/Gateways/Femsa/Customers.php create mode 100644 src/Gateways/Femsa/Events.php create mode 100644 src/Gateways/Femsa/FemsaGateway.php create mode 100644 src/Gateways/Femsa/Recipients.php create mode 100644 src/Gateways/Femsa/Webhooks.php create mode 100644 tests/Unit/FemsaGatewayTest.php diff --git a/src/Gateways/Femsa/Charges.php b/src/Gateways/Femsa/Charges.php new file mode 100644 index 0000000..0f62027 --- /dev/null +++ b/src/Gateways/Femsa/Charges.php @@ -0,0 +1,323 @@ + + */ +class Charges extends AbstractApi implements ChargeInterface +{ + /** + * Create a charge. + * + * @param int|float $amount + * @param mixed $payment + * @param string[] $options + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function create($amount, $payment, $options = []) + { + $params = []; + + $params = $this->addOrder($params, $amount, $options); + $params = $this->addPaymentMethod($params, $amount, $payment, $options); + $params = $this->addOrderDetails($params, $options); + + return $this->gateway->commit('post', $this->gateway->buildUrlFromString('orders'), $params); + } + + /** + * Get a charge. + * + * @param string $id + * @param array $options + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function get($id, $options = []) + { + throw new BadMethodCallException(); + } + + /** + * Complete a charge. + * + * @param string[] $options + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function complete($options = []) + { + throw new BadMethodCallException(); + } + + /** + * Refund a charge. + * + * @param int|float $amount + * @param string $reference + * @param string[] $options + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function refund($amount, $reference, array $options = []) + { + $params = []; + + if ($amount !== null) { + $params['amount'] = $this->gateway->amount($amount); + } + + if (array_key_exists('reason', $options)) { + $params['reason'] = $options['reason']; + } + + $url = sprintf($this->gateway->buildUrlFromString('orders').'/%s/refund', $reference); + + return $this->gateway->commit('post', $url, $params); + } + + /** + * Add order params to request. + * + * @param string[] $params + * @param int $amount + * @param string[] $options + * + * @return array + */ + protected function addOrder(array $params, $amount, array $options) + { + $params['metadata'] = ['reference' => Arr::get($options, 'reference')]; + $params['currency'] = Arr::get($options, 'currency', $this->gateway->getCurrency()); + $params['amount'] = (int) $this->gateway->amount($amount); + + return $params; + } + + /** + * Add payment method to request. + * + * @param string[] $params + * @param int $amount + * @param mixed $payment + * @param string[] $options + * + * @return array + */ + protected function addPaymentMethod(array $params, $amount, $payment, array $options) + { + $params['charges'][0]['payment_method']['type'] = 'oxxo_cash'; + $params['charges'][0]['payment_method']['expires_at'] = Arr::get($options, 'expires', strtotime(date('Y-m-d H:i:s')) + 172800); + + $params['charges'][0]['amount'] = (int) $this->gateway->amount($amount); + + return $params; + } + + /** + * Add address to request. + * + * @param string[] $params + * @param string[] $options + * + * @return array|null + */ + protected function addAddress(array $params, array $options) + { + if ($address = Arr::get($options, 'address') ?: Arr::get($options, 'billing_address')) { + $params['address']['street1'] = Arr::get($address, 'address1'); + if ($address2 = Arr::get($address, 'address2')) { + $params['address']['street2'] = $address2; + } + if ($address3 = Arr::get($address, 'address3')) { + $params['address']['street3'] = $address3; + } + if ($externalNumber = Arr::get($address, 'external_number')) { + $params['address']['external_number'] = $externalNumber; + } + $params['address']['city'] = Arr::get($address, 'city'); + $params['address']['country'] = Arr::get($address, 'country'); + $params['address']['state'] = Arr::get($address, 'state'); + $params['address']['postal_code'] = Arr::get($address, 'zip'); + + return $params; + } + } + + /** + * Add order details params. + * + * @param string[] $params + * @param string[] $options + * + * @return array + */ + protected function addOrderDetails(array $params, array $options) + { + if (isset($options['name'])) { + $params['customer_info']['name'] = Arr::get($options, 'name', ''); + } + + if (isset($options['email'])) { + $params['customer_info']['email'] = Arr::get($options, 'email', ''); + } + + if (isset($options['phone'])) { + $params['customer_info']['phone'] = Arr::get($options, 'phone', ''); + } + + $params = $this->addLineItems($params, $options); + $params = $this->addDiscountLines($params, $options); + $params = $this->addBillingAddress($params, $options); + $params = $this->addShippingAddress($params, $options); + + return $params; + } + + /** + * Add order line items param. + * + * @param string[] $params + * @param string[] $options + * + * @return array + */ + protected function addLineItems(array $params, array $options) + { + if (isset($options['line_items']) && is_array($options['line_items'])) { + foreach ($options['line_items'] as $lineItem) { + $params['line_items'][] = [ + 'name' => Arr::get($lineItem, 'name'), + 'description' => Arr::get($lineItem, 'description'), + 'unit_price' => (int) $this->gateway->amount(Arr::get($lineItem, 'unit_price')), + 'quantity' => Arr::get($lineItem, 'quantity', 1), + 'sku' => Arr::get($lineItem, 'sku'), + 'category' => Arr::get($lineItem, 'category'), + 'type' => Arr::get($lineItem, 'type', 'physical'), + 'tags' => ['none'], + ]; + } + } + + return $params; + } + + /** + * Add order discount lines param. + * + * @param string[] $params + * @param string[] $options + * + * @return array + */ + protected function addDiscountLines(array $params, array $options) + { + if (isset($options['discount'])) { + $type = Arr::get($options, 'discount_type'); + if (!in_array($type, ['loyalty', 'campaign', 'coupon', 'sign'])) { + $type = 'loyalty'; + } + + $code = Arr::get($options, 'discount_code', '---'); + if (strlen($code) < 3) { + $code .= str_repeat('-', 3 - strlen($code)); + } + + $params['discount_lines'][] = [ + 'type' => $type, + 'code' => $code, + 'amount' => $options['discount'], + ]; + } + + return $params; + } + + /** + * Add Billing address to request. + * + * @param string[] $params + * @param string[] $options + * + * @return array + */ + protected function addBillingAddress(array $params, array $options) + { + if ($address = Arr::get($options, 'billing_address') && $taxId = Arr::get($address, 'tax_id') && $companyName = Arr::get($address, 'company_name')) { + $addressFiltered = Arr::filters([ + 'street1' => Arr::get($address, 'address1'), + 'street2' => Arr::get($address, 'address2'), + 'street3' => Arr::get($address, 'address3'), + 'external_number' => Arr::get($address, 'external_number'), + 'city' => Arr::get($address, 'city'), + 'country' => Arr::get($address, 'country'), + 'state' => Arr::get($address, 'state'), + 'postal_code' => Arr::get($address, 'zip'), + ]); + + if (!empty($addressFiltered)) { + $params['fiscal_entity']['address'] = $addressFiltered; + } + + $params['fiscal_entity']['phone'] = Arr::get($address, 'phone', Arr::get($options, 'phone', 'none')); + $params['fiscal_entity']['email'] = Arr::get($address, 'email', Arr::get($options, 'email', 'none')); + $params['fiscal_entity']['tax_id'] = $taxId; + $params['fiscal_entity']['company_name'] = $companyName; + } + + return $params; + } + + /** + * Add Shipping address to request. + * + * @param string[] $params + * @param string[] $options + * + * @return array + */ + protected function addShippingAddress(array $params, array $options) + { + if ($address = Arr::get($options, 'shipping_address')) { + $addressFiltered = Arr::filters([ + 'street1' => Arr::get($address, 'address1'), + 'street2' => Arr::get($address, 'address2'), + 'street3' => Arr::get($address, 'address3'), + 'external_number' => Arr::get($address, 'external_number'), + 'city' => Arr::get($address, 'city'), + 'country' => Arr::get($address, 'country'), + 'state' => Arr::get($address, 'state'), + 'postal_code' => Arr::get($address, 'zip'), + ]); + + if (!empty($addressFiltered)) { + $params['shipping_contact']['address'] = $addressFiltered; + } + + $params['shipping_contact']['receiver'] = Arr::get($address, 'name', Arr::get($options, 'name', '')); + $params['shipping_contact']['phone'] = Arr::get($address, 'phone', Arr::get($options, 'phone', '')); + $params['shipping_contact']['email'] = Arr::get($address, 'email', Arr::get($options, 'email', '')); + + $params['shipping_lines'] = []; + $params['shipping_lines'][0]['description'] = Arr::get($address, 'carrier'); + $params['shipping_lines'][0]['carrier'] = Arr::get($address, 'carrier'); + $params['shipping_lines'][0]['method'] = Arr::get($address, 'service'); + $params['shipping_lines'][0]['amount'] = (int) $this->gateway->amount(Arr::get($address, 'price')); + + if ($trackingNumber = Arr::get($address, 'tracking_number')) { + $params['shipping_lines'][0]['tracking_number'] = $trackingNumber; + } + } + + return $params; + } +} diff --git a/src/Gateways/Femsa/Customers.php b/src/Gateways/Femsa/Customers.php new file mode 100644 index 0000000..606a72f --- /dev/null +++ b/src/Gateways/Femsa/Customers.php @@ -0,0 +1,86 @@ + + */ +class Customers extends AbstractApi implements CustomerInterface +{ + /** + * Find a customer. + * + * @param string $customer + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function find($customer) + { + return $this->gateway->commit('get', $this->gateway->buildUrlFromString('customers').'/'.$customer); + } + + /** + * Create a customer. + * + * @param string[] $attributes + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function create($attributes = []) + { + $params = [ + 'name' => Arr::get($attributes, 'name'), + 'email' => Arr::get($attributes, 'email'), + ]; + + if (isset($attributes['phone'])) { + $params['phone'] = $attributes['phone']; + } + + if (isset($attributes['card'])) { + $params['payment_sources'] = [[ + 'token_id' => $attributes['card'], + 'type' => 'card', + ]]; + } + + return $this->gateway->commit('post', $this->gateway->buildUrlFromString('customers'), $params); + } + + /** + * Update a customer. + * + * @param string $customer + * @param string[] $attributes + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function update($customer, $attributes = []) + { + $params = []; + + if (isset($attributes['name'])) { + $params['name'] = $attributes['name']; + } + + if (isset($attributes['email'])) { + $params['email'] = $attributes['email']; + } + + if (isset($attributes['phone'])) { + $params['phone'] = $attributes['phone']; + } + + if (isset($attributes['default_card'])) { + $params['default_payment_source_id'] = $attributes['default_card']; + } + + return $this->gateway->commit('put', $this->gateway->buildUrlFromString("customers/{$customer}"), $params); + } +} diff --git a/src/Gateways/Femsa/Events.php b/src/Gateways/Femsa/Events.php new file mode 100644 index 0000000..e1ab00c --- /dev/null +++ b/src/Gateways/Femsa/Events.php @@ -0,0 +1,37 @@ + + */ +class Events extends AbstractApi implements EventInterface +{ + /** + * Find all events. + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function all() + { + return $this->gateway->commit('get', $this->gateway->buildUrlFromString('events')); + } + + /** + * Find an event by its id. + * + * @param int|string $id + * @param array $options + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function find($id, array $options = []) + { + return $this->gateway->commit('get', $this->gateway->buildUrlFromString('events'), ['id' => $id]); + } +} diff --git a/src/Gateways/Femsa/FemsaGateway.php b/src/Gateways/Femsa/FemsaGateway.php new file mode 100644 index 0000000..20bc8b2 --- /dev/null +++ b/src/Gateways/Femsa/FemsaGateway.php @@ -0,0 +1,390 @@ + + * @author Arturo Rodríguez + */ +class FemsaGateway extends AbstractGateway +{ + /** + * Gateway API endpoint. + * + * @var string + */ + protected $endpoint = 'https://api.digitalfemsa.io'; + + /** + * Gateway display name. + * + * @var string + */ + protected $displayName = 'femsa'; + + /** + * Gateway default currency. + * + * @var string + */ + protected $defaultCurrency = 'MXN'; + + /** + * Gateway money format. + * + * @var string + */ + protected $moneyFormat = 'cents'; + + /** + * Conekta API version. + * + * @var string + */ + protected $apiVersion = '2.0.0'; + + /** + * Conekta API locale. + * + * @var string + */ + protected $locale = 'es'; + + /** + * Inject the configuration for a Gateway. + * + * @param string[] $config + * + * @return void + */ + public function __construct(array $config) + { + Arr::requires($config, ['private_key']); + + $config['version'] = $this->apiVersion; + $config['locale'] = $this->locale; + + parent::__construct($config); + } + + /** + * Commit a HTTP request. + * + * @param string $method + * @param string $url + * @param string[] $params + * @param string[] $options + * + * @return mixed + */ + public function commit($method, $url, $params = [], $options = []) + { + $userAgent = [ + 'bindings_version' => $this->config['version'], + 'lang' => 'php', + 'lang_version' => phpversion(), + 'publisher' => 'femsa-php', + 'uname' => php_uname(), + ]; + + $request = [ + 'exceptions' => false, + 'timeout' => '80', + 'connect_timeout' => '30', + 'headers' => [ + 'Accept' => "application/vnd.app-v{$this->config['version']}+json", + 'Accept-Language' => $this->config['locale'], + 'Authorization' => 'Basic '.base64_encode($this->config['private_key'].':'), + 'Content-Type' => 'application/json', + 'RaiseHtmlError' => 'false', + 'X-Femsa-Client-User-Agent' => json_encode($userAgent), + 'User-Agent' => 'Femsa/v2 PayMeBindings/'.$this->config['version'], + ], + ]; + + if (!empty($params)) { + $request[$method === 'get' ? 'query' : 'json'] = $params; + } + + $response = $this->performRequest($method, $url, $request); + + try { + return $this->respond($response['body']); + } catch (Exception $e) { + throw new ResponseException($e, $response); + } + } + + /** + * Respond with an array of responses or a single response. + * + * @param array $response + * @param array $_ + * + * @return array|\Shoperti\PayMe\Contracts\ResponseInterface + */ + public function respond($response, $_ = []) + { + if (Arr::get($response, 'object') === 'list') { + if (!empty($response['data'])) { + foreach ($response['data'] as $responseItem) { + $success = Arr::get($responseItem, 'object', 'error') !== 'error'; + + $responses[] = $this->mapResponse($success, $responseItem); + } + + return $responses; + } else { + $response = $response['data']; + } + } + + $success = Arr::get($response, 'object', 'error') !== 'error'; + + return $this->mapResponse($success, $response); + } + + /** + * Map HTTP response to transaction object. + * + * @param bool $success + * @param array $response + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + protected function mapResponse($success, $response) + { + $rawResponse = $response; + + $object = Arr::get($response, 'object'); + + if ($object !== 'error' && array_key_exists('type', $response) && isset($response['data']['object'])) { + $response = $response['data']['object']; + } + + $type = $this->getType($rawResponse); + [$reference, $authorization] = $success ? $this->getReferences($response, $type) : [null, null]; + + $message = ''; + + if ($success) { + $message = 'Transaction approved'; + } elseif ($object === 'error') { + foreach (Arr::get($response, 'details') as $detail) { + $message .= ' '.Arr::get($detail, 'message', ''); + } + $message = ltrim($message); + } else { + $message = Arr::get($response, 'message_to_purchaser') ?: Arr::get($response, 'message', ''); + } + + $isTest = $object === 'error' && isset($response['data']) + ? !(Arr::get($response['data'], 'livemode', true)) + : !(Arr::get($response, 'livemode', true)); + + if ($type === 'refund') { + $rawResponse['amount_refunded'] = $this->getRefundAmount($rawResponse); + } + + return (new Response())->setRaw($rawResponse)->map([ + 'isRedirect' => false, + 'success' => $success, + 'reference' => $reference, + 'message' => $message, + 'test' => $isTest, + 'authorization' => $authorization, + 'status' => $success ? $this->getStatus($response) : new Status('failed'), + 'errorCode' => $success ? null : $this->getErrorCode($response), + 'type' => $type, + ]); + } + + /** + * Get the transaction type. + * + * @param array $rawResponse + * + * @return string + */ + protected function getType($rawResponse) + { + if ($type = Arr::get($rawResponse, 'type')) { + return $type; + } + + switch (Arr::get($rawResponse, 'payment_status')) { + case 'partially_refunded': + case 'refunded': + return 'refund'; + } + + return Arr::get($rawResponse, 'object'); + } + + /** + * Get the last refund amount. + * + * @param array $response + * + * @return int + */ + protected function getRefundAmount($response) + { + $lastCharge = Arr::last($response['charges']['data']); + $lastRefund = Arr::last($lastCharge['refunds']['data']); + + return abs($lastRefund['amount']); + } + + /** + * Get the transaction reference and auth code. + * + * @param array $response + * @param string $type + * + * @return array + */ + protected function getReferences($response, $type) + { + if (in_array($type, ['order', 'refund'])) { + $charges = $response['charges']['data']; + $charge = Arr::last($charges); + + if ($type === 'refund') { + $refunds = $charge['refunds']['data']; + $refund = Arr::last($refunds); + + return [$refund['id'], $refund['auth_code']]; + } + + $id = $charge['id']; + } else { + $id = $response['id']; + } + + return [$id, $this->getAuthorization($response)]; + } + + /** + * Map reference to response. + * + * @param array $response + * + * @return string|null + */ + protected function getAuthorization($response) + { + $object = Arr::get($response, 'object'); + + if ($object == 'customer') { + return Arr::get($response, 'default_payment_source_id'); + } elseif ($object == 'payment_source') { + return Arr::get($response, 'parent_id'); + } elseif ($object == 'payee') { + return Arr::get($response, 'id'); + } elseif ($object == 'transfer') { + return Arr::get($response, 'id'); + } elseif ($object == 'event') { + return Arr::get($response, 'id'); + } + + if (isset($response['charges'])) { + $charges = $response['charges']['data']; + $charge = Arr::last($charges); + $paymentMethod = $charge['payment_method']; + + if (isset($paymentMethod['auth_code'])) { + return $paymentMethod['auth_code']; + } elseif (isset($paymentMethod['reference'])) { + return $paymentMethod['reference']; + } elseif (isset($paymentMethod['clabe'])) { + return $paymentMethod['clabe']; + } + } + } + + /** + * Map Conekta response to status object. + * + * @param array $response + * + * @return \Shoperti\PayMe\Status + */ + protected function getStatus($response) + { + $status = Arr::get($response, 'payment_status') ?: Arr::get($response, 'status'); + + switch ($status) { + case 'pending_payment': + return new Status('pending'); + case 'paid': + case 'refunded': + case 'partially_refunded': + case 'paused': + case 'active': + case 'canceled': + case 'expired': + return new Status($status); + case 'in_trial': + return new Status('trial'); + } + } + + /** + * Map Conekta response to error code object. + * + * @param array $response + * + * @return \Shoperti\PayMe\ErrorCode + */ + protected function getErrorCode($response) + { + $code = isset($response['details']) ? $response['details'][0]['code'] : null; + + switch ($code) { + case 'conekta.errors.processing.bank.declined': + case 'conekta.errors.processing.bank_bindings.declined': + return new ErrorCode('card_declined'); + + case 'conekta.errors.processing.bank.insufficient_funds': + case 'conekta.errors.processing.bank_bindings.insufficient_funds': + return new ErrorCode('insufficient_funds'); + + case 'conekta.errors.processing.charge.card_payment.suspicious_behaviour': + return new ErrorCode('suspected_fraud'); + + case 'conekta.errors.parameter_validation.expiration_date.expired': + return new ErrorCode('invalid_expiry_date'); + + case 'conekta.errors.parameter_validation.card.number': + return new ErrorCode('invalid_number'); + + case 'conekta.errors.parameter_validation.card.cvc': + return new ErrorCode('invalid_cvc'); + } + + $code = Arr::get($response, 'type'); + + return new ErrorCode($code === 'processing_error' ? $code : 'config_error'); + } + + /** + * Get the request url. + * + * @return string + */ + protected function getRequestUrl() + { + return $this->endpoint; + } +} diff --git a/src/Gateways/Femsa/Recipients.php b/src/Gateways/Femsa/Recipients.php new file mode 100644 index 0000000..3a4bf71 --- /dev/null +++ b/src/Gateways/Femsa/Recipients.php @@ -0,0 +1,117 @@ + + */ +class Recipients extends AbstractApi implements RecipientInterface +{ + /** + * Register a recipient. + * + * @param string[] $attributes + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function create($attributes = []) + { + $params = []; + + $params = $this->addPayout($params, $attributes); + $params = $this->addPayoutMethod($params, $attributes); + $params = $this->addPayoutBilling($params, $attributes); + + return $this->gateway->commit('post', $this->gateway->buildUrlFromString('payees'), $params); + } + + // /** + // * Update a recipient. + // * + // * @param int|string $reference + // * @param array $options + // * + // * @return mixed + // */ + // public function update($reference, $options = []) + // { + // // TODO: Check as there's an error from Conekta + // $params = []; + // + // $params = $this->addPayout($params, $options); + // $params = $this->addPayoutMethod($params, $options); + // $params = $this->addPayoutBilling($params, $options); + // + // return $this->commit('put', $this->gateway->buildUrlFromString('payees/'.$reference), $params); + // } + + /** + * Unstore an existing recipient. + * + * @param string $id + * @param string[] $options + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function delete($id, $options = []) + { + return $this->gateway->commit('delete', $this->gateway->buildUrlFromString('payees/'.$id)); + } + + /** + * Add payout to request. + * + * @param string[] $params + * @param string[] $options + * + * @return mixed + */ + protected function addPayout($params, $options) + { + $params['name'] = Arr::get($options, 'name'); + $params['email'] = Arr::get($options, 'email'); + $params['phone'] = Arr::get($options, 'phone'); + + return $params; + } + + /** + * Add payout method to request. + * + * @param string[] $params + * @param string[] $options + * + * @return mixed + */ + protected function addPayoutMethod(array $params, array $options) + { + $params['payout_method'] = []; + $params['payout_method']['type'] = 'bank_transfer_payout_method'; + $params['payout_method']['account_number'] = Arr::get($options, 'account_number'); + $params['payout_method']['account_holder'] = Arr::get($options, 'account_holder'); + + return $params; + } + + /** + * Add payout billing to request. + * + * @param string[] $params + * @param string[] $options + * + * @return mixed + */ + protected function addPayoutBilling(array $params, array $options) + { + $params['billing_address'] = []; + $params['billing_address']['tax_id'] = Arr::get($options, 'tax_id'); // RFC + + return $params; + } +} diff --git a/src/Gateways/Femsa/Webhooks.php b/src/Gateways/Femsa/Webhooks.php new file mode 100644 index 0000000..e8a53cc --- /dev/null +++ b/src/Gateways/Femsa/Webhooks.php @@ -0,0 +1,88 @@ + + */ +class Webhooks extends AbstractApi implements WebhookInterface +{ + /** + * Get all webhooks. + * + * @param array $params + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function all($params = []) + { + return $this->gateway->commit('get', $this->gateway->buildUrlFromString('webhooks')); + } + + /** + * Find a webhook by its id. + * + * @param int|string $id + * @param array $params + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function find($id = null, $params = []) + { + if (!$id) { + throw new InvalidArgumentException('We need an id'); + } + + return $this->gateway->commit('get', $this->gateway->buildUrlFromString('webhooks/'.$id)); + } + + /** + * Create a webhook. + * + * @param array $params + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function create($params = []) + { + return $this->gateway->commit('post', $this->gateway->buildUrlFromString('webhooks'), $params); + } + + /** + * Update a webhook. + * + * @param array $params + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function update($params = []) + { + $id = Arr::get($params, 'id', null); + + if (!$id) { + throw new InvalidArgumentException('We need an id'); + } + + return $this->gateway->commit('post', $this->gateway->buildUrlFromString('webhooks/'.$id), $params); + } + + /** + * Delete a webhook. + * + * @param int|string $id + * @param array $params + * + * @return \Shoperti\PayMe\Contracts\ResponseInterface + */ + public function delete($id, $params = []) + { + return $this->gateway->commit('delete', $this->gateway->buildUrlFromString('webhooks/'.$id)); + } +} diff --git a/tests/Unit/AbstractTestCase.php b/tests/Unit/AbstractTestCase.php index e2200c2..a4eb6ea 100644 --- a/tests/Unit/AbstractTestCase.php +++ b/tests/Unit/AbstractTestCase.php @@ -7,7 +7,7 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase /** * The gateway being tested. * - * @var \Shoperti\PayMe\Gateways\Conekta\AbstractGateway + * @var \Shoperti\PayMe\Gateways\AbstractGateway */ protected $gateway; diff --git a/tests/Unit/FemsaGatewayTest.php b/tests/Unit/FemsaGatewayTest.php new file mode 100644 index 0000000..d127b78 --- /dev/null +++ b/tests/Unit/FemsaGatewayTest.php @@ -0,0 +1,282 @@ + FemsaGateway::class, + 'config' => 'femsa', + ]; + + /** + * @test + * charges()->create() + */ + public function it_should_parse_an_approved_payment() + { + $this->approvedPaymentTest($this->getApprovedPayment()); + } + + /** + * @test + * events()->get() + */ + public function it_should_parse_an_expired_payment() + { + $this->expiredPaymentTest($this->getExpiredPaymentResponse()); + } + + /** + * @test + * events()->get() + */ + public function it_should_parse_an_expired_order() + { + $this->expiredPaymentTest($this->getExpiredOrderResponse()); + } + + private function getApprovedPayment() + { + return [ + 'data' => [ + 'previous_attributes' => [], + 'object' => [ + 'amount' => 233000, + 'livemode' => true, + 'fee' => 9460, + 'created_at' => 1554857326, + 'description' => 'Payment from order', + 'paid_at' => 1554924406, + 'currency' => 'MXN', + 'id' => '111111111111111111111111', + 'customer_id' => null, + 'order_id' => 'ord_2kSGL1fWJuLWE5KRY', + 'payment_method' => [ + 'service_name' => 'OxxoPay', + 'barcode_url' => 'https://s3.amazonaws.com/cash_payment_barcodes/90000000000000.png', + 'store' => '10AAA00000', + 'auth_code' => 11000000, + 'object' => 'cash_payment', + 'type' => 'oxxo', + 'expires_at' => 1555390800, + 'store_name' => 'OXXO', + 'reference' => '93000097488934', + ], + 'object' => 'charge', + 'status' => 'paid', + ], + ], + 'livemode' => true, + 'webhook_status' => 'pending', + 'webhook_logs' => [ + [ + 'id' => 'webhl_00000000000000000', + 'url' => 'http://52.53.178.225/modules/conektaprestashop/notification.php', + 'failed_attempts' => 0, + 'last_http_response_status' => -1, + 'object' => 'webhook_log', + 'last_attempted_at' => 0, + ], + ], + 'id' => '5cae4376518e60732a6f44a4', + 'object' => 'event', + 'type' => 'charge.paid', + 'created_at' => 1554924406, + ]; + } + + private function getExpiredPaymentResponse() + { + return [ + 'data' => [ + 'object' => [ + 'id' => '222222222222222222222222', + 'livemode' => true, + 'created_at' => 1596996917, + 'currency' => 'MXN', + 'payment_method' => [ + 'service_name' => 'OxxoPay', + 'barcode_url' => 'https://s3.amazonaws.com/cash_payment_barcodes/93000481878641.png', + 'object' => 'cash_payment', + 'type' => 'oxxo', + 'expires_at' => 1597169717, + 'store_name' => 'OXXO', + 'reference' => '99999999999999', + ], + 'object' => 'charge', + 'description' => 'Payment from order', + 'status' => 'expired', + 'amount' => 59900, + 'fee' => 2432, + 'customer_id' => null, + 'order_id' => 'ord_00000000000000001', + ], + 'previous_attributes' => [ + 'status' => 'pending_payment', + ], + ], + 'livemode' => true, + 'webhook_status' => 'pending', + 'webhook_logs' => [ + [ + 'id' => 'webhl_22222222222222222', + 'url' => 'https://domain.example.com/hooks/incoming/gateways/gtw_aaaaaaaaaaaaaaaaaaaaaaaaa', + 'failed_attempts' => 0, + 'last_http_response_status' => -1, + 'object' => 'webhook_log', + 'last_attempted_at' => 0, + ], + ], + 'id' => '555555555555555555555555', + 'object' => 'event', + 'type' => 'charge.expired', + 'created_at' => 1597186280, + ]; + } + + private function getExpiredOrderResponse() + { + return [ + 'data' => [ + 'object' => [ + 'livemode' => true, + 'amount' => 59900, + 'currency' => 'MXN', + 'payment_status' => 'expired', + 'amount_refunded' => 0, + 'customer_info' => [ + 'email' => 'custmer@example.com', + 'phone' => '1234567890', + 'name' => 'John Doe', + 'object' => 'customer_info', + ], + 'shipping_contact' => [ + 'receiver' => 'John Doe', + 'phone' => '1234567890', + 'address' => [ + 'street1' => 'Fake Address', + 'street2' => '123', + 'city' => 'City', + 'state' => 'State', + 'country' => 'mx', + 'residential' => true, + 'object' => 'shipping_address', + 'postal_code' => '80000', + ], + 'id' => 'ship_cont_22222222222222222', + 'object' => 'shipping_contact', + 'created_at' => 0, + ], + 'object' => 'order', + 'id' => 'ord_22222222222222222', + 'metadata' => [ + 'reference' => 'payme_order_0000000000000000000000001', + ], + 'created_at' => 1596996917, + 'updated_at' => 1597186281, + 'line_items' => [ + 'object' => 'list', + 'has_more' => false, + 'total' => 1, + 'data' => [ + [ + 'name' => 'Product Name', + 'description' => 'Product Description', + 'unit_price' => 59900, + 'quantity' => 1, + 'sku' => '-', + 'tags' => [ + 'none', + ], + 'object' => 'line_item', + 'id' => 'line_item_22222222222222222', + 'parent_id' => 'ord_22222222222222222', + 'metadata' => [], + 'antifraud_info' => [], + ], + ], + ], + 'shipping_lines' => [ + 'object' => 'list', + 'has_more' => false, + 'total' => 1, + 'data' => [ + [ + 'amount' => 0, + 'carrier' => 'shoperti/kometia', + 'method' => 'pending', + 'object' => 'shipping_line', + 'id' => 'ship_lin_22222222222222222', + 'parent_id' => 'ord_22222222222222222', + ], + ], + ], + 'discount_lines' => [ + 'object' => 'list', + 'has_more' => false, + 'total' => 1, + 'data' => [ + [ + 'code' => '---', + 'amount' => 0, + 'type' => 'coupon', + 'object' => 'discount_line', + 'id' => 'dis_lin_22222222222222222', + 'parent_id' => 'ord_22222222222222222', + ], + ], + ], + 'charges' => [ + 'object' => 'list', + 'has_more' => false, + 'total' => 1, + 'data' => [ + [ + 'id' => '555555555555555555555555', + 'livemode' => true, + 'created_at' => 1596996917, + 'currency' => 'MXN', + 'payment_method' => [ + 'service_name' => 'OxxoPay', + 'barcode_url' => 'https://s3.amazonaws.com/cash_payment_barcodes/93000000000000.png', + 'object' => 'cash_payment', + 'type' => 'oxxo', + 'expires_at' => 1597169717, + 'store_name' => 'OXXO', + 'reference' => '93000000000000', + ], + 'object' => 'charge', + 'description' => 'Payment from order', + 'status' => 'expired', + 'amount' => 59900, + 'fee' => 2432, + 'customer_id' => null, + 'order_id' => 'ord_22222222222222222', + ], + ], + ], + ], + 'previous_attributes' => [], + ], + 'livemode' => true, + 'webhook_status' => 'pending', + 'webhook_logs' => [ + [ + 'id' => 'webhl_22222222222222222', + 'url' => 'https://domain.example.com/hooks/incoming/gateways/gtw_aaaaaaaaaaaaaaaaaaaaaaaaa', + 'failed_attempts' => 0, + 'last_http_response_status' => -1, + 'object' => 'webhook_log', + 'last_attempted_at' => 0, + ], + ], + 'id' => 'ffffffffffffffffffffffff', + 'object' => 'event', + 'type' => 'order.expired', + 'created_at' => 1597186281, + ]; + } +}