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

Merge pull request #7054 from turbo124/v5-develop

Fixes for edge cases around duplicate contacts in portal
This commit is contained in:
David Bomba 2021-12-21 21:44:44 +11:00 committed by GitHub
commit 3e502af7dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 4846 additions and 33 deletions

View File

@ -13,7 +13,7 @@ class PaymentFailed extends Exception
public function render($request)
{
if (auth()->user()) {
if (auth()->user() || ($request->has('cko-session-id') && $request->query('cko-session-id') )) {
return render('gateways.unsuccessful', [
'message' => $this->getMessage(),
'code' => $this->getCode(),

View File

@ -20,11 +20,6 @@ use Illuminate\Database\Eloquent\Builder;
class DocumentFilters extends QueryFilters
{
// public function client_id(int $client_id) :Builder
// {
// return $this->builder->where('client_id', $client_id);
// }
/**
* Filter based on search text.
*
@ -41,6 +36,14 @@ class DocumentFilters extends QueryFilters
return $this->builder;
}
/* If client ID passed to this entity, simply return */
public function client_id(string $client_id = '') :Builder
{
return $this->builder;
}
/**
* Sorts the list based on $sort.
*
@ -64,6 +67,7 @@ class DocumentFilters extends QueryFilters
*/
public function baseQuery(int $company_id, User $user) : Builder
{
return $this->builder;
}
/**

View File

@ -0,0 +1,118 @@
<?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\Filters;
use App\Models\Design;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
/**
* PaymentTermFilters.
*/
class PaymentTermFilters extends QueryFilters
{
/**
* Filter based on search text.
*
* @param string query filter
* @return Builder
* @deprecated
*/
public function filter(string $filter = '') : Builder
{
if (strlen($filter) == 0) {
return $this->builder;
}
return $this->builder->where(function ($query) use ($filter) {
$query->where('payment_terms.name', 'like', '%'.$filter.'%');
});
}
/**
* Filters the list based on the status
* archived, active, deleted.
*
* @param string filter
* @return Builder
*/
public function status(string $filter = '') : Builder
{
if (strlen($filter) == 0) {
return $this->builder;
}
$table = 'payment_terms';
$filters = explode(',', $filter);
return $this->builder->where(function ($query) use ($filters, $table) {
$query->whereNull($table.'.id');
if (in_array(parent::STATUS_ACTIVE, $filters)) {
$query->orWhereNull($table.'.deleted_at');
}
if (in_array(parent::STATUS_ARCHIVED, $filters)) {
$query->orWhere(function ($query) use ($table) {
$query->whereNotNull($table.'.deleted_at');
if (! in_array($table, ['users'])) {
$query->where($table.'.is_deleted', '=', 0);
}
});
}
if (in_array(parent::STATUS_DELETED, $filters)) {
$query->orWhere($table.'.is_deleted', '=', 1);
}
});
}
/**
* Sorts the list based on $sort.
*
* @param string sort formatted as column|asc
* @return Builder
*/
public function sort(string $sort) : Builder
{
$sort_col = explode('|', $sort);
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
}
/**
* Returns the base query.
*
* @param int company_id
* @param User $user
* @return Builder
* @deprecated
*/
public function baseQuery(int $company_id, User $user) : Builder
{
return $this->builder;
}
/**
* Filters the query by the users company ID.
*
* @return Illuminate\Database\Query\Builder
*/
public function entityFilter()
{
//return $this->builder->whereCompanyId(auth()->user()->company()->id);
return $this->builder->whereCompanyId(auth()->user()->company()->id)->orWhere('company_id', null);
}
}

View File

@ -14,6 +14,7 @@ use App\Transformers\PaymentTermTransformer;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Filters\PaymentTermFilters;
class PaymentTermController extends BaseController
{
@ -73,9 +74,9 @@ class PaymentTermController extends BaseController
* ),
* )
*/
public function index()
public function index(PaymentTermFilters $filters)
{
$payment_terms = PaymentTerm::whereCompanyId(auth()->user()->company()->id)->orWhere('company_id', null);
$payment_terms = PaymentTerm::filter($filters);
return $this->listResponse($payment_terms);
}

View File

@ -52,7 +52,7 @@ class CheckClientExistence
return redirect()->route('client.login');
}
if (count($multiple_contacts) == 1) {
if (count($multiple_contacts) == 1 && !Auth::guard('contact')->check()) {
Auth::guard('contact')->loginUsingId($multiple_contacts[0]->id, true);
}

View File

@ -866,6 +866,7 @@ class CompanyImport implements ShouldQueue
'company_id',
'backup',
'invitation_id',
'payment_id',
],
[
['users' => 'user_id'],
@ -873,7 +874,7 @@ class CompanyImport implements ShouldQueue
['client_contacts' => 'client_contact_id'],
['projects' => 'project_id'],
['vendors' => 'vendor_id'],
['payments' => 'payment_id'],
// ['payments' => 'payment_id'],
['invoices' => 'invoice_id'],
['credits' => 'credit_id'],
['tasks' => 'task_id'],
@ -1418,10 +1419,9 @@ class CompanyImport implements ShouldQueue
if (! array_key_exists($resource, $this->ids)) {
nlog($resource);
nlog($this->ids);
$this->sendImportMail("The Import failed due to missing data in the import file. Resource {$resource} not available.");
nlog($this->ids);
throw new \Exception("Resource {$resource} not available.");
}
@ -1435,6 +1435,8 @@ class CompanyImport implements ShouldQueue
return $this->company_owner->id;
$this->sendImportMail("The Import failed due to missing data in the import file. Resource {$resource} not available.");
nlog($this->ids[$resource]);
throw new \Exception("Missing {$resource} key: {$old}");
}

View File

@ -11,6 +11,7 @@
namespace App\Models;
use App\Models\Filterable;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@ -19,6 +20,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class PaymentTerm extends BaseModel
{
use SoftDeletes;
use Filterable;
/**
* @var bool

View File

@ -84,8 +84,15 @@ trait Utilities
public function processUnsuccessfulPayment(Payment $_payment, $throw_exception = true)
{
$this->getParent()->sendFailureMail($_payment->response_summary);
// $this->getParent()->clientPaymentFailureMailer($_payment->status);
$error_message = '';
if(property_exists($_payment, 'server_response'))
$error_message = $_payment->response_summary;
elseif(property_exists($_payment, 'status'))
$error_message = $_payment->status;
$this->getParent()->sendFailureMail($error_message);
$message = [
'server_response' => $_payment,
@ -102,7 +109,8 @@ trait Utilities
);
if ($throw_exception) {
throw new PaymentFailed($_payment->status . " " . optional($_payment)->response_summary, $_payment->http_code);
throw new PaymentFailed($_payment->status . " " . $error_message, $_payment->http_code);
}
}

View File

@ -18,7 +18,7 @@ trait Utilities
public function convertFromStripeAmount($amount, $precision, $currency)
{
if($currency->code == "JPY")
if(in_array($amount, ["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"]))
return $amount;
return $amount / pow(10, $precision);
@ -28,7 +28,7 @@ trait Utilities
public function convertToStripeAmount($amount, $precision, $currency)
{
if($currency->code == "JPY")
if(in_array($amount, ["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"]))
return $amount;
return round(($amount * pow(10, $precision)),0);

View File

@ -33,19 +33,6 @@ class NinjaTranslationServiceProvider extends TranslationServiceProvider
*
*/
// $this->app->bind('translator', function($app) {
// $loader = $app['translation.loader'];
// $locale = $app['config']['app.locale'];
// $trans = new NinjaTranslator($loader, $locale);
// $trans->setFallback($app['config']['app.fallback_locale']);
// return $trans;
// });
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];

View File

@ -299,7 +299,10 @@ class InvoiceService
if($this->invoice->status_id == Invoice::STATUS_DRAFT)
return $this;
if ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
if($this->invoice->balance == 0){
$this->setStatus(Invoice::STATUS_PAID);
}
elseif ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
$this->setStatus(Invoice::STATUS_PARTIAL);
}

View File

@ -0,0 +1,33 @@
<?php
use App\Models\Language;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddSerbianLanguageTranslations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$serbian = ['id' => 33, 'name' => 'Serbian', 'locale' => 'sr'];
Language::unguard();
Language::create($serbian);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -22,7 +22,7 @@
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="MAIL_MAILER" value="array"/>
<env name="DB_CONNECTION" value="mysql"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
</php>
<logging/>

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'These credentials do not match our records.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
];

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '« Previous',
'next' => 'Next »',
];

View File

@ -0,0 +1,23 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'Passwords must be at least six characters and match the confirmation.',
'reset' => 'Your password has been reset!',
'sent' => 'We have e-mailed your password reset link!',
'token' => 'This password reset token is invalid.',
'user' => "We can't find a user with that e-mail address.",
'throttled' => "You have requested password reset recently, please check your email.",
];

4359
resources/lang/sr/texts.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'The :attribute must be accepted.',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'before' => 'The :attribute must be a date before :date.',
'before_or_equal' => 'The :attribute must be a date before or equal to :date.',
'between' => [
'numeric' => 'The :attribute must be between :min and :max.',
'file' => 'The :attribute must be between :min and :max kilobytes.',
'string' => 'The :attribute must be between :min and :max characters.',
'array' => 'The :attribute must have between :min and :max items.',
],
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'email' => 'The :attribute must be a valid email address.',
'exists' => 'The selected :attribute is invalid.',
'file' => 'The :attribute must be a file.',
'filled' => 'The :attribute field must have a value.',
'gt' => [
'numeric' => 'The :attribute must be greater than :value.',
'file' => 'The :attribute must be greater than :value kilobytes.',
'string' => 'The :attribute must be greater than :value characters.',
'array' => 'The :attribute must have more than :value items.',
],
'gte' => [
'numeric' => 'The :attribute must be greater than or equal :value.',
'file' => 'The :attribute must be greater than or equal :value kilobytes.',
'string' => 'The :attribute must be greater than or equal :value characters.',
'array' => 'The :attribute must have :value items or more.',
],
'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.',
'in_array' => 'The :attribute field does not exist in :other.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'ipv4' => 'The :attribute must be a valid IPv4 address.',
'ipv6' => 'The :attribute must be a valid IPv6 address.',
'json' => 'The :attribute must be a valid JSON string.',
'lt' => [
'numeric' => 'The :attribute must be less than :value.',
'file' => 'The :attribute must be less than :value kilobytes.',
'string' => 'The :attribute must be less than :value characters.',
'array' => 'The :attribute must have less than :value items.',
],
'lte' => [
'numeric' => 'The :attribute must be less than or equal :value.',
'file' => 'The :attribute must be less than or equal :value kilobytes.',
'string' => 'The :attribute must be less than or equal :value characters.',
'array' => 'The :attribute must not have more than :value items.',
],
'max' => [
'numeric' => 'The :attribute may not be greater than :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.',
'string' => 'The :attribute may not be greater than :max characters.',
'array' => 'The :attribute may not have more than :max items.',
],
'mimes' => 'The :attribute must be a file of type: :values.',
'mimetypes' => 'The :attribute must be a file of type: :values.',
'min' => [
'numeric' => 'The :attribute must be at least :min.',
'file' => 'The :attribute must be at least :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.',
'array' => 'The :attribute must have at least :min items.',
],
'not_in' => 'The selected :attribute is invalid.',
'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'present' => 'The :attribute field must be present.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
'size' => [
'numeric' => 'The :attribute must be :size.',
'file' => 'The :attribute must be :size kilobytes.',
'string' => 'The :attribute must be :size characters.',
'array' => 'The :attribute must contain :size items.',
],
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'url' => 'The :attribute format is invalid.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];

View File

@ -60,7 +60,6 @@
<script>
document.addEventListener("DOMContentLoaded", function () {
document.querySelector('div[data-ref="required-fields-container"]').classList.add('hidden');
document.querySelector('div[data-ref="gateway-container"]').classList.remove('opacity-25');
document.querySelector('div[data-ref="gateway-container"]').classList.remove('pointer-events-none');
});

View File

@ -182,6 +182,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::put('vendors/{vendor}/upload', 'VendorController@upload');
Route::get('users', 'UserController@index');
Route::get('users/{user}', 'UserController@show')->middleware('password_protected');
Route::put('users/{user}', 'UserController@update')->middleware('password_protected');
Route::post('users', 'UserController@store')->middleware('password_protected');
//Route::post('users/{user}/attach_to_company', 'UserController@attach')->middleware('password_protected');

View File

@ -57,6 +57,37 @@ class PaymentTermsApiTest extends TestCase
$response->assertStatus(200);
}
public function testPaymentTermsGetStatusActive()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/payment_terms?status=active');
$response->assertStatus(200);
}
public function testPaymentTermsGetStatusArchived()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/payment_terms?status=archived');
$response->assertStatus(200);
}
public function testPaymentTermsGetStatusDeleted()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/payment_terms?status=deleted');
$response->assertStatus(200);
}
public function testPostPaymentTerm()
{
$response = $this->withHeaders([

View File

@ -0,0 +1,58 @@
<?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://opensource.org/licenses/AAL
*/
namespace Tests\Unit;
use App\Jobs\Util\UploadFile;
use App\Models\Document;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Storage;
use Tests\MockAccountData;
use Tests\TestCase;
class ZeroDecimalTest extends TestCase
{
public array $currencies = ["BIF","CLP","DJF","GNF","JPY","KMF","KRW","MGA","PYG","RWF","UGX","VND","VUV","XAF","XOF","XPF"];
public function setUp() :void
{
}
public function testCurrencyHit()
{
$this->assertTrue(in_array("KRW", $this->currencies));
}
public function testCurrencyMiss()
{
$this->assertFalse(in_array("USD", $this->currencies));
}
public function testCurrencyNotexist()
{
$this->assertFalse(in_array("USDddd", $this->currencies));
}
}