1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-14 15:13:29 +01:00
invoiceninja/app/Ninja/PaymentDrivers/BasePaymentDriver.php

1044 lines
36 KiB
PHP
Raw Normal View History

2017-01-30 20:40:43 +01:00
<?php
namespace App\Ninja\PaymentDrivers;
2016-06-20 16:14:43 +02:00
2016-06-28 20:21:54 +02:00
use App\Models\Account;
2017-01-30 20:40:43 +01:00
use App\Models\AccountGatewaySettings;
use App\Models\AccountGatewayToken;
2016-06-20 16:14:43 +02:00
use App\Models\Country;
2016-09-09 23:19:32 +02:00
use App\Models\GatewayType;
2017-01-30 20:40:43 +01:00
use App\Models\License;
use App\Models\Payment;
use App\Models\PaymentMethod;
2017-06-26 21:23:21 +02:00
use Omnipay\Common\Item;
2017-01-30 20:40:43 +01:00
use CreditCard;
use DateTime;
use Exception;
use Omnipay;
use Request;
use Session;
use URL;
use Utils;
2016-06-20 16:14:43 +02:00
class BasePaymentDriver
{
public $invitation;
public $accountGateway;
protected $gatewayType;
protected $gateway;
protected $customer;
protected $sourceId;
protected $input;
protected $customerResponse;
protected $tokenResponse;
protected $purchaseResponse;
2016-06-21 18:10:22 +02:00
protected $sourceReferenceParam = 'token';
2016-06-20 16:14:43 +02:00
protected $customerReferenceParam;
protected $transactionReferenceParam;
public $canRefundPayments = false;
2016-07-21 14:35:23 +02:00
public function __construct($accountGateway = false, $invitation = false, $gatewayType = false)
2016-06-20 16:14:43 +02:00
{
$this->accountGateway = $accountGateway;
$this->invitation = $invitation;
$this->gatewayType = $gatewayType ?: $this->gatewayTypes()[0];
}
public function isGateway($gatewayId)
{
return $this->accountGateway->gateway_id == $gatewayId;
}
public function isValid()
{
return true;
}
2016-06-23 19:17:02 +02:00
// optionally pass a paymentMethod to determine the type from the token
protected function isGatewayType($gatewayType, $paymentMethod = false)
2016-06-20 16:14:43 +02:00
{
2016-06-23 19:17:02 +02:00
if ($paymentMethod) {
return $paymentMethod->gatewayType() == $gatewayType;
} else {
return $this->gatewayType === $gatewayType;
}
2016-06-20 16:14:43 +02:00
}
2016-06-22 20:42:09 +02:00
public function gatewayTypes()
2016-06-20 16:14:43 +02:00
{
return [
2017-01-30 20:40:43 +01:00
GATEWAY_TYPE_CREDIT_CARD,
2016-06-20 16:14:43 +02:00
];
}
public function handles($type)
{
return in_array($type, $this->gatewayTypes());
}
2016-07-21 14:35:23 +02:00
// when set to true we won't pass the card details with the form
2016-06-20 16:14:43 +02:00
public function tokenize()
{
return false;
}
2016-07-21 14:35:23 +02:00
// set payment method as pending until confirmed
2016-06-20 16:14:43 +02:00
public function isTwoStep()
{
return false;
}
public function providerName()
{
return strtolower($this->accountGateway->gateway->provider);
}
protected function invoice()
{
return $this->invitation->invoice;
}
protected function contact()
{
return $this->invitation->contact;
}
protected function client()
{
return $this->invoice()->client;
}
protected function account()
{
return $this->client()->account;
}
2016-07-21 14:35:23 +02:00
public function startPurchase($input = false, $sourceId = false)
2016-06-20 16:14:43 +02:00
{
$this->input = $input;
$this->sourceId = $sourceId;
Session::put('invitation_key', $this->invitation->invitation_key);
Session::put($this->invitation->id . 'gateway_type', $this->gatewayType);
Session::put($this->invitation->id . 'payment_ref', $this->invoice()->id . '_' . uniqid());
$gateway = $this->accountGateway->gateway;
2017-01-30 17:05:31 +01:00
if (! $this->meetsGatewayTypeLimits($this->gatewayType)) {
// The customer must have hacked the URL
Session::flash('error', trans('texts.limits_not_met'));
2017-01-30 20:40:43 +01:00
return redirect()->to('view/' . $this->invitation->invitation_key);
}
2017-03-16 18:34:14 +01:00
if (! $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
// apply gateway fees
$invoicRepo = app('App\Ninja\Repositories\InvoiceRepository');
$invoicRepo->setGatewayFee($this->invoice(), $this->gatewayType);
}
2017-03-15 16:09:23 +01:00
2017-09-05 10:36:37 +02:00
// For these gateway types we use the API directrly rather than Omnipay
2017-09-05 15:37:19 +02:00
if ($this->shouldUseSource()) {
2017-09-05 10:36:37 +02:00
return $this->createSource();
}
2016-06-20 16:14:43 +02:00
if ($this->isGatewayType(GATEWAY_TYPE_TOKEN) || $gateway->is_offsite) {
if (Session::has('error')) {
Session::reflash();
} else {
$this->completeOnsitePurchase();
2016-12-27 22:56:55 +01:00
if ($redirectUrl = session('redirect_url:' . $this->invitation->invitation_key)) {
$separator = strpos($redirectUrl, '?') === false ? '?' : '&';
2017-01-30 20:40:43 +01:00
2016-12-27 22:56:55 +01:00
return redirect()->to($redirectUrl . $separator . 'invoice_id=' . $this->invoice()->public_id);
} else {
Session::flash('message', trans('texts.applied_payment'));
}
2016-06-20 16:14:43 +02:00
}
return redirect()->to('view/' . $this->invitation->invitation_key);
}
2017-06-21 22:36:59 +02:00
$url = 'payment/' . $this->invitation->invitation_key;
if (request()->update) {
$url .= '?update=true';
}
2016-06-20 16:14:43 +02:00
$data = [
2016-06-21 18:10:22 +02:00
'details' => ! empty($input['details']) ? json_decode($input['details']) : false,
2016-06-20 16:14:43 +02:00
'accountGateway' => $this->accountGateway,
'acceptedCreditCardTypes' => $this->accountGateway->getCreditcardTypes(),
'gateway' => $gateway,
'showBreadcrumbs' => false,
2017-06-21 22:36:59 +02:00
'url' => $url,
2016-06-20 16:14:43 +02:00
'amount' => $this->invoice()->getRequestedAmount(),
'invoiceNumber' => $this->invoice()->invoice_number,
'client' => $this->client(),
'contact' => $this->invitation->contact,
'invitation' => $this->invitation,
2016-06-20 16:14:43 +02:00
'gatewayType' => $this->gatewayType,
'currencyId' => $this->client()->getCurrencyId(),
'currencyCode' => $this->client()->getCurrencyCode(),
'account' => $this->account(),
'sourceId' => $sourceId,
'tokenize' => $this->tokenize(),
'transactionToken' => $this->createTransactionToken(),
];
return view($this->paymentView(), $data);
}
2016-07-21 14:35:23 +02:00
// check if a custom view exists for this provider
2016-06-20 16:14:43 +02:00
protected function paymentView()
{
2016-09-09 23:19:32 +02:00
$gatewayTypeAlias = GatewayType::getAliasFromId($this->gatewayType);
$file = sprintf('%s/views/payments/%s/%s.blade.php', resource_path(), $this->providerName(), $gatewayTypeAlias);
2016-06-20 16:14:43 +02:00
if (file_exists($file)) {
2016-09-09 23:19:32 +02:00
return sprintf('payments.%s/%s', $this->providerName(), $gatewayTypeAlias);
2016-06-20 16:14:43 +02:00
} else {
2016-09-09 23:19:32 +02:00
return sprintf('payments.%s', $gatewayTypeAlias);
2016-06-20 16:14:43 +02:00
}
}
2016-07-21 14:35:23 +02:00
// check if a custom partial exists for this provider
2016-06-20 16:14:43 +02:00
public function partialView()
{
$file = sprintf('%s/views/payments/%s/partial.blade.php', resource_path(), $this->providerName());
if (file_exists($file)) {
return sprintf('payments.%s.partial', $this->providerName());
} else {
return false;
}
}
public function rules()
{
$rules = [];
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) {
$rules = array_merge($rules, [
'first_name' => 'required',
'last_name' => 'required',
]);
// TODO check this is always true
2017-01-30 17:05:31 +01:00
if (! $this->tokenize()) {
2016-06-20 16:14:43 +02:00
$rules = array_merge($rules, [
'card_number' => 'required',
'expiration_month' => 'required',
'expiration_year' => 'required',
'cvv' => 'required',
]);
}
if ($this->accountGateway->show_address) {
$rules = array_merge($rules, [
'address1' => 'required',
'city' => 'required',
'state' => 'required',
'postal_code' => 'required',
'country_id' => 'required',
]);
}
}
return $rules;
}
protected function gateway()
{
if ($this->gateway) {
return $this->gateway;
}
$this->gateway = Omnipay::create($this->accountGateway->gateway->provider);
$this->gateway->initialize((array) $this->accountGateway->getConfig());
return $this->gateway;
}
2016-07-21 14:35:23 +02:00
public function completeOnsitePurchase($input = false, $paymentMethod = false)
2016-06-20 16:14:43 +02:00
{
$this->input = count($input) ? $input : false;
$gateway = $this->gateway();
if ($input) {
2016-07-12 22:46:41 +02:00
$this->updateClient();
2016-06-20 16:14:43 +02:00
}
// load or create token
if ($this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
2017-01-30 17:05:31 +01:00
if (! $paymentMethod) {
2016-06-20 16:14:43 +02:00
$paymentMethod = PaymentMethod::clientId($this->client()->id)
->wherePublicId($this->sourceId)
->firstOrFail();
}
2017-03-16 18:34:14 +01:00
$invoicRepo = app('App\Ninja\Repositories\InvoiceRepository');
$invoicRepo->setGatewayFee($this->invoice(), $paymentMethod->payment_type->gateway_type_id);
2017-01-30 17:05:31 +01:00
if (! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) {
// The customer must have hacked the URL
Session::flash('error', trans('texts.limits_not_met'));
2017-01-30 20:40:43 +01:00
return redirect()->to('view/' . $this->invitation->invitation_key);
}
} else {
if ($this->shouldCreateToken()) {
$paymentMethod = $this->createToken();
}
2017-01-30 17:05:31 +01:00
if (! $this->meetsGatewayTypeLimits($this->gatewayType)) {
// The customer must have hacked the URL
Session::flash('error', trans('texts.limits_not_met'));
2017-01-30 20:40:43 +01:00
return redirect()->to('view/' . $this->invitation->invitation_key);
}
2016-06-20 16:14:43 +02:00
}
2017-06-21 22:36:59 +02:00
if ($this->isTwoStep() || request()->update) {
2016-06-20 16:14:43 +02:00
return;
}
// prepare and process payment
$data = $this->paymentDetails($paymentMethod);
2017-12-20 14:15:30 +01:00
// TODO move to payment driver class
if ($this->isGateway(GATEWAY_SAGE_PAY_DIRECT) || $this->isGateway(GATEWAY_SAGE_PAY_SERVER)) {
$items = null;
} else {
$items = $this->paymentItems();
}
2017-06-26 21:23:21 +02:00
$response = $gateway->purchase($data)
->setItems($items)
->send();
2016-06-20 16:14:43 +02:00
$this->purchaseResponse = (array) $response->getData();
// parse the transaction reference
if ($this->transactionReferenceParam) {
2017-10-18 19:14:35 +02:00
if (! empty($this->purchaseResponse[$this->transactionReferenceParam])) {
2017-10-18 19:13:57 +02:00
$ref = $this->purchaseResponse[$this->transactionReferenceParam];
} else {
throw new Exception($response->getMessage() ?: trans('texts.payment_error'));
}
2016-06-20 16:14:43 +02:00
} else {
$ref = $response->getTransactionReference();
}
// wrap up
if ($response->isSuccessful() && $ref) {
$payment = $this->createPayment($ref, $paymentMethod);
// TODO move this to stripe driver
2017-05-25 19:29:47 +02:00
if ($this->invitation->invoice->account->isNinjaAccount()) {
2016-06-20 16:14:43 +02:00
Session::flash('trackEventCategory', '/account');
Session::flash('trackEventAction', '/buy_pro_plan');
Session::flash('trackEventAmount', $payment->amount);
}
return $payment;
} elseif ($response->isRedirect()) {
$this->invitation->transaction_reference = $ref;
$this->invitation->save();
//Session::put('transaction_reference', $ref);
Session::save();
$response->redirect();
} else {
throw new Exception($response->getMessage() ?: trans('texts.payment_error'));
}
}
2017-06-26 21:23:21 +02:00
private function paymentItems()
{
$invoice = $this->invoice();
$items = [];
$total = 0;
foreach ($invoice->invoice_items as $invoiceItem) {
// Some gateways require quantity is an integer
if (floatval($invoiceItem->qty) != intval($invoiceItem->qty)) {
return null;
}
2017-06-26 21:23:21 +02:00
$item = new Item([
'name' => $invoiceItem->product_key,
'description' => $invoiceItem->notes,
'price' => $invoiceItem->cost,
'quantity' => $invoiceItem->qty,
]);
$items[] = $item;
$total += $invoiceItem->cost * $invoiceItem->qty;
2017-06-26 21:23:21 +02:00
}
if ($total != $invoice->getRequestedAmount()) {
$item = new Item([
2017-06-28 21:01:12 +02:00
'name' => trans('texts.taxes_and_fees'),
2017-06-26 21:23:21 +02:00
'description' => '',
'price' => $invoice->getRequestedAmount() - $total,
'quantity' => 1,
]);
$items[] = $item;
}
return $items;
}
2016-07-12 22:46:41 +02:00
private function updateClient()
2016-06-20 16:14:43 +02:00
{
2017-01-30 17:05:31 +01:00
if (! $this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) {
2016-07-13 11:03:39 +02:00
return;
}
// update the contact info
2017-01-30 17:05:31 +01:00
if (! $this->contact()->getFullName()) {
2016-07-13 11:03:39 +02:00
$this->contact()->first_name = $this->input['first_name'];
$this->contact()->last_name = $this->input['last_name'];
}
2017-01-30 17:05:31 +01:00
if (! $this->contact()->email) {
2016-07-12 22:46:41 +02:00
$this->contact()->email = $this->input['email'];
}
2016-07-13 11:03:39 +02:00
if ($this->contact()->isDirty()) {
$this->contact()->save();
2016-06-20 16:14:43 +02:00
}
2017-11-19 21:34:34 +01:00
// update the address info
2017-11-20 15:44:28 +01:00
$client = $this->client();
if ($this->accountGateway->show_address && $this->accountGateway->update_address) {
$client->address1 = trim($this->input['address1']);
$client->address2 = trim($this->input['address2']);
$client->city = trim($this->input['city']);
$client->state = trim($this->input['state']);
$client->postal_code = trim($this->input['postal_code']);
$client->country_id = trim($this->input['country_id']);
2017-11-19 21:34:34 +01:00
}
2017-11-20 15:44:28 +01:00
if ($this->accountGateway->show_shipping_address) {
$client->shipping_address1 = trim($this->input['shipping_address1']);
$client->shipping_address2 = trim($this->input['shipping_address2']);
$client->shipping_city = trim($this->input['shipping_city']);
$client->shipping_state = trim($this->input['shipping_state']);
$client->shipping_postal_code = trim($this->input['shipping_postal_code']);
$client->shipping_country_id = trim($this->input['shipping_country_id']);
2016-06-20 16:14:43 +02:00
}
2017-11-20 15:44:28 +01:00
if ($client->isDirty()) {
$client->save();
}
2016-06-20 16:14:43 +02:00
}
2016-07-21 14:35:23 +02:00
protected function paymentDetails($paymentMethod = false)
2016-06-20 16:14:43 +02:00
{
$invoice = $this->invoice();
$gatewayTypeAlias = $this->gatewayType == GATEWAY_TYPE_TOKEN ? $this->gatewayType : GatewayType::getAliasFromId($this->gatewayType);
2017-03-22 20:39:05 +01:00
$completeUrl = $this->invitation->getLink('complete', true) . '/' . $gatewayTypeAlias;
2016-06-20 16:14:43 +02:00
$data = [
'amount' => $invoice->getRequestedAmount(),
'currency' => $invoice->getCurrencyCode(),
'returnUrl' => $completeUrl,
'cancelUrl' => $this->invitation->getLink(),
'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->invoice_number}",
'transactionId' => $invoice->invoice_number,
'transactionType' => 'Purchase',
'clientIp' => Request::getClientIp(),
2016-06-20 16:14:43 +02:00
];
if ($paymentMethod) {
if ($this->customerReferenceParam) {
$data[$this->customerReferenceParam] = $paymentMethod->account_gateway_token->token;
}
$data[$this->sourceReferenceParam] = $paymentMethod->source_reference;
} elseif ($this->input) {
$data['card'] = new CreditCard($this->paymentDetailsFromInput($this->input));
} else {
$data['card'] = new CreditCard($this->paymentDetailsFromClient());
}
return $data;
}
2016-07-21 14:35:23 +02:00
private function paymentDetailsFromInput($input)
2016-06-20 16:14:43 +02:00
{
2016-07-21 14:35:23 +02:00
$invoice = $this->invoice();
2016-06-20 16:14:43 +02:00
$client = $this->client();
$data = [
'company' => $client->getDisplayName(),
'firstName' => isset($input['first_name']) ? $input['first_name'] : null,
'lastName' => isset($input['last_name']) ? $input['last_name'] : null,
'email' => isset($input['email']) ? $input['email'] : null,
'number' => isset($input['card_number']) ? $input['card_number'] : null,
'expiryMonth' => isset($input['expiration_month']) ? $input['expiration_month'] : null,
'expiryYear' => isset($input['expiration_year']) ? $input['expiration_year'] : null,
];
// allow space until there's a setting to disable
if (isset($input['cvv']) && $input['cvv'] != ' ') {
$data['cvv'] = $input['cvv'];
}
if (isset($input['address1'])) {
2017-12-08 13:53:10 +01:00
$hasShippingAddress = $this->accountGateway->show_shipping_address;
$country = Utils::getFromCache($input['country_id'], 'countries');
$shippingCountry = $hasShippingAddress ? Utils::getFromCache($input['shipping_country_id'], 'countries') : $country;
2016-06-20 16:14:43 +02:00
$data = array_merge($data, [
2017-12-08 13:53:10 +01:00
'billingAddress1' => trim($input['address1']),
'billingAddress2' => trim($input['address2']),
'billingCity' => trim($input['city']),
'billingState' => trim($input['state']),
'billingPostcode' => trim($input['postal_code']),
2016-06-20 16:14:43 +02:00
'billingCountry' => $country->iso_3166_2,
2017-12-08 13:53:10 +01:00
'shippingAddress1' => $hasShippingAddress ? trim($this->input['shipping_address1']) : trim($input['address1']),
'shippingAddress2' => $hasShippingAddress ? trim($this->input['shipping_address2']) : trim($input['address2']),
'shippingCity' => $hasShippingAddress ? trim($this->input['shipping_city']) : trim($input['city']),
'shippingState' => $hasShippingAddress ? trim($this->input['shipping_state']) : trim($input['state']),
'shippingPostcode' => $hasShippingAddress ? trim($this->input['shipping_postal_code']) : trim($input['postal_code']),
'shippingCountry' => $hasShippingAddress ? $shippingCountry->iso_3166_2 : $country->iso_3166_2,
2016-06-20 16:14:43 +02:00
]);
}
return $data;
}
public function paymentDetailsFromClient()
{
2016-07-21 14:35:23 +02:00
$invoice = $this->invoice();
2016-06-20 16:14:43 +02:00
$client = $this->client();
$contact = $this->invitation->contact ?: $client->contacts()->first();
2017-12-08 13:53:10 +01:00
$hasShippingAddress = $this->accountGateway->show_shipping_address;
2016-06-20 16:14:43 +02:00
return [
'email' => $contact->email,
'company' => $client->getDisplayName(),
'firstName' => $contact->first_name,
'lastName' => $contact->last_name,
'billingAddress1' => $client->address1,
'billingAddress2' => $client->address2,
'billingCity' => $client->city,
'billingPostcode' => $client->postal_code,
'billingState' => $client->state,
'billingCountry' => $client->country ? $client->country->iso_3166_2 : '',
'billingPhone' => $contact->phone,
2017-12-08 13:53:10 +01:00
'shippingAddress1' => $client->shipping_address1 ? $client->shipping_address1 : $client->address1,
'shippingAddress2' => $client->shipping_address1 ? $client->shipping_address1 : $client->address2,
'shippingCity' => $client->shipping_address1 ? $client->shipping_address1 : $client->city,
'shippingPostcode' => $client->shipping_address1 ? $client->shipping_address1 : $client->postal_code,
'shippingState' => $client->shipping_address1 ? $client->shipping_address1 : $client->state,
'shippingCountry' => $client->shipping_address1 ? ($client->shipping_country ? $client->shipping_country->iso_3166_2 : '') : ($client->country ? $client->country->iso_3166_2 : ''),
2016-06-20 16:14:43 +02:00
'shippingPhone' => $contact->phone,
];
}
2017-09-05 15:37:19 +02:00
public function shouldUseSource()
{
// Use Omnipay by default
return false;
}
2016-06-20 16:14:43 +02:00
protected function shouldCreateToken()
{
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
return true;
}
2017-01-30 17:05:31 +01:00
if (! $this->handles(GATEWAY_TYPE_TOKEN)) {
2016-06-20 16:14:43 +02:00
return false;
}
if ($this->account()->token_billing_type_id == TOKEN_BILLING_ALWAYS) {
return true;
}
return boolval(array_get($this->input, 'token_billing'));
}
2016-07-21 14:35:23 +02:00
/*
protected function tokenDetails()
{
$details = [];
if ($customer = $this->customer()) {
$details['customerReference'] = $customer->token;
}
return $details;
}
*/
2016-06-20 16:14:43 +02:00
public function customer($clientId = false)
{
if ($this->customer) {
return $this->customer;
}
2017-01-30 17:05:31 +01:00
if (! $clientId) {
2016-06-20 16:14:43 +02:00
$clientId = $this->client()->id;
}
$this->customer = AccountGatewayToken::clientAndGateway($clientId, $this->accountGateway->id)
->with('payment_methods')
->first();
2016-07-27 13:37:36 +02:00
if ($this->customer && $this->invitation) {
2016-06-20 16:14:43 +02:00
$this->customer = $this->checkCustomerExists($this->customer) ? $this->customer : null;
}
return $this->customer;
}
protected function checkCustomerExists($customer)
{
return true;
}
public function verifyBankAccount($client, $publicId, $amount1, $amount2)
{
throw new Exception('verifyBankAccount not implemented');
}
2016-07-21 14:35:23 +02:00
public function removePaymentMethod($paymentMethod)
2016-06-20 16:14:43 +02:00
{
$paymentMethod->delete();
}
2016-07-21 14:35:23 +02:00
// Some gateways (ie, Checkout.com and Braintree) require generating a token before paying for the invoice
2016-06-20 16:14:43 +02:00
public function createTransactionToken()
{
return null;
}
public function createToken()
{
$account = $this->account();
2017-01-30 17:05:31 +01:00
if (! $customer = $this->customer()) {
2016-06-20 16:14:43 +02:00
$customer = new AccountGatewayToken();
$customer->account_id = $account->id;
$customer->contact_id = $this->invitation->contact_id;
$customer->account_gateway_id = $this->accountGateway->id;
$customer->client_id = $this->client()->id;
$customer = $this->creatingCustomer($customer);
$customer->save();
}
$paymentMethod = $this->createPaymentMethod($customer);
if ($paymentMethod) {
2016-06-20 16:14:43 +02:00
$customer->default_payment_method_id = $paymentMethod->id;
$customer->save();
}
return $paymentMethod;
}
protected function creatingCustomer($customer)
{
return $customer;
}
public function createPaymentMethod($customer)
{
$paymentMethod = PaymentMethod::createNew($this->invitation);
2016-07-15 10:23:19 +02:00
$paymentMethod->contact_id = $this->contact()->id;
2016-06-20 16:14:43 +02:00
$paymentMethod->ip = Request::ip();
$paymentMethod->account_gateway_token_id = $customer->id;
$paymentMethod->setRelation('account_gateway_token', $customer);
$paymentMethod = $this->creatingPaymentMethod($paymentMethod);
2016-09-19 12:29:56 +02:00
if ($paymentMethod) {
// archive the old payment method
$oldPaymentMethod = PaymentMethod::clientId($this->client()->id)
->wherePaymentTypeId($paymentMethod->payment_type_id)
->first();
2016-09-19 12:29:56 +02:00
if ($oldPaymentMethod) {
$oldPaymentMethod->delete();
}
2016-06-20 16:14:43 +02:00
$paymentMethod->save();
}
return $paymentMethod;
}
2016-07-21 14:35:23 +02:00
protected function creatingPaymentMethod($paymentMethod)
2016-06-20 16:14:43 +02:00
{
return $paymentMethod;
}
public function deleteToken()
{
}
2016-07-21 14:35:23 +02:00
public function createPayment($ref = false, $paymentMethod = null)
2016-06-20 16:14:43 +02:00
{
2017-05-24 20:09:47 +02:00
$account = $this->account();
2016-06-20 16:14:43 +02:00
$invitation = $this->invitation;
$invoice = $this->invoice();
2017-07-06 11:45:44 +02:00
if (! $invoice->canBePaid()) {
return false;
}
$invoice->markSentIfUnsent();
2016-06-20 16:14:43 +02:00
$payment = Payment::createNew($invitation);
$payment->invitation_id = $invitation->id;
$payment->account_gateway_id = $this->accountGateway->id;
$payment->invoice_id = $invoice->id;
$payment->amount = $invoice->getRequestedAmount();
$payment->client_id = $invoice->client_id;
$payment->contact_id = $invitation->contact_id;
$payment->transaction_reference = $ref;
2017-05-24 20:09:47 +02:00
$payment->payment_date = $account->getDateTime()->format('Y-m-d');
2016-06-20 16:14:43 +02:00
$payment->ip = Request::ip();
2016-06-23 19:17:02 +02:00
$payment = $this->creatingPayment($payment, $paymentMethod);
2016-06-20 16:14:43 +02:00
if ($paymentMethod) {
$payment->last4 = $paymentMethod->last4;
$payment->expiration = $paymentMethod->expiration;
$payment->routing_number = $paymentMethod->routing_number;
$payment->payment_type_id = $paymentMethod->payment_type_id;
$payment->email = $paymentMethod->email;
$payment->bank_name = $paymentMethod->bank_name;
$payment->payment_method_id = $paymentMethod->id;
}
$payment->save();
2016-12-27 22:56:55 +01:00
$accountKey = $invoice->account->account_key;
if ($accountKey == env('NINJA_LICENSE_ACCOUNT_KEY')) {
$this->createLicense($payment);
2016-06-20 16:14:43 +02:00
// TODO move this code
// enable pro plan for hosted users
2017-05-25 19:29:47 +02:00
} elseif ($invoice->account->isNinjaAccount()) {
2016-06-20 16:14:43 +02:00
foreach ($invoice->invoice_items as $invoice_item) {
// Hacky, but invoices don't have meta fields to allow us to store this easily
if (1 == preg_match('/^Plan - (.+) \((.+)\)$/', $invoice_item->product_key, $matches)) {
$plan = strtolower($matches[1]);
$term = strtolower($matches[2]);
2016-07-14 11:46:00 +02:00
$price = $invoice_item->cost;
2016-07-11 19:08:43 +02:00
if ($plan == PLAN_ENTERPRISE) {
2017-10-13 10:34:26 +02:00
preg_match('/###[\d]* [\w]* (\d*)/', $invoice_item->notes, $numUserMatches);
2016-09-27 20:03:46 +02:00
if (count($numUserMatches)) {
$numUsers = $numUserMatches[1];
2016-09-11 16:30:23 +02:00
} else {
$numUsers = 5;
}
2016-07-11 19:08:43 +02:00
} else {
$numUsers = 1;
}
2016-06-20 16:14:43 +02:00
}
}
2017-01-30 20:40:43 +01:00
if (! empty($plan)) {
2016-06-20 16:14:43 +02:00
$account = Account::with('users')->find($invoice->client->public_id);
2016-07-14 11:46:00 +02:00
$company = $account->company;
2016-06-20 16:14:43 +02:00
2017-01-30 17:05:31 +01:00
if (
2016-07-14 11:46:00 +02:00
$company->plan != $plan
2017-01-25 19:01:12 +01:00
|| DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) <= date_create('-7 days')
2016-06-20 16:14:43 +02:00
) {
// Either this is a different plan, or the subscription expired more than a week ago
// Reset any grandfathering
2016-07-14 11:46:00 +02:00
$company->plan_started = date_create()->format('Y-m-d');
2016-06-20 16:14:43 +02:00
}
if (
2016-07-14 11:46:00 +02:00
$company->plan == $plan
&& $company->plan_term == $term
&& DateTime::createFromFormat('Y-m-d', $company->plan_expires) >= date_create()
2016-06-20 16:14:43 +02:00
) {
// This is a renewal; mark it paid as of when this term expires
2016-07-14 11:46:00 +02:00
$company->plan_paid = $company->plan_expires;
2016-06-20 16:14:43 +02:00
} else {
2016-07-14 11:46:00 +02:00
$company->plan_paid = date_create()->format('Y-m-d');
2016-06-20 16:14:43 +02:00
}
2016-07-14 11:46:00 +02:00
$company->payment_id = $payment->id;
$company->plan = $plan;
$company->plan_term = $term;
$company->plan_price = $price;
$company->num_users = $numUsers;
$company->plan_expires = DateTime::createFromFormat('Y-m-d', $account->company->plan_paid)
2016-06-20 16:14:43 +02:00
->modify($term == PLAN_TERM_MONTHLY ? '+1 month' : '+1 year')->format('Y-m-d');
2016-12-14 15:19:16 +01:00
if ($company->hasActivePromo()) {
$company->discount_expires = date_create()->modify('1 year')->format('Y-m-d');
$company->promo_expires = null;
}
2016-07-14 11:46:00 +02:00
$company->save();
2016-06-20 16:14:43 +02:00
}
}
return $payment;
}
2016-12-27 22:56:55 +01:00
protected function createLicense($payment)
{
// TODO parse invoice to determine license
if ($payment->amount == 20) {
$affiliateId = 4;
$productId = PRODUCT_WHITE_LABEL;
} else {
$affiliateId = 1;
$productId = PRODUCT_ONE_CLICK_INSTALL;
}
$license = new License();
$license->first_name = $this->contact()->first_name;
$license->last_name = $this->contact()->last_name;
$license->email = $this->contact()->email;
$license->transaction_reference = $payment->transaction_reference;
$license->license_key = Utils::generateLicense();
$license->affiliate_id = $affiliateId;
$license->product_id = $productId;
$license->save();
2016-12-28 15:53:18 +01:00
// Add the license key to the invoice content
$invoiceItem = $payment->invoice->invoice_items->first();
2017-11-10 11:03:05 +01:00
$invoiceItem->notes .= "\n\n#{$license->license_key}";
2016-12-28 15:53:18 +01:00
$invoiceItem->save();
2016-12-27 22:56:55 +01:00
// Add the license key to the redirect URL
$key = 'redirect_url:' . $payment->invitation->invitation_key;
$redirectUrl = session($key);
session([$key => "{$redirectUrl}?license_key={$license->license_key}&product_id={$productId}"]);
}
2016-07-21 14:35:23 +02:00
protected function creatingPayment($payment, $paymentMethod)
2016-06-20 16:14:43 +02:00
{
return $payment;
}
2016-07-21 14:35:23 +02:00
public function refundPayment($payment, $amount = 0)
2016-06-20 16:14:43 +02:00
{
2016-06-28 20:21:54 +02:00
if ($amount) {
$amount = min($amount, $payment->getCompletedAmount());
} else {
$amount = $payment->getCompletedAmount();
}
2016-06-20 16:14:43 +02:00
2017-01-30 17:05:31 +01:00
if (! $amount) {
2016-06-20 16:14:43 +02:00
return false;
}
if ($payment->payment_type_id == PAYMENT_TYPE_CREDIT) {
return $payment->recordRefund($amount);
}
$details = $this->refundDetails($payment, $amount);
$response = $this->gateway()->refund($details)->send();
if ($response->isSuccessful()) {
return $payment->recordRefund($amount);
} elseif ($this->attemptVoidPayment($response, $payment, $amount)) {
$details = ['transactionReference' => $payment->transaction_reference];
$response = $this->gateway->void($details)->send();
if ($response->isSuccessful()) {
return $payment->markVoided();
}
}
return false;
}
2016-07-21 14:35:23 +02:00
protected function refundDetails($payment, $amount)
2016-06-20 16:14:43 +02:00
{
return [
'amount' => $amount,
'transactionReference' => $payment->transaction_reference,
];
}
2016-07-21 14:35:23 +02:00
protected function attemptVoidPayment($response, $payment, $amount)
2016-06-20 16:14:43 +02:00
{
// Partial refund not allowed for unsettled transactions
return $amount == $payment->amount;
}
protected function createLocalPayment($payment)
{
return $payment;
}
2017-11-20 15:32:17 +01:00
protected function updateClientFromOffsite($transRef, $paymentRef)
{
// do nothing
}
2016-07-21 14:35:23 +02:00
public function completeOffsitePurchase($input)
2016-06-20 16:14:43 +02:00
{
$this->input = $input;
2017-11-20 15:32:17 +01:00
$transRef = array_get($this->input, 'token') ?: $this->invitation->transaction_reference;
2016-06-20 16:14:43 +02:00
if (method_exists($this->gateway(), 'completePurchase')) {
$details = $this->paymentDetails();
$response = $this->gateway()->completePurchase($details)->send();
2017-11-20 15:32:17 +01:00
$paymentRef = $response->getTransactionReference() ?: $transRef;
2016-06-20 16:14:43 +02:00
if ($response->isCancelled()) {
return false;
2017-01-30 17:05:31 +01:00
} elseif (! $response->isSuccessful()) {
2016-06-20 16:14:43 +02:00
throw new Exception($response->getMessage());
}
2017-11-20 15:32:17 +01:00
} else {
$paymentRef = $transRef;
2016-06-20 16:14:43 +02:00
}
2017-11-20 15:32:17 +01:00
$this->updateClientFromOffsite($transRef, $paymentRef);
2016-07-18 20:45:32 +02:00
// check invoice still has balance
2017-01-30 17:05:31 +01:00
if (! floatval($this->invoice()->balance)) {
2016-07-18 20:45:32 +02:00
throw new Exception(trans('texts.payment_error_code', ['code' => 'NB']));
}
// check this isn't a duplicate transaction reference
if (Payment::whereAccountId($this->invitation->account_id)
2017-11-20 15:32:17 +01:00
->whereTransactionReference($paymentRef)
2016-07-18 20:45:32 +02:00
->first()) {
throw new Exception(trans('texts.payment_error_code', ['code' => 'DT']));
}
2017-11-20 15:32:17 +01:00
return $this->createPayment($paymentRef);
2016-06-20 16:14:43 +02:00
}
public function tokenLinks()
2016-06-20 16:14:43 +02:00
{
2017-01-30 17:05:31 +01:00
if (! $this->customer()) {
2016-06-20 16:14:43 +02:00
return [];
}
$paymentMethods = $this->customer()->payment_methods;
$links = [];
foreach ($paymentMethods as $paymentMethod) {
if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH && $paymentMethod->status != PAYMENT_METHOD_STATUS_VERIFIED) {
continue;
}
2017-01-30 17:05:31 +01:00
if (! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) {
continue;
}
2016-06-20 16:14:43 +02:00
$url = URL::to("/payment/{$this->invitation->invitation_key}/token/".$paymentMethod->public_id);
if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) {
if ($paymentMethod->bank_name) {
$label = $paymentMethod->bank_name;
} else {
$label = trans('texts.use_bank_on_file');
}
} elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_PAYPAL) {
$label = 'PayPal: ' . $paymentMethod->email;
} else {
$label = trans('texts.payment_type_on_file', ['type' => $paymentMethod->payment_type->name]);
2016-06-20 16:14:43 +02:00
}
2017-03-16 21:58:31 +01:00
$label .= $this->invoice()->present()->gatewayFee($paymentMethod->payment_type->gateway_type_id);
2017-03-15 16:09:23 +01:00
2016-06-20 16:14:43 +02:00
$links[] = [
'url' => $url,
'label' => $label,
];
}
return $links;
}
public function paymentLinks()
2016-06-20 16:14:43 +02:00
{
$links = [];
foreach ($this->gatewayTypes() as $gatewayTypeId) {
if ($gatewayTypeId === GATEWAY_TYPE_TOKEN) {
continue;
}
2017-01-30 17:05:31 +01:00
if (! $this->meetsGatewayTypeLimits($gatewayTypeId)) {
2016-06-20 16:14:43 +02:00
continue;
}
2016-09-09 23:19:32 +02:00
$gatewayTypeAlias = GatewayType::getAliasFromId($gatewayTypeId);
2016-09-26 11:33:30 +02:00
if ($gatewayTypeId == GATEWAY_TYPE_CUSTOM) {
2017-01-30 20:40:43 +01:00
$url = 'javascript:showCustomModal();';
2016-11-06 12:44:58 +01:00
$label = e($this->accountGateway->getConfigField('name'));
2016-09-26 11:33:30 +02:00
} else {
2017-09-05 10:36:37 +02:00
$url = $this->paymentUrl($gatewayTypeAlias);
2017-08-16 14:33:41 +02:00
if ($custom = $this->account()->getLabel($gatewayTypeAlias)) {
$label = $custom;
} else {
$label = trans("texts.{$gatewayTypeAlias}");
}
2016-09-26 11:33:30 +02:00
}
2017-03-16 21:58:31 +01:00
$label .= $this->invoice()->present()->gatewayFee($gatewayTypeId);
2017-03-15 16:09:23 +01:00
2016-06-20 16:14:43 +02:00
$links[] = [
2016-09-26 11:33:30 +02:00
'gatewayTypeId' => $gatewayTypeId,
'url' => $url,
'label' => $label,
2016-06-20 16:14:43 +02:00
];
}
return $links;
}
2017-03-14 14:18:31 +01:00
public function supportsGatewayType($gatewayTypeId)
{
return in_array($gatewayTypeId, $this->gatewayTypes());
}
protected function meetsGatewayTypeLimits($gatewayTypeId)
{
2017-01-30 20:40:43 +01:00
if (! $gatewayTypeId) {
return true;
}
2016-09-20 08:03:48 +02:00
$accountGatewaySettings = AccountGatewaySettings::scope(false, $this->invitation->account_id)
->where('account_gateway_settings.gateway_type_id', '=', $gatewayTypeId)->first();
if ($accountGatewaySettings) {
$invoice = $this->invoice();
2016-09-14 16:26:50 +02:00
if ($accountGatewaySettings->min_limit !== null && $invoice->balance < $accountGatewaySettings->min_limit) {
return false;
}
2017-01-30 20:40:43 +01:00
if ($accountGatewaySettings->max_limit !== null && $invoice->balance > $accountGatewaySettings->max_limit) {
return false;
}
}
return true;
}
2016-09-09 23:19:32 +02:00
protected function paymentUrl($gatewayTypeAlias)
2016-06-20 16:14:43 +02:00
{
$account = $this->account();
2016-09-09 23:19:32 +02:00
$url = URL::to("/payment/{$this->invitation->invitation_key}/{$gatewayTypeAlias}");
2016-06-20 16:14:43 +02:00
return $url;
}
2016-06-23 15:15:52 +02:00
public function handleWebHook($input)
{
throw new Exception('Unsupported gateway');
}
2016-06-20 16:14:43 +02:00
}