2017-01-30 20:40:43 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Ninja\PaymentDrivers;
|
2016-06-20 16:14:43 +02:00
|
|
|
|
2016-06-23 15:15:52 +02:00
|
|
|
use App\Models\Payment;
|
2017-09-04 20:36:12 +02:00
|
|
|
use App\Models\Invitation;
|
2016-06-20 16:14:43 +02:00
|
|
|
use App\Models\PaymentMethod;
|
2017-09-04 12:14:58 +02:00
|
|
|
use App\Models\GatewayType;
|
2017-01-30 20:40:43 +01:00
|
|
|
use Cache;
|
|
|
|
use Exception;
|
2017-08-31 14:55:15 +02:00
|
|
|
use App\Models\PaymentType;
|
2019-07-09 22:20:02 +02:00
|
|
|
use Stripe\PaymentIntent;
|
|
|
|
use Stripe\Stripe;
|
2016-06-20 16:14:43 +02:00
|
|
|
|
|
|
|
class StripePaymentDriver extends BasePaymentDriver
|
|
|
|
{
|
|
|
|
protected $customerReferenceParam = 'customerReference';
|
2017-05-16 11:20:35 +02:00
|
|
|
public $canRefundPayments = true;
|
2016-06-20 16:14:43 +02:00
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
protected function prepareStripeAPI()
|
|
|
|
{
|
|
|
|
Stripe::setApiKey($this->accountGateway->getConfigField('apiKey'));
|
|
|
|
}
|
|
|
|
|
2016-06-22 20:42:09 +02:00
|
|
|
public function gatewayTypes()
|
2016-06-20 16:14:43 +02:00
|
|
|
{
|
2017-01-30 20:40:43 +01:00
|
|
|
$types = [
|
2016-06-20 16:14:43 +02:00
|
|
|
GATEWAY_TYPE_CREDIT_CARD,
|
2017-01-30 20:40:43 +01:00
|
|
|
GATEWAY_TYPE_TOKEN,
|
2016-06-20 16:14:43 +02:00
|
|
|
];
|
|
|
|
|
2017-09-05 15:37:19 +02:00
|
|
|
if ($gateway = $this->accountGateway) {
|
2017-09-06 10:12:34 +02:00
|
|
|
$achEnabled = $gateway->getAchEnabled();
|
|
|
|
$sofortEnabled = $gateway->getSofortEnabled();
|
|
|
|
if ($achEnabled && $sofortEnabled) {
|
|
|
|
if ($this->invitation) {
|
|
|
|
$country = ($this->client() && $this->client()->country) ? $this->client()->country->iso_3166_3 : ($this->account()->country ? $this->account()->country->iso_3166_3 : false);
|
|
|
|
// https://stripe.com/docs/sources/sofort
|
|
|
|
if ($country && in_array($country, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) {
|
|
|
|
$types[] = GATEWAY_TYPE_SOFORT;
|
|
|
|
} else {
|
|
|
|
$types[] = GATEWAY_TYPE_BANK_TRANSFER;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$types[] = GATEWAY_TYPE_BANK_TRANSFER;
|
|
|
|
$types[] = GATEWAY_TYPE_SOFORT;
|
|
|
|
}
|
|
|
|
} elseif ($achEnabled) {
|
2017-09-05 15:37:19 +02:00
|
|
|
$types[] = GATEWAY_TYPE_BANK_TRANSFER;
|
2017-09-06 10:12:34 +02:00
|
|
|
} elseif ($sofortEnabled) {
|
2017-09-05 20:53:52 +02:00
|
|
|
$types[] = GATEWAY_TYPE_SOFORT;
|
|
|
|
}
|
2017-10-19 15:49:15 +02:00
|
|
|
|
|
|
|
if ($gateway->getSepaEnabled()) {
|
|
|
|
$types[] = GATEWAY_TYPE_SEPA;
|
|
|
|
}
|
|
|
|
if ($gateway->getBitcoinEnabled()) {
|
|
|
|
$types[] = GATEWAY_TYPE_BITCOIN;
|
|
|
|
}
|
2017-09-05 15:37:19 +02:00
|
|
|
if ($gateway->getAlipayEnabled()) {
|
|
|
|
$types[] = GATEWAY_TYPE_ALIPAY;
|
|
|
|
}
|
2017-11-27 15:50:06 +01:00
|
|
|
if ($gateway->getApplePayEnabled()) {
|
|
|
|
$types[] = GATEWAY_TYPE_APPLE_PAY;
|
|
|
|
}
|
2017-09-04 12:14:58 +02:00
|
|
|
}
|
|
|
|
|
2016-06-20 16:14:43 +02:00
|
|
|
return $types;
|
|
|
|
}
|
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
/**
|
|
|
|
* Returns a setup intent that allows the user to enter card details without initiating a transaction.
|
|
|
|
*
|
|
|
|
* @return \Stripe\SetupIntent
|
|
|
|
*/
|
|
|
|
public function getSetupIntent()
|
|
|
|
{
|
|
|
|
$this->prepareStripeAPI();
|
|
|
|
return \Stripe\SetupIntent::create();
|
|
|
|
}
|
|
|
|
|
2016-06-20 16:14:43 +02:00
|
|
|
public function tokenize()
|
|
|
|
{
|
2018-03-07 16:23:25 +01:00
|
|
|
return $this->accountGateway->getPublishableKey();
|
2016-06-20 16:14:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function rules()
|
|
|
|
{
|
|
|
|
$rules = parent::rules();
|
|
|
|
|
2017-11-27 15:50:06 +01:00
|
|
|
if ($this->isGatewayType(GATEWAY_TYPE_APPLE_PAY)) {
|
|
|
|
return ['sourceToken' => 'required'];
|
|
|
|
}
|
|
|
|
|
2016-06-20 16:14:43 +02:00
|
|
|
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
|
|
|
|
$rules['authorize_ach'] = 'required';
|
|
|
|
}
|
|
|
|
|
|
|
|
return $rules;
|
|
|
|
}
|
|
|
|
|
2016-07-29 13:58:26 +02:00
|
|
|
public function isValid()
|
|
|
|
{
|
|
|
|
$result = $this->makeStripeCall(
|
|
|
|
'GET',
|
|
|
|
'charges',
|
|
|
|
'limit=1'
|
|
|
|
);
|
|
|
|
|
|
|
|
if (array_get($result, 'object') == 'list') {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-05 15:37:19 +02:00
|
|
|
public function shouldUseSource()
|
|
|
|
{
|
2017-10-23 20:47:24 +02:00
|
|
|
return in_array($this->gatewayType, [GATEWAY_TYPE_ALIPAY, GATEWAY_TYPE_SOFORT, GATEWAY_TYPE_BITCOIN]);
|
2017-09-05 15:37:19 +02:00
|
|
|
}
|
|
|
|
|
2016-06-20 16:14:43 +02:00
|
|
|
protected function checkCustomerExists($customer)
|
|
|
|
{
|
|
|
|
$response = $this->gateway()
|
|
|
|
->fetchCustomer(['customerReference' => $customer->token])
|
|
|
|
->send();
|
|
|
|
|
2017-01-30 17:05:31 +01:00
|
|
|
if (! $response->isSuccessful()) {
|
2016-06-20 16:14:43 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-05-26 17:24:00 +02:00
|
|
|
/*
|
2016-06-20 16:14:43 +02:00
|
|
|
$this->tokenResponse = $response->getData();
|
|
|
|
|
|
|
|
// import Stripe tokens created before payment methods table was added
|
2018-01-16 14:48:56 +01:00
|
|
|
if (! $customer->payment_methods->count()) {
|
2016-06-20 16:14:43 +02:00
|
|
|
if ($paymentMethod = $this->createPaymentMethod($customer)) {
|
|
|
|
$customer->default_payment_method_id = $paymentMethod->id;
|
|
|
|
$customer->save();
|
|
|
|
$customer->load('payment_methods');
|
|
|
|
}
|
|
|
|
}
|
2019-05-26 17:24:00 +02:00
|
|
|
*/
|
2016-06-20 16:14:43 +02:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function isTwoStep()
|
|
|
|
{
|
|
|
|
return $this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER) && empty($this->input['plaidPublicToken']);
|
|
|
|
}
|
|
|
|
|
2016-07-21 14:35:23 +02:00
|
|
|
protected function paymentDetails($paymentMethod = false)
|
2016-06-20 16:14:43 +02:00
|
|
|
{
|
|
|
|
$data = parent::paymentDetails($paymentMethod);
|
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
// Stripe complains if the email field is set
|
|
|
|
unset($data['email']);
|
|
|
|
|
|
|
|
if ( ! empty($this->input['paymentIntentID'])) {
|
|
|
|
// If we're completing a previously initiated payment intent, use that ID first.
|
|
|
|
$data['payment_intent'] = $this->input['paymentIntentID'];
|
|
|
|
unset($data['card']);
|
2016-06-26 12:45:50 +02:00
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
if ($paymentMethod) {
|
|
|
|
return $data;
|
|
|
|
}
|
2016-06-28 20:21:54 +02:00
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
if ( ! empty($this->input['paymentMethodID'])) {
|
|
|
|
// We're using an existing payment method.
|
|
|
|
$data['payment_method'] = $this->input['paymentMethodID'];
|
|
|
|
unset($data['card']);
|
|
|
|
} else if ( ! empty($this->input['sourceToken'])) {
|
2016-06-20 16:14:43 +02:00
|
|
|
$data['token'] = $this->input['sourceToken'];
|
|
|
|
unset($data['card']);
|
|
|
|
}
|
|
|
|
|
2017-01-30 17:05:31 +01:00
|
|
|
if (! empty($this->input['plaidPublicToken'])) {
|
2016-06-20 16:14:43 +02:00
|
|
|
$data['plaidPublicToken'] = $this->input['plaidPublicToken'];
|
|
|
|
$data['plaidAccountId'] = $this->input['plaidAccountId'];
|
|
|
|
unset($data['card']);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
/**
|
|
|
|
* @param bool $input
|
|
|
|
* @param bool $paymentMethod
|
|
|
|
* @param bool $offSession True if this payment is being made automatically rather than manually initiated by the user.
|
|
|
|
*
|
|
|
|
* @return bool|mixed
|
|
|
|
* @throws PaymentActionRequiredException When further interaction is required from the user.
|
|
|
|
*/
|
|
|
|
public function completeOnsitePurchase($input = false, $paymentMethod = false, $offSession = false)
|
2016-06-20 16:14:43 +02:00
|
|
|
{
|
2019-07-09 22:20:02 +02:00
|
|
|
$data = $this->prepareOnsitePurchase($input, $paymentMethod);
|
|
|
|
|
|
|
|
if ( ! $data && request()->capture) {
|
|
|
|
// We only want to save the payment details, not actually charge the card.
|
|
|
|
$real_data = $this->paymentDetails($paymentMethod);
|
|
|
|
|
|
|
|
if ( ! empty($real_data['payment_method'])) {
|
|
|
|
// Attach the payment method to the existing customer.
|
|
|
|
$this->prepareStripeAPI();
|
|
|
|
$payment_method = \Stripe\PaymentMethod::retrieve($real_data['payment_method']);
|
|
|
|
$payment_method = $payment_method->attach(['customer' => $this->getCustomerID()]);
|
|
|
|
$this->tokenResponse = $payment_method;
|
|
|
|
parent::createToken();
|
|
|
|
return $payment_method;
|
|
|
|
}
|
|
|
|
}
|
2016-06-20 16:14:43 +02:00
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
if ( ! $data) {
|
|
|
|
// No payment method to charge against yet; probably a 2-step or capture-only transaction.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( ! empty($data['payment_method']) || ! empty($data['payment_intent']) || ! empty($data['token'])) {
|
|
|
|
// Need to use Stripe's new Payment Intent API.
|
|
|
|
$this->prepareStripeAPI();
|
|
|
|
|
|
|
|
if ( ! empty($data['payment_intent'])) {
|
|
|
|
// Find the existing payment intent.
|
|
|
|
$intent = PaymentIntent::retrieve($data['payment_intent']);
|
|
|
|
|
|
|
|
if ( ! $intent->amount == $data['amount'] * 100) {
|
|
|
|
// Make sure that the provided payment intent matches the invoice amount.
|
|
|
|
throw new Exception('Incorrect PaymentIntent amount.');
|
|
|
|
}
|
|
|
|
$intent->confirm();
|
|
|
|
} elseif ( ! empty($data['token']) || ! empty($data['payment_method'])) {
|
|
|
|
$params = [
|
|
|
|
'amount' => $data['amount'] * 100,
|
|
|
|
'currency' => $data['currency'],
|
|
|
|
'confirmation_method' => 'manual',
|
|
|
|
'confirm' => true,
|
|
|
|
];
|
|
|
|
|
|
|
|
if ($offSession) {
|
|
|
|
$params['off_session'] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( ! empty($data['description'])) {
|
|
|
|
$params['description'] = $data['description'];
|
|
|
|
}
|
2016-06-20 16:14:43 +02:00
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
if ( ! empty($data['payment_method'])) {
|
|
|
|
$params['payment_method'] = $data['payment_method'];
|
|
|
|
|
|
|
|
if ($this->shouldCreateToken()) {
|
|
|
|
// Tell Stripe to save the payment method for future usage.
|
|
|
|
$params['setup_future_usage'] = 'off_session';
|
|
|
|
$params['save_payment_method'] = true;
|
|
|
|
$params['customer'] = $this->getCustomerID();
|
|
|
|
}
|
|
|
|
} elseif ( ! empty($data['token'])) {
|
|
|
|
// Use a stored payment method.
|
|
|
|
$params['payment_method'] = $data['token'];
|
|
|
|
$params['customer'] = $this->getCustomerID();
|
2019-07-09 22:32:00 +02:00
|
|
|
|
|
|
|
if (substr($data['token'], 0, 3) === 'ba_') {
|
|
|
|
// The PaymentIntent API doesn't seem to work with saved Bank Accounts.
|
|
|
|
// For now, just use the old API.
|
|
|
|
return $this->doOmnipayOnsitePurchase($data, $paymentMethod);
|
|
|
|
}
|
2019-07-09 22:20:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$intent = PaymentIntent::create($params);
|
|
|
|
}
|
2016-06-20 16:14:43 +02:00
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
if (empty($intent)) {
|
|
|
|
throw new \Exception('PaymentIntent not found.');
|
|
|
|
} elseif (($intent->status == 'requires_source_action' || $intent->status == 'requires_action') &&
|
|
|
|
$intent->next_action->type == 'use_stripe_sdk') {
|
|
|
|
// Throw an exception that can either be logged or be handled by getting further interaction from the user.
|
|
|
|
throw new PaymentActionRequiredException(['payment_intent' => $intent]);
|
|
|
|
} else if ($intent->status == 'succeeded') {
|
|
|
|
$ref = ! empty($intent->charges->data) ? $intent->charges->data[0]->id : null;
|
|
|
|
$payment = $this->createPayment($ref, $paymentMethod);
|
|
|
|
|
|
|
|
if ($this->invitation->invoice->account->isNinjaAccount()) {
|
|
|
|
Session::flash('trackEventCategory', '/account');
|
|
|
|
Session::flash('trackEventAction', '/buy_pro_plan');
|
|
|
|
Session::flash('trackEventAmount', $payment->amount);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($intent->setup_future_usage == 'off_session') {
|
|
|
|
// Save the payment method ID.
|
|
|
|
$payment_method = \Stripe\PaymentMethod::retrieve($intent->payment_method);
|
|
|
|
$this->tokenResponse = $payment_method;
|
|
|
|
parent::createToken();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $payment;
|
|
|
|
} else {
|
|
|
|
throw new Exception('Invalid PaymentIntent status: ' . $intent->status);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return $this->doOmnipayOnsitePurchase($data, $paymentMethod);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getCustomerID()
|
|
|
|
{
|
2017-07-28 15:04:05 +02:00
|
|
|
// if a customer already exists link the token to it
|
|
|
|
if ($customer = $this->customer()) {
|
2019-07-09 22:20:02 +02:00
|
|
|
return $customer->token;
|
2017-07-28 15:04:05 +02:00
|
|
|
} else {
|
2019-07-09 22:20:02 +02:00
|
|
|
// otherwise create a new czustomer
|
|
|
|
$invoice = $this->invitation->invoice;
|
|
|
|
$client = $invoice->client;
|
|
|
|
|
2017-07-28 15:04:05 +02:00
|
|
|
$response = $this->gateway()->createCustomer([
|
|
|
|
'description' => $client->getDisplayName(),
|
2019-07-09 22:20:02 +02:00
|
|
|
'email' => $this->contact()->email,
|
2017-07-28 15:04:05 +02:00
|
|
|
])->send();
|
2019-07-09 22:20:02 +02:00
|
|
|
return $response->getCustomerReference();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function createToken()
|
|
|
|
{
|
|
|
|
$invoice = $this->invitation->invoice;
|
|
|
|
$client = $invoice->client;
|
|
|
|
|
|
|
|
$data = $this->paymentDetails();
|
|
|
|
|
|
|
|
if ( ! empty($data['payment_method']) || ! empty($data['payment_intent'])) {
|
|
|
|
// Using the PaymentIntent API; we'll save the details later.
|
|
|
|
return null;
|
2017-07-28 15:04:05 +02:00
|
|
|
}
|
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
$data['description'] = $client->getDisplayName();
|
|
|
|
$data['customerReference'] = $this->getCustomerID();
|
|
|
|
|
2017-01-30 17:05:31 +01:00
|
|
|
if (! empty($data['plaidPublicToken'])) {
|
2016-06-20 16:14:43 +02:00
|
|
|
$plaidResult = $this->getPlaidToken($data['plaidPublicToken'], $data['plaidAccountId']);
|
|
|
|
unset($data['plaidPublicToken']);
|
|
|
|
unset($data['plaidAccountId']);
|
|
|
|
$data['token'] = $plaidResult['stripe_bank_account_token'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$tokenResponse = $this->gateway()
|
|
|
|
->createCard($data)
|
|
|
|
->send();
|
|
|
|
|
|
|
|
if ($tokenResponse->isSuccessful()) {
|
|
|
|
$this->tokenResponse = $tokenResponse->getData();
|
|
|
|
|
|
|
|
return parent::createToken();
|
|
|
|
} else {
|
|
|
|
throw new Exception($tokenResponse->getMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function creatingCustomer($customer)
|
|
|
|
{
|
2017-07-28 15:04:05 +02:00
|
|
|
if (isset($this->tokenResponse['customer'])) {
|
|
|
|
$customer->token = $this->tokenResponse['customer'];
|
|
|
|
} else {
|
|
|
|
$customer->token = $this->tokenResponse['id'];
|
|
|
|
}
|
2016-06-20 16:14:43 +02:00
|
|
|
|
|
|
|
return $customer;
|
|
|
|
}
|
|
|
|
|
2016-07-21 14:35:23 +02:00
|
|
|
protected function creatingPaymentMethod($paymentMethod)
|
2016-06-20 16:14:43 +02:00
|
|
|
{
|
|
|
|
$data = $this->tokenResponse;
|
2016-09-28 20:07:43 +02:00
|
|
|
$source = false;
|
2016-06-20 16:14:43 +02:00
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) {
|
2016-06-20 16:14:43 +02:00
|
|
|
$source = $data;
|
2017-01-30 20:40:43 +01:00
|
|
|
} elseif (! empty($data['object']) && $data['object'] == 'customer') {
|
|
|
|
$sources = ! empty($data['sources']) ? $data['sources'] : $data['cards'];
|
2016-06-20 16:14:43 +02:00
|
|
|
$source = reset($sources['data']);
|
2017-01-30 20:40:43 +01:00
|
|
|
} elseif (! empty($data['source'])) {
|
2016-09-28 20:07:43 +02:00
|
|
|
$source = $data['source'];
|
2017-01-30 20:40:43 +01:00
|
|
|
} elseif (! empty($data['card'])) {
|
2016-09-28 20:07:43 +02:00
|
|
|
$source = $data['card'];
|
2016-06-20 16:14:43 +02:00
|
|
|
}
|
|
|
|
|
2017-01-30 17:05:31 +01:00
|
|
|
if (! $source) {
|
2016-06-20 16:14:43 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-07-09 22:20:02 +02:00
|
|
|
if ( ! empty($source['id'])) {
|
|
|
|
$paymentMethod->source_reference = $source['id'];
|
|
|
|
} elseif ( ! empty($data['id'])) {
|
|
|
|
// Find an ID on the payment method instead of the card.
|
|
|
|
$paymentMethod->source_reference = $data['id'];
|
|
|
|
}
|
2016-06-20 16:14:43 +02:00
|
|
|
$paymentMethod->last4 = $source['last4'];
|
|
|
|
|
2016-07-27 11:53:11 +02:00
|
|
|
// For older users the Stripe account may just have the customer token but not the card version
|
|
|
|
// In that case we'd use GATEWAY_TYPE_TOKEN even though we're creating the credit card
|
2017-11-27 15:50:06 +01:00
|
|
|
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)
|
|
|
|
|| $this->isGatewayType(GATEWAY_TYPE_APPLE_PAY)
|
|
|
|
|| $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
|
2019-01-30 11:45:46 +01:00
|
|
|
if (isset($source['exp_year']) && isset($source['exp_month'])) {
|
|
|
|
$paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01';
|
|
|
|
}
|
|
|
|
if (isset($source['brand'])) {
|
|
|
|
$paymentMethod->payment_type_id = PaymentType::parseCardType($source['brand']);
|
|
|
|
}
|
2016-06-20 16:14:43 +02:00
|
|
|
} elseif ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
|
|
|
|
$paymentMethod->routing_number = $source['routing_number'];
|
|
|
|
$paymentMethod->payment_type_id = PAYMENT_TYPE_ACH;
|
|
|
|
$paymentMethod->status = $source['status'];
|
|
|
|
$currency = Cache::get('currencies')->where('code', strtoupper($source['currency']))->first();
|
|
|
|
|
|
|
|
if ($currency) {
|
|
|
|
$paymentMethod->currency_id = $currency->id;
|
|
|
|
$paymentMethod->setRelation('currency', $currency);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $paymentMethod;
|
|
|
|
}
|
|
|
|
|
2016-07-21 14:35:23 +02:00
|
|
|
protected function creatingPayment($payment, $paymentMethod)
|
2016-06-20 16:14:43 +02:00
|
|
|
{
|
2017-09-04 22:05:26 +02:00
|
|
|
$isBank = $this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER, $paymentMethod);
|
|
|
|
$isAlipay = $this->isGatewayType(GATEWAY_TYPE_ALIPAY, $paymentMethod);
|
2017-09-05 20:53:52 +02:00
|
|
|
$isSofort = $this->isGatewayType(GATEWAY_TYPE_SOFORT, $paymentMethod);
|
2017-10-23 22:01:03 +02:00
|
|
|
$isBitcoin = $this->isGatewayType(GATEWAY_TYPE_BITCOIN, $paymentMethod);
|
2017-09-04 22:05:26 +02:00
|
|
|
|
2017-10-23 22:01:03 +02:00
|
|
|
if ($isBank || $isAlipay || $isSofort || $isBitcoin) {
|
2016-07-21 14:35:23 +02:00
|
|
|
$payment->payment_status_id = $this->purchaseResponse['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING;
|
2017-09-04 22:05:26 +02:00
|
|
|
if ($isAlipay) {
|
|
|
|
$payment->payment_type_id = PAYMENT_TYPE_ALIPAY;
|
2017-09-05 20:53:52 +02:00
|
|
|
} elseif ($isSofort) {
|
2017-09-05 17:17:58 +02:00
|
|
|
$payment->payment_type_id = PAYMENT_TYPE_SOFORT;
|
2017-10-23 22:01:03 +02:00
|
|
|
} elseif ($isBitcoin) {
|
|
|
|
$payment->payment_type_id = PAYMENT_TYPE_BITCOIN;
|
2017-09-04 22:05:26 +02:00
|
|
|
}
|
2019-07-18 08:26:08 +02:00
|
|
|
} else if (! $paymentMethod && $this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD) && ! strcmp($this->purchaseResponse['payment_method_details']['type'], "card")) {
|
|
|
|
$card = $this->purchaseResponse['payment_method_details']['card'];
|
|
|
|
$payment->last4 = $card['last4'];
|
|
|
|
$payment->expiration = $card['exp_year'] . '-' . $card['exp_month'] . '-01';
|
|
|
|
$payment->payment_type_id = PaymentType::parseCardType($card['brand']);
|
2016-06-20 16:14:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $payment;
|
|
|
|
}
|
|
|
|
|
2016-07-21 14:35:23 +02:00
|
|
|
public function removePaymentMethod($paymentMethod)
|
2016-06-20 16:14:43 +02:00
|
|
|
{
|
2016-06-24 14:40:10 +02:00
|
|
|
parent::removePaymentMethod($paymentMethod);
|
|
|
|
|
2017-01-30 17:05:31 +01:00
|
|
|
if (! $paymentMethod->relationLoaded('account_gateway_token')) {
|
2016-06-20 16:14:43 +02:00
|
|
|
$paymentMethod->load('account_gateway_token');
|
|
|
|
}
|
|
|
|
|
|
|
|
$response = $this->gateway()->deleteCard([
|
|
|
|
'customerReference' => $paymentMethod->account_gateway_token->token,
|
2017-01-30 20:40:43 +01:00
|
|
|
'cardReference' => $paymentMethod->source_reference,
|
2016-06-20 16:14:43 +02:00
|
|
|
])->send();
|
|
|
|
|
|
|
|
if ($response->isSuccessful()) {
|
2016-06-24 14:40:10 +02:00
|
|
|
return true;
|
2016-06-20 16:14:43 +02:00
|
|
|
} else {
|
|
|
|
throw new Exception($response->getMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getPlaidToken($publicToken, $accountId)
|
|
|
|
{
|
|
|
|
$clientId = $this->accountGateway->getPlaidClientId();
|
|
|
|
$secret = $this->accountGateway->getPlaidSecret();
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $clientId) {
|
2016-06-20 16:14:43 +02:00
|
|
|
throw new Exception('plaid client id not set'); // TODO use text strings
|
|
|
|
}
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $secret) {
|
2016-06-20 16:14:43 +02:00
|
|
|
throw new Exception('plaid secret not set');
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$subdomain = $this->accountGateway->getPlaidEnvironment() == 'production' ? 'api' : 'tartan';
|
2017-01-30 20:40:43 +01:00
|
|
|
$response = (new \GuzzleHttp\Client(['base_uri' => "https://{$subdomain}.plaid.com"]))->request(
|
2016-06-20 16:14:43 +02:00
|
|
|
'POST',
|
|
|
|
'exchange_token',
|
|
|
|
[
|
|
|
|
'allow_redirects' => false,
|
2017-01-30 20:40:43 +01:00
|
|
|
'headers' => ['content-type' => 'application/x-www-form-urlencoded'],
|
2016-07-03 18:11:58 +02:00
|
|
|
'body' => http_build_query([
|
2016-06-20 16:14:43 +02:00
|
|
|
'client_id' => $clientId,
|
|
|
|
'secret' => $secret,
|
|
|
|
'public_token' => $publicToken,
|
|
|
|
'account_id' => $accountId,
|
2017-01-30 20:40:43 +01:00
|
|
|
]),
|
2016-06-20 16:14:43 +02:00
|
|
|
]
|
|
|
|
);
|
2017-01-30 20:40:43 +01:00
|
|
|
|
2016-06-20 16:14:43 +02:00
|
|
|
return json_decode($response->getBody(), true);
|
|
|
|
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
|
|
|
$response = $e->getResponse();
|
|
|
|
$body = json_decode($response->getBody(), true);
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if ($body && ! empty($body['message'])) {
|
2016-06-20 16:14:43 +02:00
|
|
|
throw new Exception($body['message']);
|
|
|
|
} else {
|
|
|
|
throw new Exception($e->getMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function verifyBankAccount($client, $publicId, $amount1, $amount2)
|
|
|
|
{
|
|
|
|
$customer = $this->customer($client->id);
|
|
|
|
$paymentMethod = PaymentMethod::clientId($client->id)
|
|
|
|
->wherePublicId($publicId)
|
|
|
|
->firstOrFail();
|
|
|
|
|
|
|
|
// Omnipay doesn't support verifying payment methods
|
|
|
|
// Also, it doesn't want to urlencode without putting numbers inside the brackets
|
|
|
|
$result = $this->makeStripeCall(
|
|
|
|
'POST',
|
|
|
|
'customers/' . $customer->token . '/sources/' . $paymentMethod->source_reference . '/verify',
|
|
|
|
'amounts[]=' . intval($amount1) . '&amounts[]=' . intval($amount2)
|
|
|
|
);
|
|
|
|
|
2017-06-01 20:36:26 +02:00
|
|
|
if (is_string($result) && $result != 'This bank account has already been verified.') {
|
2016-06-20 16:14:43 +02:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
$paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED;
|
|
|
|
$paymentMethod->save();
|
|
|
|
|
2017-01-30 17:05:31 +01:00
|
|
|
if (! $customer->default_payment_method_id) {
|
2016-06-20 16:14:43 +02:00
|
|
|
$customer->default_payment_method_id = $paymentMethod->id;
|
|
|
|
$customer->save();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-09-04 12:14:58 +02:00
|
|
|
public function createSource()
|
|
|
|
{
|
|
|
|
$amount = intval($this->invoice()->getRequestedAmount() * 100);
|
2017-09-05 16:47:28 +02:00
|
|
|
$invoiceNumber = $this->invoice()->invoice_number;
|
2017-09-04 12:14:58 +02:00
|
|
|
$currency = $this->client()->getCurrencyCode();
|
2017-10-23 20:47:24 +02:00
|
|
|
$email = $this->contact()->email;
|
2017-09-04 12:14:58 +02:00
|
|
|
$gatewayType = GatewayType::getAliasFromId($this->gatewayType);
|
|
|
|
$redirect = url("/complete_source/{$this->invitation->invitation_key}/{$gatewayType}");
|
2017-09-05 16:47:28 +02:00
|
|
|
$country = $this->client()->country ? $this->client()->country->iso_3166_2 : ($this->account()->country ? $this->account()->country->iso_3166_2 : '');
|
|
|
|
$extra = '';
|
2017-09-04 12:14:58 +02:00
|
|
|
|
2017-09-05 16:47:28 +02:00
|
|
|
if ($this->gatewayType == GATEWAY_TYPE_ALIPAY) {
|
|
|
|
if (! $this->accountGateway->getAlipayEnabled()) {
|
|
|
|
throw new Exception('Alipay is not enabled');
|
|
|
|
}
|
|
|
|
$type = 'alipay';
|
2017-10-23 20:47:24 +02:00
|
|
|
} elseif ($this->gatewayType == GATEWAY_TYPE_BITCOIN) {
|
|
|
|
if (! $this->accountGateway->getBitcoinEnabled()) {
|
|
|
|
throw new Exception('Bitcoin is not enabled');
|
|
|
|
}
|
|
|
|
$type = 'bitcoin';
|
|
|
|
$extra = "&owner[email]={$email}";
|
2017-09-05 16:47:28 +02:00
|
|
|
} else {
|
|
|
|
if (! $this->accountGateway->getSofortEnabled()) {
|
|
|
|
throw new Exception('Sofort is not enabled');
|
|
|
|
}
|
|
|
|
$type = 'sofort';
|
|
|
|
$extra = "&sofort[country]={$country}&statement_descriptor={$invoiceNumber}";
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = "type={$type}&amount={$amount}¤cy={$currency}&redirect[return_url]={$redirect}{$extra}";
|
2017-09-04 12:14:58 +02:00
|
|
|
$response = $this->makeStripeCall('POST', 'sources', $data);
|
|
|
|
|
2017-09-05 12:51:08 +02:00
|
|
|
if (is_array($response) && isset($response['id'])) {
|
|
|
|
$this->invitation->transaction_reference = $response['id'];
|
|
|
|
$this->invitation->save();
|
2017-09-04 12:14:58 +02:00
|
|
|
|
2017-10-23 20:47:24 +02:00
|
|
|
if ($this->gatewayType == GATEWAY_TYPE_BITCOIN) {
|
|
|
|
return view('payments/stripe/bitcoin', [
|
|
|
|
'client' => $this->client(),
|
|
|
|
'account' => $this->account(),
|
|
|
|
'invitation' => $this->invitation,
|
|
|
|
'invoiceNumber' => $invoiceNumber,
|
2017-12-14 12:10:05 +01:00
|
|
|
'amount' => $this->invoice()->getRequestedAmount(),
|
2017-10-23 20:47:24 +02:00
|
|
|
'source' => $response,
|
|
|
|
]);
|
|
|
|
} else {
|
|
|
|
return redirect($response['redirect']['url']);
|
|
|
|
}
|
2017-09-05 12:51:08 +02:00
|
|
|
} else {
|
|
|
|
throw new Exception($response);
|
|
|
|
}
|
2017-09-04 12:14:58 +02:00
|
|
|
}
|
|
|
|
|
2016-06-20 16:14:43 +02:00
|
|
|
public function makeStripeCall($method, $url, $body = null)
|
|
|
|
{
|
|
|
|
$apiKey = $this->accountGateway->getConfig()->apiKey;
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $apiKey) {
|
2016-06-20 16:14:43 +02:00
|
|
|
return 'No API key set';
|
|
|
|
}
|
|
|
|
|
2017-01-30 17:05:31 +01:00
|
|
|
try {
|
2016-06-20 16:14:43 +02:00
|
|
|
$options = [
|
2017-01-30 20:40:43 +01:00
|
|
|
'headers' => ['content-type' => 'application/x-www-form-urlencoded'],
|
2016-06-20 16:14:43 +02:00
|
|
|
'auth' => [$apiKey, ''],
|
|
|
|
];
|
|
|
|
|
|
|
|
if ($body) {
|
|
|
|
$options['body'] = $body;
|
|
|
|
}
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
$response = (new \GuzzleHttp\Client(['base_uri' => 'https://api.stripe.com/v1/']))->request(
|
2016-06-20 16:14:43 +02:00
|
|
|
$method,
|
|
|
|
$url,
|
|
|
|
$options
|
|
|
|
);
|
2017-01-30 20:40:43 +01:00
|
|
|
|
2016-06-20 16:14:43 +02:00
|
|
|
return json_decode($response->getBody(), true);
|
|
|
|
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
|
|
|
|
$response = $e->getResponse();
|
|
|
|
|
|
|
|
$body = json_decode($response->getBody(), true);
|
|
|
|
if ($body && $body['error'] && $body['error']['type'] == 'invalid_request_error') {
|
|
|
|
return $body['error']['message'];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $e->getMessage();
|
|
|
|
}
|
|
|
|
}
|
2016-06-23 15:15:52 +02:00
|
|
|
|
2016-06-23 15:27:54 +02:00
|
|
|
public function handleWebHook($input)
|
2016-06-23 15:15:52 +02:00
|
|
|
{
|
|
|
|
$eventId = array_get($input, 'id');
|
2017-01-30 20:40:43 +01:00
|
|
|
$eventType = array_get($input, 'type');
|
2016-06-23 15:15:52 +02:00
|
|
|
|
|
|
|
$accountGateway = $this->accountGateway;
|
|
|
|
$accountId = $accountGateway->account_id;
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $eventId) {
|
2016-06-23 15:15:52 +02:00
|
|
|
throw new Exception('Missing event id');
|
|
|
|
}
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $eventType) {
|
2016-06-23 15:15:52 +02:00
|
|
|
throw new Exception('Missing event type');
|
|
|
|
}
|
|
|
|
|
2016-07-03 18:11:58 +02:00
|
|
|
$supportedEvents = [
|
2016-06-23 15:15:52 +02:00
|
|
|
'charge.failed',
|
|
|
|
'charge.succeeded',
|
2016-06-24 14:40:10 +02:00
|
|
|
'charge.refunded',
|
2016-06-23 15:15:52 +02:00
|
|
|
'customer.source.updated',
|
|
|
|
'customer.source.deleted',
|
2016-06-24 14:40:10 +02:00
|
|
|
'customer.bank_account.deleted',
|
2017-09-04 12:50:09 +02:00
|
|
|
'source.chargeable',
|
2016-07-03 18:11:58 +02:00
|
|
|
];
|
2016-06-23 15:15:52 +02:00
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! in_array($eventType, $supportedEvents)) {
|
2016-07-03 18:11:58 +02:00
|
|
|
return ['message' => 'Ignoring event'];
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch the event directly from Stripe for security
|
|
|
|
$eventDetails = $this->makeStripeCall('GET', 'events/'.$eventId);
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (is_string($eventDetails) || ! $eventDetails) {
|
2016-08-13 21:19:37 +02:00
|
|
|
return false;
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($eventType != $eventDetails['type']) {
|
2016-08-13 21:19:37 +02:00
|
|
|
return false;
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $eventDetails['pending_webhooks']) {
|
2016-08-13 21:19:37 +02:00
|
|
|
return false;
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
|
|
|
|
2017-09-04 20:36:12 +02:00
|
|
|
$source = $eventDetails['data']['object'];
|
|
|
|
$sourceRef = $source['id'];
|
2016-06-23 15:15:52 +02:00
|
|
|
|
2017-09-04 20:36:12 +02:00
|
|
|
if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded' || $eventType == 'charge.refunded') {
|
|
|
|
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $sourceRef)->first();
|
2016-06-23 15:15:52 +02:00
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $payment) {
|
2016-08-13 21:19:37 +02:00
|
|
|
return false;
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
|
|
|
|
2017-10-19 11:11:39 +02:00
|
|
|
if ($payment->is_deleted || $payment->invoice->is_deleted) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-06-23 15:15:52 +02:00
|
|
|
if ($eventType == 'charge.failed') {
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $payment->isFailed()) {
|
2017-09-04 20:36:12 +02:00
|
|
|
$payment->markFailed($source['failure_message']);
|
2016-06-23 15:15:52 +02:00
|
|
|
|
|
|
|
$userMailer = app('App\Ninja\Mailers\UserMailer');
|
|
|
|
$userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment);
|
|
|
|
}
|
|
|
|
} elseif ($eventType == 'charge.succeeded') {
|
|
|
|
$payment->markComplete();
|
|
|
|
} elseif ($eventType == 'charge.refunded') {
|
2017-09-04 20:36:12 +02:00
|
|
|
$payment->recordRefund($source['amount_refunded'] / 100 - $payment->refunded);
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
2017-01-30 17:05:31 +01:00
|
|
|
} elseif ($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted' || $eventType == 'customer.bank_account.deleted') {
|
2016-06-23 15:15:52 +02:00
|
|
|
$paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first();
|
|
|
|
|
2017-01-30 20:40:43 +01:00
|
|
|
if (! $paymentMethod) {
|
2016-09-12 07:51:41 +02:00
|
|
|
return false;
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
|
|
|
|
2016-06-24 14:40:10 +02:00
|
|
|
if ($eventType == 'customer.source.deleted' || $eventType == 'customer.bank_account.deleted') {
|
2016-06-23 15:15:52 +02:00
|
|
|
$paymentMethod->delete();
|
|
|
|
} elseif ($eventType == 'customer.source.updated') {
|
2016-06-28 20:21:54 +02:00
|
|
|
//$this->paymentService->convertPaymentMethodFromStripe($source, null, $paymentMethod)->save();
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
2017-09-04 12:50:09 +02:00
|
|
|
} elseif ($eventType == 'source.chargeable') {
|
2017-09-04 20:36:12 +02:00
|
|
|
$this->invitation = Invitation::scope(false, $accountId)->where('transaction_reference', '=', $sourceRef)->first();
|
|
|
|
if (! $this->invitation) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
$data = sprintf('amount=%d¤cy=%s&source=%s', $source['amount'], $source['currency'], $source['id']);
|
2017-09-05 12:51:08 +02:00
|
|
|
$this->purchaseResponse = $response = $this->makeStripeCall('POST', 'charges', $data);
|
2017-09-06 10:45:50 +02:00
|
|
|
$this->gatewayType = GatewayType::getIdFromAlias($source['type']);
|
2017-09-05 12:51:08 +02:00
|
|
|
if (is_array($response) && isset($response['id'])) {
|
|
|
|
$this->createPayment($response['id']);
|
|
|
|
}
|
2016-06-23 15:15:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return 'Processed successfully';
|
|
|
|
}
|
2016-06-20 16:14:43 +02:00
|
|
|
}
|