1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-22 09:21:34 +02:00

Merge branch 'v5-develop' into v5-stable

This commit is contained in:
David Bomba 2021-08-17 20:15:57 +10:00
commit ca6468f8d6
59 changed files with 231967 additions and 231851 deletions

View File

@ -1 +1 @@
5.2.18 5.2.19

View File

@ -14,6 +14,7 @@ namespace App\Exceptions;
use App\Exceptions\FilePermissionsFailure; use App\Exceptions\FilePermissionsFailure;
use App\Exceptions\InternalPDFFailure; use App\Exceptions\InternalPDFFailure;
use App\Exceptions\PhantomPDFFailure; use App\Exceptions\PhantomPDFFailure;
use App\Exceptions\StripeConnectFailure;
use App\Utils\Ninja; use App\Utils\Ninja;
use Exception; use Exception;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
@ -197,6 +198,8 @@ class Handler extends ExceptionHandler
return response()->json(['message' => $exception->getMessage()], 400); return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof GenericPaymentDriverFailure) { } elseif ($exception instanceof GenericPaymentDriverFailure) {
return response()->json(['message' => $exception->getMessage()], 400); return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof StripeConnectFailure) {
return response()->json(['message' => $exception->getMessage()], 400);
} }
return parent::render($request, $exception); return parent::render($request, $exception);

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class StripeConnectFailure extends Exception
{
// ..
}

View File

@ -81,6 +81,8 @@ class InvoiceItemSum
private function push() private function push()
{ {
nlog($this->sub_total . " + ". $this->getLineTotal());
$this->sub_total += $this->getLineTotal(); $this->sub_total += $this->getLineTotal();
$this->line_items[] = $this->item; $this->line_items[] = $this->item;
@ -92,6 +94,7 @@ class InvoiceItemSum
private function sumLineItem() private function sumLineItem()
{ //todo need to support quantities less than the precision amount { //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->formatValue($this->item->cost, $this->currency->precision) * $this->formatValue($this->item->quantity, $this->currency->precision));
$this->setLineTotal($this->item->cost * $this->item->quantity); $this->setLineTotal($this->item->cost * $this->item->quantity);
return $this; return $this;
@ -102,7 +105,15 @@ class InvoiceItemSum
if ($this->invoice->is_amount_discount) { if ($this->invoice->is_amount_discount) {
$this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision)); $this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision));
} else { } else {
$this->setLineTotal($this->getLineTotal() - $this->formatValue(round($this->item->line_total * ($this->item->discount / 100), 2), $this->currency->precision));
/*Test 16-08-2021*/
$discount = ($this->item->line_total * ($this->item->discount / 100));
$this->setLineTotal($this->formatValue(($this->getLineTotal() - $discount), $this->currency->precision));
/*Test 16-08-2021*/
//replaces the following
// $this->setLineTotal($this->getLineTotal() - $this->formatValue(round($this->item->line_total * ($this->item->discount / 100), 2), $this->currency->precision));
} }
$this->item->is_amount_discount = $this->invoice->is_amount_discount; $this->item->is_amount_discount = $this->invoice->is_amount_discount;

View File

@ -433,9 +433,14 @@ class CompanyGatewayController extends BaseController
*/ */
public function destroy(DestroyCompanyGatewayRequest $request, CompanyGateway $company_gateway) public function destroy(DestroyCompanyGatewayRequest $request, CompanyGateway $company_gateway)
{ {
$company_gateway->driver(new Client)
->disconnect();
$company_gateway->delete(); $company_gateway->delete();
return $this->itemResponse($company_gateway->fresh()); return $this->itemResponse($company_gateway->fresh());
} }
/** /**

View File

@ -795,6 +795,10 @@ class InvoiceController extends BaseController
public function downloadPdf($invitation_key) public function downloadPdf($invitation_key)
{ {
$invitation = $this->invoice_repo->getInvitationByKey($invitation_key); $invitation = $this->invoice_repo->getInvitationByKey($invitation_key);
if(!$invitation)
return response()->json(["message" => "no record found"], 400);
$contact = $invitation->contact; $contact = $invitation->contact;
$invoice = $invitation->invoice; $invoice = $invitation->invoice;

View File

@ -81,6 +81,8 @@ class StripeConnectController extends BaseController
} }
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = Company::where('company_key', $request->getTokenContent()['company_key'])->first(); $company = Company::where('company_key', $request->getTokenContent()['company_key'])->first();
$company_gateway = CompanyGateway::query() $company_gateway = CompanyGateway::query()

View File

@ -14,10 +14,14 @@ namespace App\Http\Controllers;
use App\Jobs\Util\ImportStripeCustomers; use App\Jobs\Util\ImportStripeCustomers;
use App\Jobs\Util\StripeUpdatePaymentMethods; use App\Jobs\Util\StripeUpdatePaymentMethods;
use App\Models\Client;
use App\Models\CompanyGateway;
class StripeController extends BaseController class StripeController extends BaseController
{ {
private $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23'];
public function update() public function update()
{ {
if(auth()->user()->isAdmin()) if(auth()->user()->isAdmin())
@ -50,4 +54,22 @@ class StripeController extends BaseController
return response()->json(['message' => 'Unauthorized'], 403); return response()->json(['message' => 'Unauthorized'], 403);
} }
public function verify()
{
if(auth()->user()->isAdmin())
{
$company_gateway = CompanyGateway::where('company_id', auth()->user()->company()->id)
->where('is_deleted',0)
->whereIn('gateway_key', $this->stripe_keys)
->first();
return $company_gateway->driver(new Client)->verifyConnect();
}
return response()->json(['message' => 'Unauthorized'], 403);
}
} }

View File

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

View File

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

View File

@ -49,6 +49,7 @@ class ImportStripeCustomers implements ShouldQueue
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
$cgs = CompanyGateway::where('company_id', $this->company->id) $cgs = CompanyGateway::where('company_id', $this->company->id)
->where('is_deleted',0)
->whereIn('gateway_key', $this->stripe_keys) ->whereIn('gateway_key', $this->stripe_keys)
->get(); ->get();

View File

@ -175,7 +175,8 @@ class ReminderJob implements ShouldQueue
$invoice->line_items = $invoice_items; $invoice->line_items = $invoice_items;
/**Refresh Invoice values*/ /**Refresh Invoice values*/
$invoice = $invoice->calc()->getInvoice()->save(); $invoice->calc()->getInvoice()->save();
$invoice->fresh();
$invoice->service()->deletePdf(); $invoice->service()->deletePdf();
nlog("adjusting client balance and invoice balance by ". ($invoice->balance - $temp_invoice_balance)); nlog("adjusting client balance and invoice balance by ". ($invoice->balance - $temp_invoice_balance));

View File

@ -53,7 +53,8 @@ class SupportMessageSent extends Mailable
$account = auth()->user()->account; $account = auth()->user()->account;
$priority = ''; $priority = '';
$plan = $account->plan ?: ''; $plan = $account->plan ?: 'customer support';
$plan = ucfirst($plan);
if(strlen($plan) >1) if(strlen($plan) >1)
$priority = '[PRIORITY] '; $priority = '[PRIORITY] ';
@ -63,9 +64,9 @@ class SupportMessageSent extends Mailable
$db = str_replace("db-ninja-", "", $company->db); $db = str_replace("db-ninja-", "", $company->db);
if(Ninja::isHosted()) 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 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()) return $this->from(config('mail.from.address'), $user->present()->name())
->replyTo($user->email, $user->present()->name()) ->replyTo($user->email, $user->present()->name())

View File

@ -115,7 +115,7 @@ class AuthorizeCreditCard
]; ];
$logger_message = [ $logger_message = [
'server_response' => $response->getTransactionResponse()->getTransId(), 'server_response' => $response->getTransId(),
'data' => $this->formatGatewayResponse($data, $vars), 'data' => $this->formatGatewayResponse($data, $vars),
]; ];
@ -130,11 +130,11 @@ class AuthorizeCreditCard
]; ];
$logger_message = [ $logger_message = [
'server_response' => $response->getTransactionResponse()->getTransId(), 'server_response' => $response->getTransId(),
'data' => $this->formatGatewayResponse($data, $vars), 'data' => $this->formatGatewayResponse($data, $vars),
]; ];
PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransactionResponse()->getTransId(), $this->authorize->client->company, $amount); PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransId(), $this->authorize->client->company, $amount);
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company); SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company);
@ -147,8 +147,8 @@ class AuthorizeCreditCard
{ {
$response = $data['response']; $response = $data['response'];
if ($response != null && $response->getMessages()->getResultCode() == 'Ok') { // if ($response != null && $response->getMessages()->getResultCode() == 'Ok') {
if ($response != null && $response->getMessages() != null) {
return $this->processSuccessfulResponse($data, $request); return $this->processSuccessfulResponse($data, $request);
} }
@ -165,7 +165,7 @@ class AuthorizeCreditCard
$payment_record['amount'] = $amount; $payment_record['amount'] = $amount;
$payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER; $payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER;
$payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD; $payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD;
$payment_record['transaction_reference'] = $response->getTransactionResponse()->getTransId(); $payment_record['transaction_reference'] = $response->getTransId();
$payment = $this->authorize->createPayment($payment_record); $payment = $this->authorize->createPayment($payment_record);
@ -183,7 +183,7 @@ class AuthorizeCreditCard
]; ];
$logger_message = [ $logger_message = [
'server_response' => $data['response']->getTransactionResponse()->getTransId(), 'server_response' => $data['response']->getTransId(),
'data' => $this->formatGatewayResponse($data, $vars), 'data' => $this->formatGatewayResponse($data, $vars),
]; ];
@ -204,9 +204,39 @@ class AuthorizeCreditCard
$response = $data['response']; $response = $data['response'];
$amount = array_key_exists('amount_with_fee', $data) ? $data['amount_with_fee'] : 0; $amount = array_key_exists('amount_with_fee', $data) ? $data['amount_with_fee'] : 0;
PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransactionResponse()->getTransId(), $this->authorize->client->company, $data['amount_with_fee']); $code = "Error";
$description = "There was an error processing the payment";
if ($response->getErrors() != null) {
$code = $response->getErrors()[0]->getErrorCode();
$description = $response->getErrors()[0]->getErrorText();
}
PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransId(), $this->authorize->client->company, $amount);
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->firstOrFail();
$vars = [
'invoices' => $payment_hash->invoices(),
'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total,
];
$logger_message = [
'server_response' => $response->getErrors(),
'data' => $this->formatGatewayResponse($data, $vars),
];
SystemLogger::dispatch(
$logger_message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_AUTHORIZE,
$this->authorize->client,
$this->authorize->client->company,
);
throw new PaymentFailed($description, $code);
throw new \Exception(ctrans('texts.error_title'));
} }
private function formatGatewayResponse($data, $vars) private function formatGatewayResponse($data, $vars)
@ -216,15 +246,20 @@ class AuthorizeCreditCard
$code = ''; $code = '';
$description = ''; $description = '';
if($response->getTransactionResponse()->getMessages() !== null){ if($response->getMessages() !== null){
$code = $response->getTransactionResponse()->getMessages()[0]->getCode(); $code = $response->getMessages()[0]->getCode();
$description = $response->getTransactionResponse()->getMessages()[0]->getDescription(); $description = $response->getMessages()[0]->getDescription();
}
if ($response->getErrors() != null) {
$code = $response->getErrors()[0]->getErrorCode();
$description = $response->getErrors()[0]->getErrorText();
} }
return [ return [
'transaction_reference' => $response->getTransactionResponse()->getTransId(), 'transaction_reference' => $response->getTransId(),
'amount' => $vars['amount'], 'amount' => $vars['amount'],
'auth_code' => $response->getTransactionResponse()->getAuthCode(), 'auth_code' => $response->getAuthCode(),
'code' => $code, 'code' => $code,
'description' => $description, 'description' => $description,
'invoices' => $vars['invoices'], 'invoices' => $vars['invoices'],

View File

@ -93,7 +93,7 @@ class ChargePaymentProfile
} }
return [ return [
'response' => $response, 'response' => $tresponse,
'amount' => $amount, 'amount' => $amount,
'profile_id' => $profile_id, 'profile_id' => $profile_id,
'payment_profile_id' => $payment_profile_id, 'payment_profile_id' => $payment_profile_id,

View File

@ -631,4 +631,9 @@ class BaseDriver extends AbstractPaymentDriver
return $types; return $types;
} }
public function disconnect()
{
return true;
}
} }

View File

@ -180,7 +180,7 @@ class CreditCard
'transaction_reference' => $payment->id, 'transaction_reference' => $payment->id,
]; ];
$payment_record = $this->mollie->createPayment($data, Payment::STATUS_COMPLETED); $payment_record = $this->mollie->createPayment($data, $payment->status === 'paid' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING);
SystemLogger::dispatch( SystemLogger::dispatch(
['response' => $payment, 'data' => $data], ['response' => $payment, 'data' => $data],

View File

@ -273,6 +273,9 @@ class MolliePaymentDriver extends BaseDriver
public function processWebhookRequest(PaymentWebhookRequest $request) public function processWebhookRequest(PaymentWebhookRequest $request)
{ {
// Allow app to catch up with webhook request.
sleep(2);
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'id' => ['required', 'starts_with:tr'], 'id' => ['required', 'starts_with:tr'],
]); ]);

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

@ -0,0 +1,76 @@
<?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\Connect;
use App\Exceptions\PaymentFailed;
use App\Exceptions\StripeConnectFailure;
use App\Http\Requests\ClientPortal\PaymentMethod\VerifyPaymentMethodRequest;
use App\Http\Requests\Request;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Mail\Gateways\ACHVerificationNotification;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Traits\MakesHash;
use Exception;
use Stripe\Customer;
use Stripe\Exception\CardException;
use Stripe\Exception\InvalidRequestException;
class Verify
{
use MakesHash;
/** @var StripePaymentDriver */
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe = $stripe;
}
public function run()
{
$this->stripe->init();
if($this->stripe->stripe_connect && strlen($this->stripe->company_gateway->getConfigField('account_id')) < 1)
throw new StripeConnectFailure('Stripe Connect has not been configured');
$customers = Customer::all([], $this->stripe->stripe_connect_auth);
$stripe_customers = $this->stripe->company_gateway->client_gateway_tokens->map(function ($cgt){
$customer = Customer::retrieve($cgt->gateway_customer_reference, $this->stripe->stripe_connect_auth);
return [
'customer' => $cgt->gateway_customer_reference,
'record' => $customer
];
});
$data = [
'stripe_customer_count' => count($customers),
'stripe_customers' => $stripe_customers,
];
return response()->json($data, 200);
}
}

View File

@ -108,7 +108,7 @@ class CreditCard
return $this->processUnsuccessfulPayment($server_response); 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); $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)]); 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); PaymentFailureMailer::dispatch($this->stripe->client, $server_response->cancellation_reason, $this->stripe->client->company, $server_response->amount);

View File

@ -12,6 +12,7 @@
namespace App\PaymentDrivers\Stripe; namespace App\PaymentDrivers\Stripe;
use App\Exceptions\StripeConnectFailure;
use App\Factory\ClientContactFactory; use App\Factory\ClientContactFactory;
use App\Factory\ClientFactory; use App\Factory\ClientFactory;
use App\Factory\ClientGatewayTokenFactory; use App\Factory\ClientGatewayTokenFactory;
@ -22,6 +23,7 @@ use App\Models\Currency;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\PaymentDrivers\StripePaymentDriver; use App\PaymentDrivers\StripePaymentDriver;
use App\PaymentDrivers\Stripe\UpdatePaymentMethods; use App\PaymentDrivers\Stripe\UpdatePaymentMethods;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Stripe\Customer; use Stripe\Customer;
use Stripe\PaymentMethod; use Stripe\PaymentMethod;
@ -29,6 +31,7 @@ use Stripe\PaymentMethod;
class ImportCustomers class ImportCustomers
{ {
use MakesHash; use MakesHash;
use GeneratesCounter;
/** @var StripePaymentDriver */ /** @var StripePaymentDriver */
public $stripe; public $stripe;
@ -48,6 +51,9 @@ class ImportCustomers
$this->update_payment_methods = new UpdatePaymentMethods($this->stripe); $this->update_payment_methods = new UpdatePaymentMethods($this->stripe);
if(strlen($this->stripe->company_gateway->getConfigField('account_id')) < 1)
throw new StripeConnectFailure('Stripe Connect has not been configured');
$customers = Customer::all([], $this->stripe->stripe_connect_auth); $customers = Customer::all([], $this->stripe->stripe_connect_auth);
foreach($customers as $customer) foreach($customers as $customer)
@ -120,6 +126,10 @@ class ImportCustomers
$client->name = property_exists($customer, 'name') ? $customer->name : $customer->email; $client->name = property_exists($customer, 'name') ? $customer->name : $customer->email;
if (!isset($client->number) || empty($client->number)) {
$client->number = $this->getNextClientNumber($client);
}
$client->save(); $client->save();
$contact = ClientContactFactory::create($client->company_id, $client->user_id); $contact = ClientContactFactory::create($client->company_id, $client->user_id);

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

@ -13,6 +13,7 @@
namespace App\PaymentDrivers; namespace App\PaymentDrivers;
use App\Exceptions\PaymentFailed; use App\Exceptions\PaymentFailed;
use App\Exceptions\StripeConnectFailure;
use App\Factory\PaymentFactory; use App\Factory\PaymentFactory;
use App\Http\Requests\Payments\PaymentWebhookRequest; use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Http\Requests\Request; use App\Http\Requests\Request;
@ -24,7 +25,9 @@ use App\Models\PaymentHash;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\Stripe\ACH; use App\PaymentDrivers\Stripe\ACH;
use App\PaymentDrivers\Stripe\Alipay; use App\PaymentDrivers\Stripe\Alipay;
use App\PaymentDrivers\Stripe\ApplePay;
use App\PaymentDrivers\Stripe\Charge; use App\PaymentDrivers\Stripe\Charge;
use App\PaymentDrivers\Stripe\Connect\Verify;
use App\PaymentDrivers\Stripe\CreditCard; use App\PaymentDrivers\Stripe\CreditCard;
use App\PaymentDrivers\Stripe\ImportCustomers; use App\PaymentDrivers\Stripe\ImportCustomers;
use App\PaymentDrivers\Stripe\SOFORT; use App\PaymentDrivers\Stripe\SOFORT;
@ -70,7 +73,7 @@ class StripePaymentDriver extends BaseDriver
GatewayType::BANK_TRANSFER => ACH::class, GatewayType::BANK_TRANSFER => ACH::class,
GatewayType::ALIPAY => Alipay::class, GatewayType::ALIPAY => Alipay::class,
GatewayType::SOFORT => SOFORT::class, GatewayType::SOFORT => SOFORT::class,
GatewayType::APPLE_PAY => 1, // TODO GatewayType::APPLE_PAY => ApplePay::class,
GatewayType::SEPA => 1, // TODO GatewayType::SEPA => 1, // TODO
]; ];
@ -86,7 +89,10 @@ class StripePaymentDriver extends BaseDriver
{ {
Stripe::setApiKey(config('ninja.ninja_stripe_key')); Stripe::setApiKey(config('ninja.ninja_stripe_key'));
if(strlen($this->company_gateway->getConfigField('account_id')) > 1)
$this->stripe_connect_auth = ["stripe_account" => $this->company_gateway->getConfigField('account_id')]; $this->stripe_connect_auth = ["stripe_account" => $this->company_gateway->getConfigField('account_id')];
else
throw new StripeConnectFailure('Stripe Connect has not been configured');
} }
else else
{ {
@ -390,6 +396,9 @@ class StripePaymentDriver extends BaseDriver
public function processWebhookRequest(PaymentWebhookRequest $request) public function processWebhookRequest(PaymentWebhookRequest $request)
{ {
// Allow app to catch up with webhook request.
sleep(2);
if ($request->type === 'charge.succeeded' || $request->type === 'source.chargeable') { if ($request->type === 'charge.succeeded' || $request->type === 'source.chargeable') {
foreach ($request->data as $transaction) { foreach ($request->data as $transaction) {
$payment = Payment::query() $payment = Payment::query()
@ -533,4 +542,34 @@ class StripePaymentDriver extends BaseDriver
//match clients based on the gateway_customer_reference column //match clients based on the gateway_customer_reference column
} }
public function verifyConnect()
{
return (new Verify($this))->run();
}
public function disconnect()
{
if(!$this->stripe_connect)
return true;
if(!strlen($this->company_gateway->getConfigField('account_id')) > 1 )
throw new StripeConnectFailure('Stripe Connect has not been configured');
Stripe::setApiKey(config('ninja.ninja_stripe_key'));
try {
\Stripe\OAuth::deauthorize([
'client_id' => config('ninja.ninja_stripe_client_id'),
'stripe_user_id' => $this->company_gateway->getConfigField('account_id'),
]);
}
catch(\Exception $e){
throw new StripeConnectFailure('Unable to disconnect Stripe Connect');
}
return response()->json(['message' => 'success'], 200);
}
} }

View File

@ -65,6 +65,7 @@ class HandleReversal extends AbstractService
/* Generate a credit for the $total_paid amount */ /* Generate a credit for the $total_paid amount */
$notes = 'Credit for reversal of '.$this->invoice->number; $notes = 'Credit for reversal of '.$this->invoice->number;
$credit = false;
if ($total_paid > 0) { if ($total_paid > 0) {

View File

@ -450,7 +450,7 @@ class Design extends BaseDesign
['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']], ['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']],
]], ]],
]], ]],
['element' => 'div', 'properties' => ['class' => 'totals-table-right-side'], 'elements' => []], ['element' => 'div', 'properties' => ['class' => 'totals-table-right-side', 'dir' => '$dir'], 'elements' => []],
]; ];
if ($this->type == 'delivery_note') { if ($this->type == 'delivery_note') {

View File

@ -433,6 +433,9 @@ class HtmlEngine
$data['$paymentLink'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')]; $data['$paymentLink'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_payment').'</a>', 'label' => ctrans('texts.view_payment')];
$data['$portalButton'] = &$data['$paymentLink']; $data['$portalButton'] = &$data['$paymentLink'];
$data['$dir'] = ['value' => optional($this->client->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];
$data['$dir_text_align'] = ['value' => optional($this->client->language())->locale === 'ar' ? 'right' : 'left', 'label' => ''];
$arrKeysLength = array_map('strlen', array_keys($data)); $arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data); array_multisort($arrKeysLength, SORT_DESC, $data);

View File

@ -55,6 +55,13 @@ trait CleanLineItems
//always cast the value! //always cast the value!
$item[$key] = BaseSettings::castAttribute(InvoiceItem::$casts[$key], $item[$key]); $item[$key] = BaseSettings::castAttribute(InvoiceItem::$casts[$key], $item[$key]);
} }
if(array_key_exists('type_id', $item) && $item['type_id'] == '0')
$item['type_id'] = '1';
if(!array_key_exists('type_id', $item))
$item['type_id'] = '1';
} }
if (array_key_exists('id', $item)) { if (array_key_exists('id', $item)) {

View File

@ -44,6 +44,7 @@
"fakerphp/faker": "^1.14", "fakerphp/faker": "^1.14",
"fideloper/proxy": "^4.2", "fideloper/proxy": "^4.2",
"fruitcake/laravel-cors": "^2.0", "fruitcake/laravel-cors": "^2.0",
"gocardless/gocardless-pro": "^4.12",
"google/apiclient": "^2.7", "google/apiclient": "^2.7",
"guzzlehttp/guzzle": "^7.0.1", "guzzlehttp/guzzle": "^7.0.1",
"hashids/hashids": "^4.0", "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "275a9dd3910b6ec79607b098406dc6c7", "content-hash": "bcd9405b1978cef268d732794883e91d",
"packages": [ "packages": [
{ {
"name": "asm/php-ansible", "name": "asm/php-ansible",
@ -2221,6 +2221,61 @@
], ],
"time": "2021-04-26T11:24:25+00:00" "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", "name": "google/apiclient",
"version": "v2.10.1", "version": "v2.10.1",
@ -14972,5 +15027,5 @@
"platform-dev": { "platform-dev": {
"php": "^7.3|^7.4|^8.0" "php": "^7.3|^7.4|^8.0"
}, },
"plugin-api-version": "2.1.0" "plugin-api-version": "2.0.0"
} }

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.2.18', 'app_version' => '5.2.19',
'app_tag' => '5.2.18', 'app_tag' => '5.2.19',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),

View File

@ -1 +1 @@
{"assets/images/google-icon.png":["assets/images/google-icon.png"],"assets/images/icon.png":["assets/images/icon.png"],"assets/images/logo.png":["assets/images/logo.png"],"assets/images/payment_types/ach.png":["assets/images/payment_types/ach.png"],"assets/images/payment_types/amex.png":["assets/images/payment_types/amex.png"],"assets/images/payment_types/carteblanche.png":["assets/images/payment_types/carteblanche.png"],"assets/images/payment_types/dinerscard.png":["assets/images/payment_types/dinerscard.png"],"assets/images/payment_types/discover.png":["assets/images/payment_types/discover.png"],"assets/images/payment_types/jcb.png":["assets/images/payment_types/jcb.png"],"assets/images/payment_types/laser.png":["assets/images/payment_types/laser.png"],"assets/images/payment_types/maestro.png":["assets/images/payment_types/maestro.png"],"assets/images/payment_types/mastercard.png":["assets/images/payment_types/mastercard.png"],"assets/images/payment_types/other.png":["assets/images/payment_types/other.png"],"assets/images/payment_types/paypal.png":["assets/images/payment_types/paypal.png"],"assets/images/payment_types/solo.png":["assets/images/payment_types/solo.png"],"assets/images/payment_types/switch.png":["assets/images/payment_types/switch.png"],"assets/images/payment_types/unionpay.png":["assets/images/payment_types/unionpay.png"],"assets/images/payment_types/visa.png":["assets/images/payment_types/visa.png"],"packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf":["packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf"]} {"assets/images/google_logo.png":["assets/images/google_logo.png"],"assets/images/icon.png":["assets/images/icon.png"],"assets/images/logo_dark.png":["assets/images/logo_dark.png"],"assets/images/logo_light.png":["assets/images/logo_light.png"],"assets/images/payment_types/ach.png":["assets/images/payment_types/ach.png"],"assets/images/payment_types/amex.png":["assets/images/payment_types/amex.png"],"assets/images/payment_types/carteblanche.png":["assets/images/payment_types/carteblanche.png"],"assets/images/payment_types/dinerscard.png":["assets/images/payment_types/dinerscard.png"],"assets/images/payment_types/discover.png":["assets/images/payment_types/discover.png"],"assets/images/payment_types/jcb.png":["assets/images/payment_types/jcb.png"],"assets/images/payment_types/laser.png":["assets/images/payment_types/laser.png"],"assets/images/payment_types/maestro.png":["assets/images/payment_types/maestro.png"],"assets/images/payment_types/mastercard.png":["assets/images/payment_types/mastercard.png"],"assets/images/payment_types/other.png":["assets/images/payment_types/other.png"],"assets/images/payment_types/paypal.png":["assets/images/payment_types/paypal.png"],"assets/images/payment_types/solo.png":["assets/images/payment_types/solo.png"],"assets/images/payment_types/switch.png":["assets/images/payment_types/switch.png"],"assets/images/payment_types/unionpay.png":["assets/images/payment_types/unionpay.png"],"assets/images/payment_types/visa.png":["assets/images/payment_types/visa.png"],"packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf":["packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf"]}

View File

@ -5198,8 +5198,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
cached_network_image cached_network_image
cached_network_image_platform_interface
cached_network_image_web
The MIT License (MIT) The MIT License (MIT)
@ -11935,6 +11933,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
js js
markdown
Copyright 2012, the Dart project authors. All rights reserved. Copyright 2012, the Dart project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@ -13795,6 +13794,31 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
linkify
MIT License
Copyright (c) 2019 Charles-William Crete
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
material_design_icons_flutter material_design_icons_flutter
@ -17105,6 +17129,17 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
--------------------------------------------------------------------------------
super_editor
Copyright (c) 2021 Superlist, SuperDeclarative! and the contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
synchronized synchronized

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -19,21 +19,22 @@ const RESOURCES = {
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024", "assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71", "assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541", "assets/assets/images/icon.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3", "assets/assets/images/google_logo.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/logo.png": "e5f46d5a78e226e7a9553d4ca6f69219", "assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
"assets/AssetManifest.json": "753bba1dee0531d5fad970b5ce1d296d", "assets/assets/images/logo_dark.png": "a233ed1d4d0f7414bf97a9a10f11fb0a",
"assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f", "assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/fonts/MaterialIcons-Regular.otf": "4e6447691c9509f7acdbf8a931a85ca1", "assets/fonts/MaterialIcons-Regular.otf": "4e6447691c9509f7acdbf8a931a85ca1",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296", "assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"assets/NOTICES": "f44f710ef9af0b68d977d458631873e1", "assets/NOTICES": "4d4db4724f228190d45706dc2890bfff",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed", "icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35", "icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"favicon.png": "dca91c54388f52eded692718d5a98b8b", "favicon.png": "dca91c54388f52eded692718d5a98b8b",
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3", "manifest.json": "17bae385e59f59be709280b542203f8e",
"version.json": "46d4015fc9abcefe5371cafcf2084173", "version.json": "46d4015fc9abcefe5371cafcf2084173",
"favicon.ico": "51636d3a390451561744c42188ccd628", "favicon.ico": "51636d3a390451561744c42188ccd628",
"main.dart.js": "7bcab146a5f6ade3cd027cc9e429f732", "main.dart.js": "870ee0de972245c17e9db71991aecfc1",
"/": "d389ab59423a76b2aaaa683ed382c78e" "/": "24783ba7c9b4ad6d85740d03acd4bf25"
}; };
// The application shell files that are downloaded before a service worker can // The application shell files that are downloaded before a service worker can

231637
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

231209
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@
"display": "standalone", "display": "standalone",
"background_color": "#0175C2", "background_color": "#0175C2",
"theme_color": "#0175C2", "theme_color": "#0175C2",
"description": "The leading open-source invoicing app", "description": "Invoice Clients, Track Work-Time, Get Paid Online.",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"prefer_related_applications": true, "prefer_related_applications": true,
"related_applications": [ "related_applications": [
@ -19,7 +19,7 @@
], ],
"icons": [ "icons": [
{ {
"src": "images/logo.png", "src": "images/icon.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }

View File

@ -4257,7 +4257,7 @@ $LANG = array(
'user_duplicate_error' => 'Cannot add the same user to the same company', 'user_duplicate_error' => 'Cannot add the same user to the same company',
'user_cross_linked_error' => 'User exists but cannot be crossed linked to multiple accounts', 'user_cross_linked_error' => 'User exists but cannot be crossed linked to multiple accounts',
'ach_verification_notification_label' => 'ACH verification', 'ach_verification_notification_label' => 'ACH verification',
'ach_verification_notification' => 'Connecting bank accounts require verification. Stripe will automatically sends two small deposits for this purpose. These deposits take 1-2 business days to appear on the customer\'s online statement.', 'ach_verification_notification' => 'Connecting bank accounts require verification. Payment gateway will automatically send two small deposits for this purpose. These deposits take 1-2 business days to appear on the customer\'s online statement.',
'login_link_requested_label' => 'Login link requested', 'login_link_requested_label' => 'Login link requested',
'login_link_requested' => 'There was a request to login using link. If you did not request this, it\'s safe to ignore it.', 'login_link_requested' => 'There was a request to login using link. If you did not request this, it\'s safe to ignore it.',
'invoices_backup_subject' => 'Your invoices are ready for download', 'invoices_backup_subject' => 'Your invoices are ready for download',

View File

@ -156,7 +156,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -305,7 +305,7 @@
<div class="entity-details-wrapper-right-side"> <div class="entity-details-wrapper-right-side">
<div class="entity-details-wrapper"> <div class="entity-details-wrapper">
<table id="entity-details"></table> <table id="entity-details" dir="$dir"></table>
</div> </div>
</div> </div>
</div> </div>

View File

@ -193,7 +193,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -293,7 +293,7 @@
<div id="client-details"></div> <div id="client-details"></div>
<div class="entity-details-wrapper"> <div class="entity-details-wrapper">
<table id="entity-details" cellspacing="0"></table> <table id="entity-details" cellspacing="0" dir="$dir"></table>
</div> </div>
</div> </div>

View File

@ -156,7 +156,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -248,7 +248,7 @@
<p class="entity-label">$entity_label</p> <p class="entity-label">$entity_label</p>
<div class="client-and-entity-wrapper"> <div class="client-and-entity-wrapper">
<table id="entity-details" cellspacing="0"></table> <table id="entity-details" cellspacing="0" dir="$dir"></table>
<div id="client-details"></div> <div id="client-details"></div>
</div> </div>

View File

@ -158,7 +158,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -258,7 +258,7 @@
<span>#$entity_number</span> <span>#$entity_number</span>
</h1> </h1>
<table id="entity-details" cellspacing="0"></table> <table id="entity-details" cellspacing="0" dir="$dir"></table>
</div> </div>
<table id="product-table" cellspacing="0"></table> <table id="product-table" cellspacing="0"></table>

View File

@ -152,7 +152,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -261,7 +261,7 @@
<div class="wrapper-right-side"> <div class="wrapper-right-side">
<h2 class="wrapper-info-text">$details_label</h2> <h2 class="wrapper-info-text">$details_label</h2>
<table id="entity-details" cellspacing="0"></table> <table id="entity-details" cellspacing="0" dir="$dir"></table>
</div> </div>
</div> </div>

View File

@ -167,7 +167,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -213,6 +213,16 @@
margin-bottom: 0; margin-bottom: 0;
} }
.entity-details-wrapper > * {
direction: $dir;
}
.entity-details-wrapper {
display: flex;
flex-wrap: wrap;
direction: $dir;
}
/** Useful snippets, uncomment to enable. **/ /** Useful snippets, uncomment to enable. **/
/** Hide company logo **/ /** Hide company logo **/
@ -271,30 +281,31 @@
<h1 class="entity-label">$entity_label</h1> <h1 class="entity-label">$entity_label</h1>
<div class="entity-details-wrapper"> <div class="entity-details-wrapper">
<span> <div>
<span <span class="entity-property-label" data-element="entity-details-wrapper-invoice-number-label">
class="entity-property-label" $entity_number_label:
data-element="entity-details-wrapper-invoice-number-label"
>$entity_number_label:</span
>
<span class="entity-property-value">$entity_number</span>
</span> </span>
<span> <span class="entity-property-value">$entity_number</span>
</div>
<div>
<span class="entity-property-label">$date_label:</span> <span class="entity-property-label">$date_label:</span>
<span class="entity-property-value">$date</span> <span class="entity-property-value">$date</span>
</span> </div>
<span>
<div>
<span class="entity-property-label">$payment_due_label:</span> <span class="entity-property-label">$payment_due_label:</span>
<span class="entity-property-value">$payment_due</span> <span class="entity-property-value">$payment_due</span>
</span> </div>
<span>
<div>
<span class="entity-property-label">$amount_due_label:</span> <span class="entity-property-label">$amount_due_label:</span>
<span <span
class="entity-property-value" class="entity-property-value"
data-element="entity-details-wrapper-amount-due" data-element="entity-details-wrapper-amount-due"
>$amount_due</span >$amount_due</span
> >
</span> </div>
</div> </div>
<table id="product-table" cellspacing="0"></table> <table id="product-table" cellspacing="0"></table>

View File

@ -189,7 +189,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -232,7 +232,7 @@
[data-ref="total_table-footer"] { [data-ref="total_table-footer"] {
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 2rem margin-bottom: 2rem;
} }
/** Repeating header & footer styling. */ /** Repeating header & footer styling. */
@ -307,7 +307,7 @@
<td> <td>
<div class="header-container" id="header"> <div class="header-container" id="header">
<h1 class="company-name">$company.name</h1> <h1 class="company-name">$company.name</h1>
<table id="entity-details" cellspacing="0"></table> <table id="entity-details" cellspacing="0" dir="$dir"></table>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -132,7 +132,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -223,7 +223,7 @@
alt="$company.name logo" alt="$company.name logo"
/> />
<table id="entity-details" cellspacing="0"></table> <table id="entity-details" cellspacing="0" dir="$dir"></table>
</div> </div>
</div> </div>

View File

@ -184,7 +184,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
margin-top: .75rem; margin-top: .75rem;
} }
@ -301,7 +301,7 @@
<div></div> <div></div>
<div class="entity-details-wrapper"> <div class="entity-details-wrapper">
<table id="entity-details" cellspacing="0"></table> <table id="entity-details" cellspacing="0" dir="$dir"></table>
</div> </div>
</div> </div>

View File

@ -190,7 +190,7 @@
} }
#table-totals>.totals-table-right-side>*> :nth-child(1) { #table-totals>.totals-table-right-side>*> :nth-child(1) {
text-align: left; text-align: "$dir_text_align";
padding: 7px; padding: 7px;
} }
@ -278,15 +278,15 @@
<img class="company-logo" src="$company.logo" <img class="company-logo" src="$company.logo"
alt="$company.name logo"> alt="$company.name logo">
<div class="top-right-side-section"> <div class="top-right-side-section">
<section> <div dir="$dir">
<span class="header-payment-due-label">$payment_due_label:</span> <span class="header-payment-due-label">$payment_due_label:</span>
<span>$payment_due</span> <span>$payment_due</span>
</section> </div>
<section> <div dir="$dir">
<span class="header-amount-due-label">$amount_due_label:</span> <span class="header-amount-due-label">$amount_due_label:</span>
<span class="header-amount-due-value">$amount_due</span> <span class="header-amount-due-value">$amount_due</span>
</section> </div>
</div> </div>
</div> </div>
</div> </div>

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

View File

@ -84,7 +84,7 @@
</div> </div>
</div> </div>
@if(optional($payment_method->meta)->state === 'unauthorized' && $payment_method->gateway_type_id === \App\Models\GatewayType::BANK_TRANSFER) @if((optional($payment_method->meta)->state === 'unauthorized' || optional($payment_method->meta)->state === 'pending') && $payment_method->gateway_type_id === \App\Models\GatewayType::BANK_TRANSFER)
<div class="mt-4 mb-4 bg-white shadow sm:rounded-lg"> <div class="mt-4 mb-4 bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between"> <div class="sm:flex sm:items-start sm:justify-between">

View File

@ -190,6 +190,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('stripe/update_payment_methods', 'StripeController@update')->middleware('password_protected')->name('stripe.update'); Route::post('stripe/update_payment_methods', 'StripeController@update')->middleware('password_protected')->name('stripe.update');
Route::post('stripe/import_customers', 'StripeController@import')->middleware('password_protected')->name('stripe.import'); Route::post('stripe/import_customers', 'StripeController@import')->middleware('password_protected')->name('stripe.import');
Route::post('stripe/verify', 'StripeController@verify')->middleware('password_protected')->name('stripe.verify');
Route::resource('subscriptions', 'SubscriptionController'); Route::resource('subscriptions', 'SubscriptionController');
Route::post('subscriptions/bulk', 'SubscriptionController@bulk')->name('subscriptions.bulk'); Route::post('subscriptions/bulk', 'SubscriptionController@bulk')->name('subscriptions.bulk');

View File

@ -158,6 +158,7 @@ class CompanyLedgerTest extends TestCase
$item = []; $item = [];
$item['quantity'] = 1; $item['quantity'] = 1;
$item['cost'] = 10; $item['cost'] = 10;
$item['type_id'] = "1";
$line_items[] = $item; $line_items[] = $item;