1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 21:22:58 +01:00

Merge pull request #4799 from turbo124/v5-stable

5.0.56
This commit is contained in:
David Bomba 2021-01-30 00:05:21 +11:00 committed by GitHub
commit 0c2e18beb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 45015 additions and 44678 deletions

View File

@ -4,12 +4,18 @@
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable)
[![codecov](https://codecov.io/gh/invoiceninja/invoiceninja/branch/v2/graph/badge.svg)](https://codecov.io/gh/invoiceninja/invoiceninja)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d39acb4bf0f74a0698dc77f382769ba5)](https://www.codacy.com/app/turbo124/invoiceninja?utm_source=github.com&utm_medium=referral&utm_content=invoiceninja/invoiceninja&utm_campaign=Badge_Grade)
# Invoice Ninja version 5 is in Beta!
# Invoice Ninja version 5.1 RC2!
We will be using the lessons learnt in Invoice Ninja 4.0 to build a bigger better platform to work from. If you would like to contribute to the project we will gladly accept contributions for code, user guides, bug tracking and feedback! Please consider the following guidelines prior to submitting a pull request:
Invoice Ninja version 5.1 has now reached Release Candidate 2!
What does this mean exactly? We consider this version _almost_ stable. There may be some remaining small issues which we would love to get feedback on. We would really appreciate the community booting up this version and attempting the migration from their Invoice Ninja V4 application and inspect the migrated data.
We'd also like feedback on any issues that you can see, and help us nail down the few remaining issues before Version 5 graduates to Stable Gold Release.
Please note we do not consider this version ready for production use, please stick with your V4 installation for your production clients!
## Quick Start
@ -17,13 +23,10 @@ Currently the client portal and API are of alpha quality, to get started:
```bash
git clone https://github.com/invoiceninja/invoiceninja.git
git checkout v2
git checkout v5-stable
cp .env.example .env
cp .env.dusk.example .env.dusk.local
php artisan key:generate
composer update
npm i
npm run production
```
Please Note: Your APP_KEY in the .env file is used to encrypt data, if you lose this you will not be able to run the application.
@ -33,7 +36,7 @@ Run if you want to load sample data, remember to configure .env
php artisan migrate:fresh --seed && php artisan db:seed && php artisan ninja:create-test-data
```
To Run the web server
To run the web server
```
php artisan serve
```

View File

@ -1 +1 @@
5.0.55
5.0.56

View File

@ -232,11 +232,11 @@ class CompanySettings extends BaseSettings
public $id_number = ''; //@implemented
public $page_size = 'A4'; //Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6
public $font_size = 9; //@implemented
public $font_size = 7; //@implemented
public $primary_font = 'Roboto';
public $secondary_font = 'Roboto';
public $primary_color = '#4caf50';
public $secondary_color = '#2196f3';
public $primary_color = '#142cb5';
public $secondary_color = '#7081e0';
public $hide_paid_to_date = false; //@TODO where?
public $embed_documents = false; //@TODO where?

View File

@ -184,7 +184,9 @@ class BaseController extends Controller
protected function refreshResponse($query)
{
if (auth()->user()->getCompany()->is_large)
$user = auth()->user();
if ($user->getCompany()->is_large)
$this->manager->parseIncludes($this->mini_load);
else
$this->manager->parseIncludes($this->first_load);
@ -200,74 +202,145 @@ class BaseController extends Controller
$transformer = new $this->entity_transformer($this->serializer);
$updated_at = request()->has('updated_at') ? request()->input('updated_at') : 0;
// if (auth()->user()->getCompany()->is_large && ! request()->has('updated_at')) {
// return response()->json(['message' => ctrans('texts.large_account_update_parameter'), 'errors' =>[]], 401);
// }
$updated_at = date('Y-m-d H:i:s', $updated_at);
$query->with(
[
'company' => function ($query) use ($updated_at) {
'company' => function ($query) use ($updated_at, $user) {
$query->whereNotNull('updated_at')->with('documents');
},
'company.clients' => function ($query) use ($updated_at) {
'company.clients' => function ($query) use ($updated_at, $user) {
$query->where('clients.updated_at', '>=', $updated_at)->with('contacts.company', 'gateway_tokens', 'documents');
if(!$user->hasPermission('view_client'))
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
},
'company.company_gateways' => function ($query) {
'company.company_gateways' => function ($query) use ($user) {
$query->whereNotNull('updated_at');
if(!$user->isAdmin())
$query->where('company_gateways.user_id', $user->id);
},
'company.credits'=> function ($query) use ($updated_at) {
'company.credits'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_credit'))
$query->where('credits.user_id', $user->id)->orWhere('credits.assigned_user_id', $user->id);
},
'company.designs'=> function ($query) use ($updated_at) {
'company.designs'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('company');
if(!$user->isAdmin())
$query->where('designs.user_id', $user->id);
},
'company.documents'=> function ($query) use ($updated_at) {
'company.documents'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
},
'company.expenses'=> function ($query) use ($updated_at) {
'company.expenses'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_expense'))
$query->where('expenses.user_id', $user->id)->orWhere('expenses.assigned_user_id', $user->id);
},
'company.groups' => function ($query) use ($updated_at) {
'company.groups' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('group_settings.user_id', $user->id);
},
'company.invoices'=> function ($query) use ($updated_at) {
'company.invoices'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_invoice'))
$query->where('invoices.user_id', $user->id)->orWhere('invoices.assigned_user_id', $user->id);
},
'company.payments'=> function ($query) use ($updated_at) {
'company.payments'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('paymentables', 'documents');
if(!$user->hasPermission('view_payment'))
$query->where('payments.user_id', $user->id)->orWhere('payments.assigned_user_id', $user->id);
},
'company.payment_terms'=> function ($query) use ($updated_at) {
'company.payment_terms'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('payment_terms.user_id', $user->id);
},
'company.products' => function ($query) use ($updated_at) {
'company.products' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_product'))
$query->where('products.user_id', $user->id)->orWhere('products.assigned_user_id', $user->id);
},
'company.projects'=> function ($query) use ($updated_at) {
'company.projects'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
},
'company.quotes'=> function ($query) use ($updated_at) {
'company.quotes'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_quote'))
$query->where('quotes.user_id', $user->id)->orWhere('quotes.assigned_user_id', $user->id);
},
'company.recurring_invoices'=> function ($query) use ($updated_at) {
'company.recurring_invoices'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_recurring_invoice'))
$query->where('recurring_invoices.user_id', $user->id)->orWhere('recurring_invoices.assigned_user_id', $user->id);
},
'company.tasks'=> function ($query) use ($updated_at) {
'company.tasks'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_task'))
$query->where('tasks.user_id', $user->id)->orWhere('tasks.assigned_user_id', $user->id);
},
'company.tax_rates' => function ($query) use ($updated_at) {
'company.tax_rates' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('tax_rates.user_id', $user->id);
},
'company.vendors'=> function ($query) use ($updated_at) {
'company.vendors'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('contacts', 'documents');
if(!$user->hasPermission('view_vendor'))
$query->where('vendors.user_id', $user->id)->orWhere('vendors.assigned_user_id', $user->id);
},
'company.expense_categories'=> function ($query) use ($updated_at) {
'company.expense_categories'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('expense_categories.user_id', $user->id);
},
'company.task_statuses'=> function ($query) use ($updated_at) {
'company.task_statuses'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('task_statuses.user_id', $user->id);
},
'company.activities'=> function ($query) use($user) {
if(!$user->isAdmin())
$query->where('activities.user_id', $user->id);
}
]
);

View File

@ -90,7 +90,7 @@ class InvoiceController extends Controller
//filter invoices which are payable
$invoices = $invoices->filter(function ($invoice) {
return $invoice->isPayable() && $invoice->balance > 0;
return $invoice->isPayable();
});
//return early if no invoices.

View File

@ -106,7 +106,7 @@ class PaymentController extends Controller
if ($payable_invoices->count() == 0) {
return redirect()
->route('client.invoices.index')
->with(['warning' => 'No payable invoices selected.']);
->with(['message' => 'No payable invoices selected.']);
}
$settings = auth()->user()->client->getMergedSettings();

View File

@ -370,14 +370,25 @@ class UserController extends BaseController
public function update(UpdateUserRequest $request, User $user)
{
$old_email = $user->email;
$old_company_user = $user->company_user;
$old_user = $user;
$new_email = $request->input('email');
$user = $this->user_repo->save($request->all(), $user);
$user = $user->fresh();
if ($old_email != $new_email) {
UserEmailChanged::dispatch($new_email, $old_email, auth()->user()->company());
}
if(
strcasecmp($old_company_user->permissions, $user->company_user->permissions) != 0 ||
$old_company_user->is_admin != $user->company_user->is_admin
){
$user->company_user()->update(["permissions_updated_at" => now()]);
}
event(new UserWasUpdated($user, auth()->user(), auth()->user()->company, Ninja::eventVars()));
return $this->itemResponse($user);

View File

@ -28,6 +28,7 @@ class CompanyUser extends Pivot
* @var array
*/
protected $casts = [
'permissions_updated_at' => 'timestamp',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',

View File

@ -202,15 +202,22 @@ class User extends Authenticatable implements MustVerifyEmail
$this->id = auth()->user()->id;
}
if (request()->header('X-API-TOKEN')) {
return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'company_id', 'id', 'company_id')
->where('company_tokens.token', request()->header('X-API-TOKEN'))
->withTrashed();
} else {
return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'company_id', 'id', 'company_id')
->where('company_user.user_id', $this->id)
->withTrashed();
}
return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'user_id', 'id', 'user_id')
->withTrashed();
// if (request()->header('X-API-TOKEN')) {
// nlog("with an API token");
// nlog(request()->header('X-API-TOKEN'));
// return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'company_id', 'id', 'company_id')
// ->where('company_tokens.token', request()->header('X-API-TOKEN'))
// ->withTrashed();
// } else {
// return $this->hasOneThrough(CompanyUser::class, CompanyToken::class, 'user_id', 'company_id', 'id', 'company_id')
// ->where('company_user.user_id', $this->id)
// ->withTrashed();
// }
}
/**

View File

@ -12,6 +12,7 @@
namespace App\PaymentDrivers\Authorize;
use App\Exceptions\PaymentFailed;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
@ -81,7 +82,14 @@ class AuthorizeCreditCard
private function processTokenPayment($request)
{
$client_gateway_token =ClientGatewayToken::where('token', $request->token)->firstOrFail();
$client_gateway_token = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->token))
->where('company_id', auth('contact')->user()->client->company->id)
->first();
if (!$client_gateway_token) {
throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401);
}
$data = (new ChargePaymentProfile($this->authorize))->chargeCustomerProfile($client_gateway_token->gateway_customer_reference, $client_gateway_token->token, $request->input('amount_with_fee'));
@ -129,7 +137,7 @@ class AuthorizeCreditCard
PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransactionResponse()->getTransId(), $this->authorize->client->company, $amount);
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_AUTHORIZE, $this->authorize->client);
return false;
}
}
@ -156,7 +164,7 @@ class AuthorizeCreditCard
$payment_record = [];
$payment_record['amount'] = $amount;
$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 = $this->authorize->createPayment($payment_record);

View File

@ -69,7 +69,7 @@ class AuthorizePaymentDriver extends BaseDriver
['name' => 'client_name', 'label' => ctrans('texts.name'), 'type' => 'text', 'validation' => 'required|min:2'],
['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required|email:rfc'],
['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'],
['name' => 'client_address_line_2', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'sometimes'],
['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'sometimes'],
['name' => 'client_city', 'label' => ctrans('texts.city'), 'type' => 'text', 'validation' => 'required'],
['name' => 'client_state', 'label' => ctrans('texts.state'), 'type' => 'text', 'validation' => 'required'],
['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'],
@ -103,7 +103,7 @@ class AuthorizePaymentDriver extends BaseDriver
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
{
$this->setPaymentMethod($cgt->gateway_type_id);
return $this->payment_method->tokenBilling($cgt, $payment_hash);

View File

@ -231,7 +231,7 @@ class BaseDriver extends AbstractPaymentDriver
$payment->service()->updateInvoicePayment($this->payment_hash);
if ($this->client->getSetting('client_online_payment_notification'))
if ($this->client->getSetting('client_online_payment_notification'))
$payment->service()->sendEmail();
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));

View File

@ -12,9 +12,12 @@
namespace App\PaymentDrivers\CheckoutCom;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Models\ClientGatewayToken;
use App\PaymentDrivers\CheckoutComPaymentDriver;
use App\Utils\Traits\MakesHash;
use Checkout\Library\Exceptions\CheckoutHttpException;
use Checkout\Models\Payments\IdSource;
use Checkout\Models\Payments\Payment;
@ -25,6 +28,7 @@ use Illuminate\View\View;
class CreditCard
{
use Utilities;
use MakesHash;
/**
* @var CheckoutComPaymentDriver
@ -78,6 +82,15 @@ class CreditCard
{
$this->checkout->init();
$cgt = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->input('token')))
->where('company_id', auth('contact')->user()->client->company->id)
->first();
if (!$cgt) {
throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401);
}
$state = [
'server_response' => json_decode($request->gateway_response),
'value' => $request->value,
@ -90,11 +103,12 @@ class CreditCard
$state = array_merge($state, $request->all());
$state['store_card'] = boolval($state['store_card']);
$state['token'] = $cgt;
$this->checkout->payment_hash->data = array_merge((array) $this->checkout->payment_hash->data, $state);
$this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, $state);
$this->checkout->payment_hash->save();
if ($request->has('token') && !is_null($request->token) && !empty($request->token)) {
if ($request->has('token')) {
return $this->attemptPaymentUsingToken($request);
}
@ -103,7 +117,7 @@ class CreditCard
private function attemptPaymentUsingToken(PaymentResponseRequest $request)
{
$method = new IdSource($this->checkout->payment_hash->data->token);
$method = new IdSource($this->checkout->payment_hash->data->token->token);
return $this->completePayment($method, $request);
}
@ -125,7 +139,7 @@ class CreditCard
$payment->amount = $this->checkout->payment_hash->data->value;
$payment->reference = $this->checkout->payment_hash->data->reference;
$this->checkout->payment_hash->data = array_merge((array) $this->checkout->payment_hash->data, ['checkout_payment_ref' => $payment]);
$this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, ['checkout_payment_ref' => $payment]);
$this->checkout->payment_hash->save();
if ($this->checkout->client->currency()->code === 'EUR') {
@ -155,7 +169,7 @@ class CreditCard
if ($response->status == 'Declined') {
$this->checkout->unWindGatewayFees($this->checkout->payment_hash);
PaymentFailureMailer::dispatch($this->checkout->client, $response->response_summary, $this->checkout->client->company, $this->checkout->payment_hash->data->value);
PaymentFailureMailer::dispatch($this->checkout->client, $response->response_summary, $this->checkout->client->company, $this->checkout->payment_hash->data->value);
return $this->processUnsuccessfulPayment($response);

View File

@ -81,7 +81,7 @@ trait Utilities
return redirect()->route('client.payments.show', ['payment' => $this->getParent()->encodePrimaryKey($payment->id)]);
}
public function processUnsuccessfulPayment(Payment $_payment)
public function processUnsuccessfulPayment(Payment $_payment, $throw_exception = true)
{
PaymentFailureMailer::dispatch(
$this->getParent()->client,
@ -103,7 +103,9 @@ trait Utilities
$this->getParent()->client
);
throw new PaymentFailed($_payment->status, $_payment->http_code);
if ($throw_exception) {
throw new PaymentFailed($_payment->status, $_payment->http_code);
}
}
private function processPendingPayment(Payment $_payment)

View File

@ -12,18 +12,24 @@
namespace App\PaymentDrivers;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\Company;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\CheckoutCom\CreditCard;
use App\PaymentDrivers\CheckoutCom\Utilities;
use App\Utils\Traits\SystemLogTrait;
use Checkout\CheckoutApi;
use Checkout\Library\Exceptions\CheckoutHttpException;
use Checkout\Models\Payments\IdSource;
use Checkout\Models\Payments\Refund;
use Exception;
@ -96,8 +102,8 @@ class CheckoutComPaymentDriver extends BaseDriver
public function init()
{
$config = [
'secret' => $this->company_gateway->getConfigField('secretApiKey'),
'public' => $this->company_gateway->getConfigField('publicApiKey'),
'secret' => $this->company_gateway->getConfigField('secretApiKey'),
'public' => $this->company_gateway->getConfigField('publicApiKey'),
'sandbox' => $this->company_gateway->getConfigField('testMode'),
];
@ -108,7 +114,7 @@ class CheckoutComPaymentDriver extends BaseDriver
/**
* Process different view depending on payment type
* @param int $gateway_type_id The gateway type
* @param int $gateway_type_id The gateway type
* @return string The view string
*/
public function viewForType($gateway_type_id)
@ -132,7 +138,7 @@ class CheckoutComPaymentDriver extends BaseDriver
/**
* Payment View
*
* @param array $data Payment data array
* @param array $data Payment data array
* @return view The payment view
*/
public function processPaymentView(array $data)
@ -143,7 +149,7 @@ class CheckoutComPaymentDriver extends BaseDriver
/**
* Process the payment response
*
* @param Request $request The payment request
* @param Request $request The payment request
* @return view The payment response view
*/
public function processPaymentResponse($request)
@ -188,7 +194,95 @@ class CheckoutComPaymentDriver extends BaseDriver
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
// ..
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->first();
if ($invoice) {
$description = "Invoice {$invoice->number} for {$amount} for client {$this->client->present()->name()}";
} else {
$description = "Payment with no invoice for amount {$amount} for client {$this->client->present()->name()}";
}
$this->init();
$method = new IdSource($cgt->token);
$payment = new \Checkout\Models\Payments\Payment($method, $this->client->getCurrencyCode());
$payment->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode());
$payment->reference = $cgt->meta->last4 . '-' . now();
$request = new PaymentResponseRequest();
$request->setMethod('POST');
$request->request->add(['payment_hash' => $payment_hash->hash]);
//$this->setPaymentHash($payment_hash);
try {
$response = $this->gateway->payments()->request($payment);
if ($response->status == 'Authorized') {
$this->confirmGatewayFee($request);
$data = [
'payment_method' => $response->source['id'],
'payment_type' => PaymentType::parseCardType(strtolower($response->source['scheme'])),
'amount' => $amount,
'transaction_reference' => $response->id,
];
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $response, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_CHECKOUT,
$this->client
);
return $payment;
}
if ($response->status == 'Declined') {
$this->unWindGatewayFees($payment_hash);
PaymentFailureMailer::dispatch(
$this->client, $response->response_summary,
$this->client->company,
$amount
);
$message = [
'server_response' => $response,
'data' => $payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_CHECKOUT,
$this->client
);
return false;
}
} catch (\Exception | CheckoutHttpException $e) {
$this->unWindGatewayFees($payment_hash);
$message = $e instanceof CheckoutHttpException
? $e->getBody()
: $e->getMessage();
$data = [
'status' => '',
'error_type' => '',
'error_code' => $e->getCode(),
'param' => '',
'message' => $message,
];
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_CHECKOUT, $this->client);
}
}
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)

View File

@ -22,6 +22,7 @@ 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;
@ -29,6 +30,8 @@ use Stripe\Exception\InvalidRequestException;
class ACH
{
use MakesHash;
/** @var StripePaymentDriver */
public $stripe;
@ -109,18 +112,27 @@ class ACH
{
$this->stripe->init();
$source = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->source))
->where('company_id', auth('contact')->user()->client->company->id)
->first();
if (!$source) {
throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401);
}
$state = [
'payment_method' => $request->payment_method_id,
'gateway_type_id' => $request->company_gateway_id,
'amount' => $this->stripe->convertToStripeAmount($request->amount, $this->stripe->client->currency()->precision),
'currency' => $request->currency,
'source' => $request->source,
'customer' => $request->customer,
];
$state = array_merge($state, $request->all());
$state['source'] = $source->token;
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $state);
$this->stripe->payment_hash->data = array_merge((array)$this->stripe->payment_hash->data, $state);
$this->stripe->payment_hash->save();
try {
@ -133,7 +145,7 @@ class ACH
$state = array_merge($state, $request->all());
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $state);
$this->stripe->payment_hash->data = array_merge((array)$this->stripe->payment_hash->data, $state);
$this->stripe->payment_hash->save();
if ($state['charge']->status === 'pending' && is_null($state['charge']->failure_message)) {
@ -145,6 +157,8 @@ class ACH
if ($e instanceof CardException) {
return redirect()->route('client.payment_methods.verification', ['id' => ClientGatewayToken::first()->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
}
throw new PaymentFailed($e->getMessage(), $e->getCode());
}
}
@ -203,8 +217,8 @@ class ACH
{
try {
$payment_meta = new \stdClass;
$payment_meta->brand = (string) sprintf('%s (%s)', $method->bank_name, ctrans('texts.ach'));
$payment_meta->last4 = (string) $method->last4;
$payment_meta->brand = (string)sprintf('%s (%s)', $method->bank_name, ctrans('texts.ach'));
$payment_meta->last4 = (string)$method->last4;
$payment_meta->type = GatewayType::BANK_TRANSFER;
$data = [

View File

@ -95,17 +95,37 @@ class StripePaymentDriver extends BaseDriver
{
$types = [
GatewayType::CREDIT_CARD,
GatewayType::BANK_TRANSFER,
GatewayType::CRYPTO,
GatewayType::ALIPAY,
// GatewayType::SEPA, // TODO: Missing implementation
// GatewayType::APPLE_PAY, // TODO:: Missing implementation
];
if ($this->company_gateway->getSofortEnabled() && $this->invitation && $this->client() && isset($this->client()->country) && in_array($this->client()->country, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) {
// $this->invitation = false
// $this->client doesn't exist
// $this->client->country is relationship?
// Missing Slovenia for Alipay
if ($this->company_gateway->getSofortEnabled()
&& $this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) {
$types[] = GatewayType::SOFORT;
}
if ($this->company_gateway->getAchEnabled()
&& $this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['USA'])) {
$types[] = GatewayType::BANK_TRANSFER;
}
if ($this->company_gateway->getAchEnabled()
&& $this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['AUS', 'DNK', 'DEU', 'ITA', 'LUX', 'NOR', 'SVN', 'GBR', 'AUT', 'EST', 'GRC', 'JPN', 'MYS', 'PRT', 'ESP', 'USA', 'BEL', 'FIN', 'HKG', 'LVA', 'NLD', 'SGP', 'SWE', 'CAN', 'FRA', 'IRL', 'LTU', 'NZL', 'SVK', 'CHE'])) {
$types[] = GatewayType::ALIPAY;
}
return $types;
}
@ -175,7 +195,7 @@ class StripePaymentDriver extends BaseDriver
return $this->payment_method->paymentView($data);
}
public function processPaymentResponse($request)
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request);
}

View File

@ -48,11 +48,11 @@ class AutoBillInvoice extends AbstractService
$this->invoice = $this->invoice->service()->markSent()->save();
/* Mark the invoice as paid if there is no balance */
if ((int)$this->invoice->balance == 0)
if ((int)$this->invoice->balance == 0)
return $this->invoice->service()->markPaid()->save();
//if the credits cover the payments, we stop here, build the payment with credits and exit early
if ($this->client->getSetting('use_credits_payment') != 'off')
if ($this->client->getSetting('use_credits_payment') != 'off')
$this->applyCreditPayment();
/* Determine $amount */
@ -70,9 +70,9 @@ class AutoBillInvoice extends AbstractService
$gateway_token = $this->getGateway($amount);
/* Bail out if no payment methods available */
if (! $gateway_token || ! $gateway_token->gateway->driver($this->client)->token_billing)
if (! $gateway_token || ! $gateway_token->gateway->driver($this->client)->token_billing)
return $this->invoice;
/* $gateway fee */
//$fee = $gateway_token->gateway->calcGatewayFee($amount, $gateway_token->gateway_type_id, $this->invoice->uses_inclusive_taxes);
$this->invoice = $this->invoice->service()->addGatewayFee($gateway_token->gateway, $gateway_token->gateway_type_id, $amount)->save();
@ -261,7 +261,7 @@ class AutoBillInvoice extends AbstractService
* @param float $amount The amount to charge
* @return ClientGatewayToken The client gateway token
*/
// private function
// private function
// {
// $gateway_tokens = $this->client->gateway_tokens()->orderBy('is_default', 'DESC')->get();
@ -280,18 +280,18 @@ class AutoBillInvoice extends AbstractService
$gateway_tokens = $this->client->gateway_tokens;
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use($amount) {
$company_gateway = $gateway_token->gateway;
//check if fees and limits are set
if (isset($company_gateway->fees_and_limits) && property_exists($company_gateway->fees_and_limits, $gateway_token->gateway_type_id))
if (isset($company_gateway->fees_and_limits) && property_exists($company_gateway->fees_and_limits, $gateway_token->gateway_type_id))
{
//if valid we keep this gateway_token
if ($this->invoice->client->validGatewayForAmount($company_gateway->fees_and_limits->{$gateway_token->gateway_type_id}, $amount))
if ($this->invoice->client->validGatewayForAmount($company_gateway->fees_and_limits->{$gateway_token->gateway_type_id}, $amount))
return true;
else
return false;
}
return true; //if no fees_and_limits set then we automatically must add this gateway

View File

@ -314,6 +314,12 @@ class Design extends BaseDesign
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']];
} elseif ($column == '$product.quantity' && !$this->client->company->enable_product_quantity) {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']];
} elseif ($column == '$product.tax_rate1') {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax1-th", 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.tax_rate2') {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax2-th", 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.tax_rate3') {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax3-th", 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]];
} else {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]];
}
@ -394,6 +400,12 @@ class Design extends BaseDesign
$element['elements'][] = ['element' => 'td', 'content' => $row['$product.quantity'], 'properties' => ['data-ref' => 'product_table-product.quantity-td', 'style' => 'display: none;']];
} elseif ($cell == '$task.hours') {
$element['elements'][] = ['element' => 'td', 'content' => $row['$task.quantity'], 'properties' => ['data-ref' => 'task_table-task.hours-td']];
} elseif ($cell == '$product.tax_rate1') {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax1-td']];
} elseif ($cell == '$product.tax_rate2') {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax2-td']];
} elseif ($cell == '$product.tax_rate3') {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax3-td']];
} else {
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']];
}

View File

@ -48,7 +48,7 @@ class CompanyUserTransformer extends EntityTransformer
'updated_at' => (int) $company_user->updated_at,
'archived_at' => (int) $company_user->deleted_at,
'created_at' => (int) $company_user->created_at,
'permissions_updated_at' => (int) $company_user->permissions_updated_at,
];
}

View File

@ -33,6 +33,119 @@ trait GeneratesCounter
//todo in the form validation, we need to ensure that if a prefix and pattern is set we throw a validation error,
//only one type is allow else this will cause confusion to the end user
private function getNextEntityNumber($entity, Client $client)
{
$prefix = '';
$this->resetCounters($client);
$is_client_counter = false;
$counter_string = $this->getEntityCounter($entity, $client);
$pattern = $this->getNumberPattern($entity, $client);
if (strpos($pattern, 'clientCounter') || strpos($pattern, 'client_counter')) {
if (property_exists($client->settings, $counter_string)) {
$counter = $client->settings->{$counter_string};
} else {
$counter = 1;
}
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter') || strpos($pattern, 'group_counter')) {
if (property_exists($client->group_settings, $counter_string)) {
$counter = $client->group_settings->{$counter_string};
} else {
$counter = 1;
}
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->{$counter_string};
$counter_entity = $client->company;
}
//If it is a quote - we need to
$pattern = $this->getNumberPattern($entity, $client);
$padding = $client->getSetting('counter_padding');
if($entity instanceof Invoice && $entity && $entity->recurring_id)
$prefix = $client->getSetting('recurring_number_prefix');
$entity_number = $this->checkEntityNumber($entity, $client, $counter, $padding, $pattern, $prefix);
$this->incrementCounter($counter_entity, $counter_string);
return $entity_number;
}
private function getNumberPattern($entity, Client $client)
{
$pattern_string = '';
switch ($entity) {
case Invoice::class:
$pattern_string = 'invoice_number_pattern';
break;
case Quote::class:
$pattern_string = 'quote_number_pattern';
break;
case RecurringInvoice::class:
$pattern_string = 'recurring_invoice_number_pattern';
break;
case Payment::class:
$pattern_string = 'payment_number_pattern';
break;
case Credit::class:
$pattern_string = 'credit_number_pattern';
break;
case Project::class:
$pattern_string = 'project_number_pattern';
break;
}
return $client->getSetting($pattern_string);
}
private function getEntityCounter($entity, $client)
{
switch ($entity) {
case Invoice::class:
return 'invoice_number_counter';
break;
case Quote::class:
if ($this->hasSharedCounter($client))
return 'invoice_number_counter';
return 'quote_number_counter';
break;
case RecurringInvoice::class:
return 'recurring_invoice_number_counter';
break;
case Payment::class:
return 'payment_number_counter';
break;
case Credit::class:
return 'credit_number_counter';
break;
case Project::class:
return 'project_number_counter';
break;
default:
return 'default_number_counter';
break;
}
}
/**
* Gets the next invoice number.
*
@ -43,42 +156,7 @@ trait GeneratesCounter
*/
public function getNextInvoiceNumber(Client $client, ?Invoice $invoice) :string
{
//Reset counters if enabled
$this->resetCounters($client);
//todo handle if we have specific client patterns in the future
$pattern = $client->getSetting('invoice_number_pattern');
//Determine if we are using client_counters
if (strpos($pattern, 'clientCounter') || strpos($pattern, 'client_counter')) {
if (property_exists($client->settings, 'invoice_number_counter')) {
$counter = $client->settings->invoice_number_counter;
} else {
$counter = 1;
}
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter') || strpos($pattern, 'group_counter')) {
$counter = $client->group_settings->invoice_number_counter;
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->invoice_number_counter;
$counter_entity = $client->company;
}
//Return a valid counter
$pattern = $client->getSetting('invoice_number_pattern');
$padding = $client->getSetting('counter_padding');
$prefix = '';
if ($invoice && $invoice->recurring_id) {
$prefix = $client->getSetting('recurring_number_prefix');
}
$invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $pattern, $prefix);
$this->incrementCounter($counter_entity, 'invoice_number_counter');
return $invoice_number;
return $this->getNextEntityNumber(Invoice::class, $client);
}
/**
@ -90,145 +168,36 @@ trait GeneratesCounter
*/
public function getNextCreditNumber(Client $client) :string
{
//Reset counters if enabled
$this->resetCounters($client);
//todo handle if we have specific client patterns in the future
$pattern = $client->getSetting('credit_number_pattern');
//Determine if we are using client_counters
if (strpos($pattern, 'clientCounter') || strpos($pattern, 'client_counter')) {
$counter = $client->settings->credit_number_counter;
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter') || strpos($pattern, 'group_counter')) {
$counter = $client->group_settings->credit_number_counter;
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->credit_number_counter;
$counter_entity = $client->company;
}
//Return a valid counter
$pattern = $client->getSetting('credit_number_pattern');
$padding = $client->getSetting('counter_padding');
$credit_number = $this->checkEntityNumber(Credit::class, $client, $counter, $padding, $pattern);
$this->incrementCounter($counter_entity, 'credit_number_counter');
return $credit_number;
return $this->getNextEntityNumber(Credit::class, $client);
}
/**
* Gets the next quote number.
*
* @param Client $client The client
*
* @return string The next credit number.
*/
public function getNextQuoteNumber(Client $client)
{
//Reset counters if enabled
$this->resetCounters($client);
$used_counter = 'quote_number_counter';
if ($this->hasSharedCounter($client)) {
$used_counter = 'invoice_number_counter';
}
//todo handle if we have specific client patterns in the future
$pattern = $client->getSetting('quote_number_pattern');
//Determine if we are using client_counters
if (strpos($pattern, 'clientCounter') || strpos($pattern, 'client_counter')) {
$counter = $client->settings->{$used_counter};
$counter_entity = $client;
} elseif (strpos($pattern, 'groupCounter') || strpos($pattern, 'group_counter')) {
$counter = $client->group_settings->{$used_counter};
$counter_entity = $client->group_settings;
} else {
$counter = $client->company->settings->{$used_counter};
$counter_entity = $client->company;
}
//Return a valid counter
$pattern = $client->getSetting('quote_number_pattern');
$padding = $client->getSetting('counter_padding');
$quote_number = $this->checkEntityNumber(Quote::class, $client, $counter, $padding, $pattern);
// if($this->recurring_id)
// $quote_number = $this->prefixCounter($quote_number, $client->getSetting('recurring_number_prefix'));
$this->incrementCounter($counter_entity, $used_counter);
return $quote_number;
return $this->getNextEntityNumber(Quote::class, $client);
}
public function getNextRecurringInvoiceNumber(Client $client)
{
//Reset counters if enabled
$this->resetCounters($client);
$is_client_counter = false;
//todo handle if we have specific client patterns in the future
$pattern = $client->company->settings->recurring_invoice_number_pattern;
//Determine if we are using client_counters
if (strpos($pattern, 'client_counter') === false) {
$counter = $client->company->settings->recurring_invoice_number_counter;
} else {
$counter = $client->settings->recurring_invoice_number_counter;
$is_client_counter = true;
}
//Return a valid counter
$pattern = '';
$padding = $client->getSetting('counter_padding');
$invoice_number = $this->checkEntityNumber(RecurringInvoice::class, $client, $counter, $padding, $pattern);
//$invoice_number = $this->prefixCounter($invoice_number, $client->getSetting('recurring_number_prefix'));
//increment the correct invoice_number Counter (company vs client)
if ($is_client_counter) {
$this->incrementCounter($client, 'recurring_invoice_number_counter');
} else {
$this->incrementCounter($client->company, 'recurring_invoice_number_counter');
}
return $invoice_number;
return $this->getNextEntityNumber(RecurringInvoice::class, $client);
}
/**
* Payment Number Generator.
* @param Client $client
* @return string The payment number
* Gets the next Payment number.
*
* @param Client $client The client
*
* @return string The next payment number.
*/
public function getNextPaymentNumber(Client $client) :string
{
//Reset counters if enabled
$this->resetCounters($client);
$is_client_counter = false;
//todo handle if we have specific client patterns in the future
$pattern = $client->company->settings->payment_number_pattern;
//Determine if we are using client_counters
if (strpos($pattern, 'client_counter') === false) {
$counter = $client->company->settings->payment_number_counter;
} else {
$counter = $client->settings->payment_number_counter;
$is_client_counter = true;
}
//Return a valid counter
$pattern = '';
$padding = $client->getSetting('counter_padding');
$payment_number = $this->checkEntityNumber(Payment::class, $client, $counter, $padding, $pattern);
//increment the correct invoice_number Counter (company vs client)
if ($is_client_counter) {
$this->incrementCounter($client, 'payment_number_counter');
} else {
$this->incrementCounter($client->company, 'payment_number_counter');
}
return (string) $payment_number;
return $this->getNextEntityNumber(Payment::class, $client);
}
/**
@ -342,7 +311,7 @@ trait GeneratesCounter
*
* @return bool True if has shared counter, False otherwise.
*/
public function hasSharedCounter(Client $client) : bool
public function hasSharedCounter(Client $client) : bool
{
return (bool) $client->getSetting('shared_invoice_quote_counter');
}
@ -404,6 +373,9 @@ trait GeneratesCounter
$settings->invoice_number_counter = 0;
}
if(!property_exists($settings, $counter_name))
$settings->{$counter_name} = 1;
$settings->{$counter_name} = $settings->{$counter_name} + 1;
$entity->settings = $settings;

View File

@ -13,7 +13,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.0.55',
'app_version' => '5.0.56',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddPermissionChangedTimestamp extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('company_user', function (Blueprint $table) {
$table->timestamp('permissions_updated_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -3,35 +3,35 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"manifest.json": "77215c1737c7639764e64a192be2f7b8",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "3e722fd57a6db80ee119f0e2c230ccff",
"assets/NOTICES": "c3e1cbfaeb1a4f54fadae1bd6558d91b",
"assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
"assets/NOTICES": "c3e1cbfaeb1a4f54fadae1bd6558d91b",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "3e722fd57a6db80ee119f0e2c230ccff",
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"/": "23224b5e03519aaa87594403d54412cf",
"main.dart.js": "419ce42069c50ba32d64d01d76373c60",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"manifest.json": "77215c1737c7639764e64a192be2f7b8",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"version.json": "24380404aa64649901a0878a4f6aae18",
"main.dart.js": "1071216a656504599447ac0e362ca27a",
"favicon.png": "dca91c54388f52eded692718d5a98b8b"
};

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
/*! For license information please see checkout-credit-card.js.LICENSE.txt */
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=8)}({8:function(e,t,n){e.exports=n("fQHp")},fQHp:function(e,t){function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.tokens=[]}var t,r,o;return t=e,(r=[{key:"mountFrames",value:function(){console.log("Mount checkout frames..")}},{key:"handlePaymentUsingToken",value:function(e){document.getElementById("checkout--container").classList.add("hidden"),document.getElementById("pay-now-with-token--container").classList.remove("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=e.target.dataset.token}},{key:"handlePaymentUsingCreditCard",value:function(e){var t;document.getElementById("checkout--container").classList.remove("hidden"),document.getElementById("pay-now-with-token--container").classList.add("hidden"),document.getElementById("save-card--container").style.display="grid";var n=document.getElementById("pay-button"),r=null!==(t=document.querySelector('meta[name="public-key"]').content)&&void 0!==t?t:"",o=document.getElementById("payment-form");Frames.init(r),Frames.addEventHandler(Frames.Events.CARD_VALIDATION_CHANGED,(function(e){n.disabled=!Frames.isCardValid()})),Frames.addEventHandler(Frames.Events.CARD_TOKENIZED,(function(e){document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e),document.querySelector('input[name="store_card"]').value=document.querySelector("input[name=token-billing-checkbox]:checked").value,document.getElementById("server-response").submit()})),o.addEventListener("submit",(function(e){e.preventDefault(),Frames.submitCard()}))}},{key:"completePaymentUsingToken",value:function(e){var t=document.getElementById("pay-now-with-token");t.disabled=!0,t.querySelector("svg").classList.remove("hidden"),t.querySelector("span").classList.add("hidden"),document.getElementById("server-response").submit()}},{key:"handle",value:function(){var e=this;this.handlePaymentUsingCreditCard(),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach((function(t){return t.addEventListener("click",e.handlePaymentUsingToken)})),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",this.handlePaymentUsingCreditCard),document.getElementById("pay-now-with-token").addEventListener("click",this.completePaymentUsingToken)}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()}});
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=8)}({8:function(e,t,n){e.exports=n("fQHp")},fQHp:function(e,t){function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.tokens=[]}var t,r,o;return t=e,(r=[{key:"mountFrames",value:function(){console.log("Mount checkout frames..")}},{key:"handlePaymentUsingToken",value:function(e){document.getElementById("checkout--container").classList.add("hidden"),document.getElementById("pay-now-with-token--container").classList.remove("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=e.target.dataset.token}},{key:"handlePaymentUsingCreditCard",value:function(e){var t;document.getElementById("checkout--container").classList.remove("hidden"),document.getElementById("pay-now-with-token--container").classList.add("hidden"),document.getElementById("save-card--container").style.display="grid";var n=document.getElementById("pay-button"),r=null!==(t=document.querySelector('meta[name="public-key"]').content)&&void 0!==t?t:"",o=document.getElementById("payment-form");Frames.init(r),Frames.addEventHandler(Frames.Events.CARD_VALIDATION_CHANGED,(function(e){n.disabled=!Frames.isCardValid()})),Frames.addEventHandler(Frames.Events.CARD_TOKENIZATION_FAILED,(function(e){pay.button.disabled=!1})),Frames.addEventHandler(Frames.Events.CARD_TOKENIZED,(function(e){n.disabled=!0,document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e),document.querySelector('input[name="store_card"]').value=document.querySelector("input[name=token-billing-checkbox]:checked").value,document.getElementById("server-response").submit()})),o.addEventListener("submit",(function(e){e.preventDefault(),Frames.submitCard()}))}},{key:"completePaymentUsingToken",value:function(e){var t=document.getElementById("pay-now-with-token");t.disabled=!0,t.querySelector("svg").classList.remove("hidden"),t.querySelector("span").classList.add("hidden"),document.getElementById("server-response").submit()}},{key:"handle",value:function(){var e=this;this.handlePaymentUsingCreditCard(),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach((function(t){return t.addEventListener("click",e.handlePaymentUsingToken)})),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",this.handlePaymentUsingCreditCard),document.getElementById("pay-now-with-token").addEventListener("click",this.completePaymentUsingToken)}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()}});

88811
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
{
"/js/app.js": "/js/app.js?id=a33a5a58bfc6e2174841",
"/js/app.js": "/js/app.js?id=1ee684e58f9f6eb754d5",
"/css/app.css": "/css/app.css?id=599b11149976e86c83a3",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=206d7de4ac97612980ff",
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=a376eff2227da398b0ba",
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=5469146cd629ea1b5c20",
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=edc30120fdc238cd15ea",
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=98e406fa8e4db0e93427",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=c4012ad90f17d60432ad",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=6dbe9316b98deea55421",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=9418a9c5c137994c4bd8",

16
resources/js/app.js vendored
View File

@ -14,20 +14,6 @@ window.axios = require('axios');
*/
window.valid = require('card-validator');
/**
* Toggle processing overlay.
*/
window.processingOverlay = (show) => {
if (show) {
return document
.getElementById('processing-overlay')
.classList.remove('hidden');
}
return document
.getElementById('processing-overlay')
.classList.add('hidden');
};
/**
* Remove flashing message div after 3 seconds.
@ -35,5 +21,5 @@ window.processingOverlay = (show) => {
document.querySelectorAll('.disposable-alert').forEach((element) => {
setTimeout(() => {
element.remove();
}, 3000);
}, 5000);
});

View File

@ -42,7 +42,13 @@ class CheckoutCreditCard {
payButton.disabled = !Frames.isCardValid();
});
Frames.addEventHandler(Frames.Events.CARD_TOKENIZATION_FAILED, function (event) {
pay.button.disabled = false;
});
Frames.addEventHandler(Frames.Events.CARD_TOKENIZED, function (event) {
payButton.disabled = true;
document.querySelector(
'input[name="gateway_response"]'
).value = JSON.stringify(event);

View File

@ -286,7 +286,7 @@ return [
'password' => 'Password',
'pro_plan_product' => 'Pro Plan',
'pro_plan_success' => 'Thanks for choosing Invoice Ninja\'s Pro plan!&nbsp;<br/>
Next StepsA payable invoice has been sent to the email
Next Steps. A payable invoice has been sent to the email
address associated with your account. To unlock all of the awesome
Pro features, please follow the instructions on the invoice to pay
for a year of Pro-level invoicing.
@ -1091,7 +1091,7 @@ return [
'invoice_item_fields' => 'Invoice Item Fields',
'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.',
'recurring_invoice_number' => 'Recurring Number',
'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices.',
'recurring_invoice_number_prefix_help' => 'Specify a prefix to be added to the invoice number for recurring invoices.',
// Client Passwords
'enable_portal_password' => 'Password Protect Invoices',
@ -3383,5 +3383,6 @@ return [
'create_webhook_failure' => 'Failed to create Webhook',
'number' => 'Number',
'payment_message_extended' => 'Thank you for your payment of :amount for :invoice',
'online_payments_minimum_note' => 'Note: Online payments are supported only if amount is bigger than $1 or currency equivalent.',
'payment_token_not_found' => 'Payment token not found, please try again. If an issue still persist, try with another payment method',
];

View File

@ -38,7 +38,7 @@
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->token }}"
data-token="{{ $token->hashed_id }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">**** {{ optional($token->meta)->last4 }}</span>

View File

@ -141,7 +141,7 @@
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->token }}"
data-token="{{ $token->hashed_id }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">**** {{ optional($token->meta)->last4 }}</span>

View File

@ -23,7 +23,7 @@
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->token }}"
data-token="{{ $token->hashed_id }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">{{ ctrans('texts.bank_transfer') }} (*{{ $token->meta->last4 }})</span>

View File

@ -40,7 +40,7 @@
<a href="#" @click="{ open = false }" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
{{ \App\Models\CompanyGateway::find($payment_method['company_gateway_id'])->firstOrFail()->getConfigField('name') }}
</a>
@else
@elseif($total > 0)
<a href="#" @click="{ open = false }" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
{{ $payment_method['label'] }}
</a>
@ -151,6 +151,10 @@
</div>
</div>
@endforeach
@if(intval($total) == 0)
<small>* {{ ctrans('texts.online_payments_minimum_note') }}</small>
@endif
</div>
</div>
</div>

View File

@ -48,7 +48,8 @@
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}} - {{ ctrans('texts.unpaid') }}
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.paid') }}
</h3>
</div>
</div>
@ -70,7 +71,7 @@
</svg>
</button>
</div>
<span class="text-sm text-gray-700 ml-2">{{ ctrans('texts.page') }}:
<span class="text-sm text-gray-700 ml-2">{{ ctrans('texts.page') }}:
<span id="current-page-container"></span>
<span>{{ strtolower(ctrans('texts.of')) }}</span>
<span id="total-page-container"></span>

View File

@ -21,7 +21,8 @@
</div>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}?mode=portal" class="mr-4 text-primary">
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}?mode=portal"
class="mr-4 text-primary">
&#8592; {{ ctrans('texts.client_portal') }}
</a>
@ -36,15 +37,17 @@
</div>
</form>
@else
<div class="bg-white shadow sm:rounded-lg mb-4" translate>
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.unpaid') }}
</h3>
</div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.paid') }}
</h3>
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}?mode=portal"
class="mr-4 text-primary">
&#8592; {{ ctrans('texts.client_portal') }}
</a>
</div>
</div>
</div>

View File

@ -69,6 +69,12 @@
@include('portal.ninja2020.components.primary-color')
<body class="antialiased {{ $custom_body_class ?? '' }}">
@if(session()->has('message'))
<div class="py-1 text-sm text-center text-white bg-primary disposable-alert">
{{ session('message') }}
</div>
@endif
@yield('body')
<script src="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.js" data-cfasync="false"></script>