1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 13:12:50 +01:00

Merge pull request #6468 from turbo124/v5-develop

Fixes for support messages
This commit is contained in:
David Bomba 2021-08-16 19:02:53 +10:00 committed by GitHub
commit 10fd93185d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 498 additions and 14 deletions

View File

@ -92,6 +92,7 @@ class InvoiceItemSum
private function sumLineItem()
{ //todo need to support quantities less than the precision amount
// $this->setLineTotal($this->formatValue($this->item->cost, $this->currency->precision) * $this->formatValue($this->item->quantity, $this->currency->precision));
$this->setLineTotal($this->item->cost * $this->item->quantity);
return $this;

View File

@ -39,6 +39,7 @@ class ClientTransformer extends BaseTransformer
'work_phone' => $this->getString( $data, 'client.phone' ),
'address1' => $this->getString( $data, 'client.address1' ),
'address2' => $this->getString( $data, 'client.address2' ),
'postal_code' => $this->getString( $data, 'client.postal_code'),
'city' => $this->getString( $data, 'client.city' ),
'state' => $this->getString( $data, 'client.state' ),
'shipping_address1' => $this->getString( $data, 'client.shipping_address1' ),

View File

@ -1506,12 +1506,6 @@ class Import implements ShouldQueue
'new' => $expense_category->id,
];
// $this->ids['expense_categories'] = [
// "expense_categories_{$old_user_key}" => [
// 'old' => $resource['id'],
// 'new' => $expense_category->id,
// ],
// ];
}
ExpenseCategory::reguard();

View File

@ -53,7 +53,8 @@ class SupportMessageSent extends Mailable
$account = auth()->user()->account;
$priority = '';
$plan = $account->plan ?: '';
$plan = $account->plan ?: 'customer support';
$plan = ucfirst($plan);
if(strlen($plan) >1)
$priority = '[PRIORITY] ';
@ -63,9 +64,9 @@ class SupportMessageSent extends Mailable
$db = str_replace("db-ninja-", "", $company->db);
if(Ninja::isHosted())
$subject = "{$priority}Hosted-{$db} :: {ucfirst($plan)} :: ".date('M jS, g:ia');
$subject = "{$priority}Hosted-{$db} :: {$plan} :: ".date('M jS, g:ia');
else
$subject = "{$priority}Self Hosted :: {ucfirst($plan)} :: ".date('M jS, g:ia');
$subject = "{$priority}Self Hosted :: {$plan} :: ".date('M jS, g:ia');
return $this->from(config('mail.from.address'), $user->present()->name())
->replyTo($user->email, $user->present()->name())

View File

@ -0,0 +1,117 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Stripe;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
use App\PaymentDrivers\Stripe\CreditCard;
use App\Utils\Ninja;
class ApplePay
{
/** @var StripePaymentDriver */
public $stripe_driver;
public function __construct(StripePaymentDriver $stripe_driver)
{
$this->stripe_driver = $stripe_driver;
}
public function paymentView(array $data)
{
$this->registerDomain();
$data['gateway'] = $this->stripe_driver;
$data['payment_hash'] = $this->stripe_driver->payment_hash->hash;
$data['payment_method_id'] = GatewayType::APPLE_PAY;
$data['country'] = $this->stripe_driver->client->country;
$data['currency'] = $this->stripe_driver->client->currency()->code;
$data['stripe_amount'] = $this->stripe_driver->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe_driver->client->currency()->precision, $this->stripe_driver->client->currency());
$data['invoices'] = $this->stripe_driver->payment_hash->invoices();
$data['intent'] = \Stripe\PaymentIntent::create([
'amount' => $data['stripe_amount'],
'currency' => $this->stripe_driver->client->getCurrencyCode();,
], $this->stripe_driver->stripe_connect_auth);
$this->stripe_driver->payment_hash->data = array_merge((array) $this->stripe_driver->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]);
$this->stripe_driver->payment_hash->save();
return render('gateways.stripe.applepay.pay', $data);
}
public function paymentResponse(PaymentResponseRequest $request)
{
$this->stripe_driver->init();
$state = [
'server_response' => json_decode($request->gateway_response),
'payment_hash' => $request->payment_hash,
];
$state['payment_intent'] = \Stripe\PaymentIntent::retrieve($state['server_response']->id, $this->stripe_driver->stripe_connect_auth);
$state['customer'] = $state['payment_intent']->customer;
$this->stripe_driver->payment_hash->data = array_merge((array) $this->stripe_driver->payment_hash->data, $state);
$this->stripe_driver->payment_hash->save();
$server_response = $this->stripe_driver->payment_hash->data->server_response;
$response_handler = new CreditCard($this->stripe_driver);
if ($server_response->status == 'succeeded') {
$this->stripe_driver->logSuccessfulGatewayResponse(['response' => json_decode($request->gateway_response), 'data' => $this->stripe_driver->payment_hash], SystemLog::TYPE_STRIPE);
return $response_handler->processSuccessfulPayment();
}
return $response_handler->processUnsuccessfulPayment($server_response);
}
private function registerDomain()
{
if(Ninja::isHosted())
{
$domain = isset($this->stripe_driver->company_gateway->company->portal_domain) ? $this->stripe_driver->company_gateway->company->portal_domain : $this->stripe_driver->company_gateway->company->domain();
\Stripe\ApplePayDomain::create([
'domain_name' => $domain,
], $this->stripe_driver->stripe_connect_auth);
}
else {
\Stripe\ApplePayDomain::create([
'domain_name' => config('ninja.app_url'),
]);
}
}
}

View File

@ -108,7 +108,7 @@ class CreditCard
return $this->processUnsuccessfulPayment($server_response);
}
private function processSuccessfulPayment()
public function processSuccessfulPayment()
{
$stripe_method = $this->stripe->getStripePaymentMethod($this->stripe->payment_hash->data->server_response->payment_method);
@ -148,7 +148,7 @@ class CreditCard
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
}
private function processUnsuccessfulPayment($server_response)
public function processUnsuccessfulPayment($server_response)
{
PaymentFailureMailer::dispatch($this->stripe->client, $server_response->cancellation_reason, $this->stripe->client->company, $server_response->amount);

View File

@ -0,0 +1,109 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Stripe;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
use App\PaymentDrivers\Stripe\CreditCard;
use App\Utils\Ninja;
class SEPA
{
/** @var StripePaymentDriver */
public $stripe_driver;
public function __construct(StripePaymentDriver $stripe_driver)
{
$this->stripe_driver = $stripe_driver;
}
public function authorizeView(array $data)
{
$customer = $this->stripe_driver->findOrCreateCustomer();
$setup_intent = \Stripe\SetupIntent::create([
'payment_method_types' => ['sepa_debit'],
'customer' => $customer->id,
], $this->stripe_driver->stripe_connect_auth);
$client_secret = $setup_intent->client_secret
// Pass the client secret to the client
$data['gateway'] = $this->stripe;
return render('gateways.stripe.sepa.authorize', array_merge($data));
}
public function paymentResponse(PaymentResponseRequest $request)
{
// $this->stripe_driver->init();
// $state = [
// 'server_response' => json_decode($request->gateway_response),
// 'payment_hash' => $request->payment_hash,
// ];
// $state['payment_intent'] = \Stripe\PaymentIntent::retrieve($state['server_response']->id, $this->stripe_driver->stripe_connect_auth);
// $state['customer'] = $state['payment_intent']->customer;
// $this->stripe_driver->payment_hash->data = array_merge((array) $this->stripe_driver->payment_hash->data, $state);
// $this->stripe_driver->payment_hash->save();
// $server_response = $this->stripe_driver->payment_hash->data->server_response;
// $response_handler = new CreditCard($this->stripe_driver);
// if ($server_response->status == 'succeeded') {
// $this->stripe_driver->logSuccessfulGatewayResponse(['response' => json_decode($request->gateway_response), 'data' => $this->stripe_driver->payment_hash], SystemLog::TYPE_STRIPE);
// return $response_handler->processSuccessfulPayment();
// }
// return $response_handler->processUnsuccessfulPayment($server_response);
}
/* Searches for a stripe customer by email
otherwise searches by gateway tokens in StripePaymentdriver
finally creates a new customer if none found
*/
private function getCustomer()
{
$searchResults = \Stripe\Customer::all([
"email" => $this->stripe_driver->client->present()->email(),
"limit" => 1,
"starting_after" => null
], $this->stripe_driver->stripe_connect_auth);
if(count($searchResults) >= 1)
return $searchResults[0];
return $this->stripe_driver->findOrCreateCustomer();
}
}

View File

@ -25,6 +25,7 @@ use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\PaymentDrivers\Stripe\ACH;
use App\PaymentDrivers\Stripe\Alipay;
use App\PaymentDrivers\Stripe\ApplePay;
use App\PaymentDrivers\Stripe\Charge;
use App\PaymentDrivers\Stripe\Connect\Verify;
use App\PaymentDrivers\Stripe\CreditCard;
@ -72,7 +73,7 @@ class StripePaymentDriver extends BaseDriver
GatewayType::BANK_TRANSFER => ACH::class,
GatewayType::ALIPAY => Alipay::class,
GatewayType::SOFORT => SOFORT::class,
GatewayType::APPLE_PAY => 1, // TODO
GatewayType::APPLE_PAY => ApplePay::class,
GatewayType::SEPA => 1, // TODO
];

View File

@ -44,6 +44,7 @@
"fakerphp/faker": "^1.14",
"fideloper/proxy": "^4.2",
"fruitcake/laravel-cors": "^2.0",
"gocardless/gocardless-pro": "^4.12",
"google/apiclient": "^2.7",
"guzzlehttp/guzzle": "^7.0.1",
"hashids/hashids": "^4.0",

59
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "275a9dd3910b6ec79607b098406dc6c7",
"content-hash": "bcd9405b1978cef268d732794883e91d",
"packages": [
{
"name": "asm/php-ansible",
@ -2221,6 +2221,61 @@
],
"time": "2021-04-26T11:24:25+00:00"
},
{
"name": "gocardless/gocardless-pro",
"version": "4.12.0",
"source": {
"type": "git",
"url": "https://github.com/gocardless/gocardless-pro-php.git",
"reference": "e63b97b215c27179023dd2e911133ee75e543fbd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/gocardless/gocardless-pro-php/zipball/e63b97b215c27179023dd2e911133ee75e543fbd",
"reference": "e63b97b215c27179023dd2e911133ee75e543fbd",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"guzzlehttp/guzzle": "^6.0 | ^7.0",
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^7.5",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"autoload": {
"psr-4": {
"GoCardlessPro\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "GoCardless and contributors",
"homepage": "https://github.com/gocardless/gocardless-pro-php/contributors"
}
],
"description": "GoCardless Pro PHP Client Library",
"homepage": "https://gocardless.com/",
"keywords": [
"api",
"direct debit",
"gocardless"
],
"support": {
"issues": "https://github.com/gocardless/gocardless-pro-php/issues",
"source": "https://github.com/gocardless/gocardless-pro-php/tree/v4.12.0"
},
"time": "2021-08-12T15:41:16+00:00"
},
{
"name": "google/apiclient",
"version": "v2.10.1",
@ -14972,5 +15027,5 @@
"platform-dev": {
"php": "^7.3|^7.4|^8.0"
},
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -0,0 +1,122 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Apple Pay', 'card_title' => 'Apple Pay'])
@section('gateway_head')
@endsection
@section('gateway_content')
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@include('portal.ninja2020.gateways.includes.payment_details')
<div id="payment-request-button">
<!-- A Stripe Element will be inserted here. -->
</div>
@endsection
@push('footer')
<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript">
@if($gateway->company_gateway->getConfigField('account_id'))
var stripe = Stripe('{{ config('ninja.ninja_stripe_publishable_key') }}', {
apiVersion: "2018-05-21",
stripeAccount: '{{ $gateway->company_gateway->getConfigField('account_id') }}',
});
@else
var stripe = Stripe('{{ $gateway->getPublishableKey() }}', {
apiVersion: "2018-05-21",
});
@endif
var paymentRequest = stripe.paymentRequest({
country: '{{ $country->iso_3166_2 }}',
currency: '{{ $currency }}',
total: {
label: '{{ ctrans('texts.payment_amount') }}',
amount: {{ $stripe_amount }},
},
requestPayerName: true,
requestPayerEmail: true,
});
var elements = stripe.elements();
var prButton = elements.create('paymentRequestButton', {
paymentRequest: paymentRequest,
});
// Check the availability of the Payment Request API first.
paymentRequest.canMakePayment().then(function(result) {
if (result) {
prButton.mount('#payment-request-button');
} else {
document.getElementById('payment-request-button').style.display = 'none';
}
});
paymentRequest.on('paymentmethod', function(ev) {
// Confirm the PaymentIntent without handling potential next actions (yet).
stripe.confirmCardPayment(
'{{ $intent->client_secret }}',
{payment_method: ev.paymentMethod.id},
{handleActions: false}
).then(function(confirmResult) {
if (confirmResult.error) {
// Report to the browser that the payment failed, prompting it to
// re-show the payment interface, or show an error message and close
// the payment interface.
ev.complete('fail');
} else {
// Report to the browser that the confirmation was successful, prompting
// it to close the browser payment method collection interface.
ev.complete('success');
// Check if the PaymentIntent requires any actions and if so let Stripe.js
// handle the flow. If using an API version older than "2019-02-11"
// instead check for: `paymentIntent.status === "requires_source_action"`.
if (confirmResult.paymentIntent.status === "requires_action") {
// Let Stripe.js handle the rest of the payment flow.
stripe.confirmCardPayment(clientSecret).then(function(result) {
if (result.error) {
// The payment failed -- ask your customer for a new payment method.
handleFailure(result.error)
} else {
// The payment has succeeded.
handleSuccess(result);
}
});
} else {
// The payment has succeeded.
}
}
});
});
handleSuccess(result) {
document.querySelector(
'input[name="gateway_response"]'
).value = JSON.stringify(result.paymentIntent);
document.getElementById('server-response').submit();
}
handleFailure(message) {
let errors = document.getElementById('errors');
errors.textContent = '';
errors.textContent = message;
errors.hidden = false;
}
</script>
@endpush

View File

@ -0,0 +1,82 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'SEPA', 'card_title' => 'SEPA'])
@section('gateway_head')
@if($gateway->company_gateway->getConfigField('account_id'))
<meta name="stripe-account-id" content="{{ $gateway->company_gateway->getConfigField('account_id') }}">
<meta name="stripe-publishable-key" content="{{ config('ninja.ninja_stripe_publishable_key') }}">
@else
<meta name="stripe-publishable-key" content="{{ $gateway->company_gateway->getPublishableKey() }}">
@endif
@endsection
@section('gateway_content')
@if(session()->has('ach_error'))
<div class="alert alert-failure mb-4">
<p>{{ session('ach_error') }}</p>
</div>
@endif
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::SEPA]) }}" method="post" id="server_response">
@csrf
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="hidden" name="gateway_type_id" value="9">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="is_default" id="is_default">
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_type')])
<span class="flex items-center mr-4">
<input class="form-radio mr-2" type="radio" value="individual" name="account-holder-type" checked>
<span>{{ __('texts.individual_account') }}</span>
</span>
<span class="flex items-center">
<input class="form-radio mr-2" type="radio" value="company" name="account-holder-type">
<span>{{ __('texts.company_account') }}</span>
</span>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')])
<input class="input w-full" id="account-holder-name" type="text" placeholder="{{ ctrans('texts.name') }}" required>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.country')])
<select name="countries" id="country" class="form-select input w-full" required>
@foreach($countries as $country)
<option value="{{ $country->iso_3166_2 }}">{{ $country->iso_3166_2 }} ({{ $country->name }})</option>
@endforeach
</select>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.currency')])
<select name="currencies" id="currency" class="form-select input w-full">
@foreach($currencies as $currency)
<option value="{{ $currency->code }}">{{ $currency->code }} ({{ $currency->name }})</option>
@endforeach
</select>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.routing_number')])
<input class="input w-full" id="routing-number" type="text" required>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_number')])
<input class="input w-full" id="account-number" type="text" required>
@endcomponent
@component('portal.ninja2020.components.general.card-element-single')
<input type="checkbox" class="form-checkbox mr-1" id="accept-terms" required>
<label for="accept-terms" class="cursor-pointer">{{ ctrans('texts.ach_authorization', ['company' => auth()->user()->company->present()->name, 'email' => auth('contact')->user()->client->company->settings->email]) }}</label>
@endcomponent
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'save-button'])
{{ ctrans('texts.add_payment_method') }}
@endcomponent
@endsection
@section('gateway_footer')
<script src="https://js.stripe.com/v3/"></script>
<script src="{{ asset('js/clients/payments/stripe-ach.js') }}"></script>
@endsection