mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 02:47:02 +00:00
@@ -1 +1 @@
|
||||
5.12.36
|
||||
5.12.37
|
||||
@@ -1739,13 +1739,15 @@ $products = str_getcsv($this->input['product_key'], ',', "'");
|
||||
|
||||
$data = [
|
||||
"{$model_string}s" => $query->get(),
|
||||
"start_date" => $this->start_date,
|
||||
"end_date" => $this->end_date,
|
||||
// "start_date" => $this->start_date,
|
||||
// "end_date" => $this->end_date,
|
||||
];
|
||||
|
||||
$ts = new TemplateService($template);
|
||||
$ts->setCompany($this->company);
|
||||
$ts->addGlobal(['currency_code' => $this->company->currency()->code]);
|
||||
$ts->twig->addGlobal('start_date', $this->start_date);
|
||||
$ts->twig->addGlobal('end_date', $this->end_date);
|
||||
$ts->build($data);
|
||||
|
||||
return $ts->getPdf();
|
||||
|
||||
49
app/Helpers/Cache/Atomic.php
Normal file
49
app/Helpers/Cache/Atomic.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com)
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Helpers\Cache;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class Atomic
|
||||
{
|
||||
public static function set($key, $value = true, $ttl = 1): bool
|
||||
{
|
||||
$new_ttl = now()->addSeconds($ttl);
|
||||
|
||||
try {
|
||||
return Redis::connection('sentinel-cache')->set($key, $value, 'EX', $ttl, 'NX') ? true : false;
|
||||
} catch (\Throwable) {
|
||||
return Cache::add($key, $value, $new_ttl) ? true : false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function get($key)
|
||||
{
|
||||
try {
|
||||
return Redis::connection('sentinel-cache')->get($key);
|
||||
} catch (\Throwable) {
|
||||
return Cache::get($key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function del($key)
|
||||
{
|
||||
try {
|
||||
return Redis::connection('sentinel-cache')->del($key);
|
||||
} catch (\Throwable) {
|
||||
return Cache::forget($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,7 @@ class PaymentController extends Controller
|
||||
$data = [
|
||||
'invoice' => $invoice,
|
||||
'key' => false,
|
||||
'_key' => false,
|
||||
'invitation' => $invitation,
|
||||
'variables' => $variables,
|
||||
];
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Models\Account;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Scheduler;
|
||||
use App\Jobs\Cron\AutoBill;
|
||||
use App\Helpers\Cache\Atomic;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Filters\InvoiceFilters;
|
||||
@@ -241,7 +242,7 @@ class InvoiceController extends BaseController
|
||||
|
||||
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars($user ? $user->id : null)));
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return $this->itemResponse($invoice);
|
||||
}
|
||||
@@ -411,7 +412,14 @@ class InvoiceController extends BaseController
|
||||
return $request->disallowUpdate();
|
||||
}
|
||||
|
||||
if ($invoice->isLocked()) {
|
||||
if(($invoice->isLocked() || $invoice->company->verifactuEnabled()) && $request->input('paid') == 'true'){
|
||||
|
||||
$invoice->service()
|
||||
->triggeredActions($request);
|
||||
|
||||
return $this->itemResponse($invoice->fresh());
|
||||
}
|
||||
elseif ($invoice->isLocked()) {
|
||||
return response()->json(['message' => '', 'errors' => ['number' => ctrans('texts.locked_invoice')]], 422);
|
||||
}
|
||||
|
||||
@@ -497,29 +505,29 @@ class InvoiceController extends BaseController
|
||||
$ids = $request->input('ids');
|
||||
|
||||
if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !$user->company()->account->account_sms_verified) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
return response(['message' => 'Please verify your account to send emails.'], 400);
|
||||
}
|
||||
|
||||
if (Ninja::isHosted() && $user->account->emailQuotaExceeded()) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
return response(['message' => ctrans('texts.email_quota_exceeded_subject')], 400);
|
||||
}
|
||||
|
||||
if ($user->hasExactPermission('disable_emails') && (stripos($action, 'email') !== false)) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
return response(['message' => ctrans('texts.disable_emails_error')], 400);
|
||||
}
|
||||
|
||||
if (in_array($request->action, ['auto_bill', 'mark_paid']) && $user->cannot('create', \App\Models\Payment::class)) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
return response(['message' => ctrans('texts.not_authorized'), 'errors' => ['ids' => [ctrans('texts.not_authorized')]]], 422);
|
||||
}
|
||||
|
||||
$invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
|
||||
|
||||
if ($invoices->count() == 0) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
return response()->json(['message' => 'No Invoices Found']);
|
||||
}
|
||||
|
||||
@@ -530,13 +538,13 @@ class InvoiceController extends BaseController
|
||||
if ($action == 'bulk_download' && $invoices->count() > 1) {
|
||||
$invoices->each(function ($invoice) use ($user, $request) {
|
||||
if ($user->cannot('view', $invoice)) {
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
return response()->json(['message' => ctrans('text.access_denied')]);
|
||||
}
|
||||
});
|
||||
|
||||
ZipInvoices::dispatch($invoices->pluck('id'), $invoices->first()->company, auth()->user());
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return response()->json(['message' => ctrans('texts.sent_message')], 200);
|
||||
}
|
||||
@@ -545,7 +553,7 @@ class InvoiceController extends BaseController
|
||||
|
||||
$filename = $invoices->first()->getFileName();
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return response()->streamDownload(function () use ($invoices) {
|
||||
echo $invoices->first()->service()->getInvoicePdf();
|
||||
@@ -574,7 +582,7 @@ class InvoiceController extends BaseController
|
||||
})->toArray();
|
||||
|
||||
$mergedPdf = (new PdfMerge($paths))->run();
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return response()->streamDownload(function () use ($mergedPdf) {
|
||||
echo $mergedPdf;
|
||||
@@ -600,7 +608,7 @@ class InvoiceController extends BaseController
|
||||
$request->boolean('send_email')
|
||||
);
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return response()->json(['message' => $hash_or_response], 200);
|
||||
}
|
||||
@@ -613,7 +621,7 @@ class InvoiceController extends BaseController
|
||||
}
|
||||
});
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
}
|
||||
@@ -626,7 +634,7 @@ class InvoiceController extends BaseController
|
||||
$invoice->service()->markSent()->sendEmail(email_type: $request->input('email_type', $invoice->calculateTemplate('invoice')));
|
||||
});
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
|
||||
@@ -641,7 +649,7 @@ class InvoiceController extends BaseController
|
||||
});
|
||||
|
||||
/* Need to understand which permission are required for the given bulk action ie. view / edit */
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\Cache\Atomic;
|
||||
use App\Events\Payment\PaymentWasUpdated;
|
||||
use App\Factory\PaymentFactory;
|
||||
use App\Filters\PaymentFilters;
|
||||
@@ -214,7 +215,7 @@ class PaymentController extends BaseController
|
||||
|
||||
event('eloquent.created: App\Models\Payment', $payment);
|
||||
|
||||
\Illuminate\Support\Facades\Cache::forget($request->lock_key);
|
||||
Atomic::del($request->lock_key);
|
||||
|
||||
return $this->itemResponse($payment);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class PasswordProtection
|
||||
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
|
||||
|
||||
return $next($request);
|
||||
} elseif (strlen(auth()->user()->oauth_provider_id) > 2 && !auth()->user()->company()->oauth_password_required) {
|
||||
} elseif (strlen(auth()->user()->oauth_provider_id ?? '') > 2 && !auth()->user()->company()->oauth_password_required) {
|
||||
return $next($request);
|
||||
} elseif ($request->header('X-API-OAUTH-PASSWORD') && strlen($request->header('X-API-OAUTH-PASSWORD')) > 1) {
|
||||
//user is attempting to reauth with OAuth - check the token value
|
||||
|
||||
@@ -223,9 +223,7 @@ class StoreClientRequest extends Request
|
||||
/** @var \Illuminate\Support\Collection<\App\Models\Language> */
|
||||
$languages = app('languages');
|
||||
|
||||
$language = $languages->first(function ($item) use ($language_code) {
|
||||
return $item->locale == $language_code;
|
||||
});
|
||||
$language = $languages->firstWhere('locale', $language_code);
|
||||
|
||||
return $language ? (string)$language->id : '';
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
|
||||
namespace App\Http\Requests\Invoice;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Invoice;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Utils\Traits\Invoice\ActionsInvoice;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\Traits\Invoice\ActionsInvoice;
|
||||
use App\Exceptions\DuplicatePaymentException;
|
||||
use App\Helpers\Cache\Atomic;
|
||||
|
||||
class BulkInvoiceRequest extends Request
|
||||
{
|
||||
@@ -78,13 +80,12 @@ class BulkInvoiceRequest extends Request
|
||||
$user = auth()->user();
|
||||
$key = ($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key);
|
||||
|
||||
if (\Illuminate\Support\Facades\Cache::has($key)) {
|
||||
throw new DuplicatePaymentException('Action still processing, please wait. ', 429);
|
||||
}
|
||||
// Calculate TTL: 1 second base, or up to 3 seconds for delete actions
|
||||
$delay = $this->input('action', 'delete') == 'delete' ? (min(count($this->input('ids', [])), 3)) : 1;
|
||||
|
||||
if($this->input('ids', false)){
|
||||
$delay = $this->input('action', 'delete') == 'delete' ? (min(count($this->input('ids', 2)), 3)) : 1;
|
||||
\Illuminate\Support\Facades\Cache::put($key, true, $delay);
|
||||
// Atomic lock: returns false if key already exists (request in progress)
|
||||
if (!Atomic::set($key, true, $delay)) {
|
||||
throw new DuplicatePaymentException('Action still processing, please wait. ', 429);
|
||||
}
|
||||
|
||||
$this->merge(['lock_key' => $key]);
|
||||
|
||||
@@ -102,7 +102,9 @@ class UpdateInvoiceRequest extends Request
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
|
||||
if($this->invoice->company->verifactuEnabled() && $this->invoice->status_id !== \App\Models\Invoice::STATUS_DRAFT){
|
||||
if(request()->input('paid') == 'true'){
|
||||
}
|
||||
elseif($this->invoice->company->verifactuEnabled() && $this->invoice->status_id !== \App\Models\Invoice::STATUS_DRAFT){
|
||||
$validator->errors()->add('status_id', ctrans('texts.locked_invoice'));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,17 @@
|
||||
|
||||
namespace App\Http\Requests\Payment;
|
||||
|
||||
use App\Exceptions\DuplicatePaymentException;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Http\ValidationRules\Credit\CreditsSumRule;
|
||||
use App\Http\ValidationRules\Credit\ValidCreditsRules;
|
||||
use App\Http\ValidationRules\Payment\ValidInvoicesRules;
|
||||
use App\Http\ValidationRules\PaymentAmountsBalanceRule;
|
||||
use App\Http\ValidationRules\ValidPayableInvoicesRule;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Helpers\Cache\Atomic;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Exceptions\DuplicatePaymentException;
|
||||
use App\Http\ValidationRules\Credit\CreditsSumRule;
|
||||
use App\Http\ValidationRules\Credit\ValidCreditsRules;
|
||||
use App\Http\ValidationRules\ValidPayableInvoicesRule;
|
||||
use App\Http\ValidationRules\PaymentAmountsBalanceRule;
|
||||
|
||||
class StorePaymentRequest extends Request
|
||||
{
|
||||
@@ -49,7 +50,7 @@ class StorePaymentRequest extends Request
|
||||
'client_id' => ['bail','required',Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)],
|
||||
'invoices' => ['bail', 'sometimes', 'nullable', 'array', new ValidPayableInvoicesRule()],
|
||||
'invoices.*.amount' => ['bail','required'],
|
||||
'invoices.*.invoice_id' => ['bail','required','distinct', new ValidInvoicesRules($this->all()),Rule::exists('invoices', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)->where('is_deleted',0)],
|
||||
'invoices.*.invoice_id' => ['bail','required','distinct', Rule::exists('invoices', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)->where('is_deleted',0)],
|
||||
'credits.*.credit_id' => ['bail','required','distinct', new ValidCreditsRules($this->all()),Rule::exists('credits', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)->where('is_deleted',0)],
|
||||
'credits.*.amount' => ['bail','required', new CreditsSumRule($this->all())],
|
||||
'amount' => ['bail', 'numeric', new PaymentAmountsBalanceRule(), 'max:99999999999999'],
|
||||
@@ -67,6 +68,58 @@ class StorePaymentRequest extends Request
|
||||
}
|
||||
|
||||
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$invoices = $this->input('invoices', []);
|
||||
$clientId = $this->input('client_id');
|
||||
$invCollection = Invoice::withTrashed()
|
||||
->whereIn('id', array_column($invoices, 'invoice_id'))
|
||||
->get();
|
||||
|
||||
foreach ($invoices as $index => $invoice) {
|
||||
// Check amount exists (if not caught by basic rules)
|
||||
if (!array_key_exists('amount', $invoice)) {
|
||||
$validator->errors()->add("invoices.{$index}.amount", ctrans('texts.amount') . ' required');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find invoice
|
||||
$inv = $invCollection->firstWhere('id', $invoice['invoice_id']);
|
||||
|
||||
if (!$inv) {
|
||||
$validator->errors()->add("invoices.{$index}.invoice_id", ctrans('texts.invoice_not_found'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check client match
|
||||
if ($inv->client_id != $clientId) {
|
||||
$validator->errors()->add("invoices.{$index}", ctrans('texts.invoices_dont_match_client'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check amount validation
|
||||
if ($inv->status_id == Invoice::STATUS_DRAFT && $invoice['amount'] <= $inv->amount) {
|
||||
//catch here nothing to do - we need this to prevent the last elseif triggering
|
||||
} elseif ($invoice['amount'] <= 0 && $inv->amount > 0) {
|
||||
$validator->errors()->add("invoices.{$index}.amount", 'Amount cannot be less than or equal to zero');
|
||||
} elseif ($inv->status_id == Invoice::STATUS_DRAFT && floatval($invoice['amount']) > floatval($inv->amount)) {
|
||||
$validator->errors()->add("invoices.{$index}.amount", 'Amount cannot be greater than invoice balance');
|
||||
} elseif (floatval($invoice['amount']) > floatval($inv->balance)) {
|
||||
$validator->errors()->add("invoices.{$index}.amount", ctrans('texts.amount_greater_than_balance_v5'));
|
||||
} elseif ($inv->is_deleted) {
|
||||
$validator->errors()->add("invoices.{$index}", 'One or more invoices in this request have since been deleted');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
$invoiceIds = array_column($invoices, 'invoice_id');
|
||||
if (count($invoiceIds) !== count(array_unique($invoiceIds))) {
|
||||
$validator->errors()->add('invoices', ctrans('texts.duplicate_invoices_submitted'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
|
||||
@@ -86,7 +139,8 @@ class StorePaymentRequest extends Request
|
||||
|
||||
$hash = $this->ip()."|".$hash_key."|".$client_id."|".$user->company()->company_key;
|
||||
|
||||
if (\Illuminate\Support\Facades\Cache::has($hash)) {
|
||||
// Atomic lock: returns false if key already exists (request in progress)
|
||||
if (!Atomic::set($hash, true, 1)) {
|
||||
throw new DuplicatePaymentException('Duplicate request.', 429);
|
||||
}
|
||||
|
||||
@@ -98,8 +152,6 @@ class StorePaymentRequest extends Request
|
||||
$this->files->set('file', [$this->file('file')]);
|
||||
}
|
||||
|
||||
\Illuminate\Support\Facades\Cache::put($hash, true, 1);
|
||||
|
||||
$invoices_total = 0;
|
||||
$credits_total = 0;
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ class Request extends FormRequest
|
||||
}
|
||||
|
||||
if (array_key_exists('email', $contact)) {
|
||||
$input['contacts'][$key]['email'] = trim($contact['email']);
|
||||
$input['contacts'][$key]['email'] = trim($contact['email'] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ class ValidInvoicesRules implements Rule
|
||||
|
||||
if ($inv->status_id == Invoice::STATUS_DRAFT && $invoice['amount'] <= $inv->amount) {
|
||||
//catch here nothing to do - we need this to prevent the last elseif triggering
|
||||
} elseif($invoice['amount'] <= 0 && $inv->amount > 0) {
|
||||
$this->error_msg = 'Amount cannot be less than or equal to zero';
|
||||
return false;
|
||||
} elseif ($inv->status_id == Invoice::STATUS_DRAFT && floatval($invoice['amount']) > floatval($inv->amount)) {
|
||||
$this->error_msg = 'Amount cannot be greater than invoice balance';
|
||||
return false;
|
||||
|
||||
@@ -27,6 +27,8 @@ use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Services\EDocument\Gateway\Storecove\Storecove;
|
||||
use App\Utils\Traits\Notifications\UserNotifies;
|
||||
|
||||
|
||||
class EInvoicePullDocs implements ShouldQueue
|
||||
{
|
||||
@@ -35,6 +37,7 @@ class EInvoicePullDocs implements ShouldQueue
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
use SavesDocuments;
|
||||
use UserNotifies;
|
||||
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
@@ -97,19 +100,29 @@ class EInvoicePullDocs implements ShouldQueue
|
||||
|
||||
|
||||
if($this->einvoice_received_count > 0) {
|
||||
App::setLocale($company->getLocale());
|
||||
|
||||
$mo = new EmailObject();
|
||||
$mo->subject = ctrans('texts.einvoice_received_subject');
|
||||
$mo->body = ctrans('texts.einvoice_received_body', ['count' => $this->einvoice_received_count]);
|
||||
$mo->text_body = ctrans('texts.einvoice_received_body', ['count' => $this->einvoice_received_count]);
|
||||
$mo->company_key = $company->company_key;
|
||||
$mo->html_template = 'email.template.admin';
|
||||
$mo->to = [new Address($company->owner()->email, $company->owner()->present()->name())];
|
||||
// $mo->email_template_body = 'einvoice_received_body';
|
||||
// $mo->email_template_subject = 'einvoice_received_subject';
|
||||
|
||||
Email::dispatch($mo, $company);
|
||||
foreach ($company->company_users as $company_user) {
|
||||
|
||||
$user = $company_user->user;
|
||||
|
||||
$notifications = $this->findCompanyUserNotificationType($company_user, ['enable_e_invoice_received_notification']);
|
||||
|
||||
if(!array_search('mail', $notifications)){
|
||||
continue;
|
||||
}
|
||||
|
||||
App::setLocale($company->getLocale());
|
||||
|
||||
$mo = new EmailObject();
|
||||
$mo->subject = ctrans('texts.einvoice_received_subject');
|
||||
$mo->body = ctrans('texts.einvoice_received_body', ['count' => $this->einvoice_received_count]);
|
||||
$mo->text_body = ctrans('texts.einvoice_received_body', ['count' => $this->einvoice_received_count]);
|
||||
$mo->company_key = $company->company_key;
|
||||
$mo->html_template = 'email.template.admin';
|
||||
$mo->to = [new Address($user->email, $user->present()->name())];
|
||||
|
||||
Email::dispatch($mo, $company);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -162,6 +162,21 @@ class CheckACHStatus implements ShouldQueue
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Blockonomics payments that have been pending for over 3 days are deleted
|
||||
*/
|
||||
Payment::where('status_id', 1)
|
||||
->where('created_at', '<', now()->startOfDay()->subDays(3))
|
||||
->whereHas('company_gateway', function ($q) {
|
||||
$q->where('gateway_key', 'wbhf02us6owgo7p4nfjd0ymssdshks4d');
|
||||
})
|
||||
->cursor()
|
||||
->each(function ($p) {
|
||||
$p->service()->deletePayment();
|
||||
$p->status_id = \App\Models\Payment::STATUS_FAILED;
|
||||
$p->save();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +197,5 @@ class PaymentNotification implements ShouldQueue
|
||||
|
||||
curl_setopt_array($curl, $opts);
|
||||
curl_exec($curl);
|
||||
curl_close($curl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ class PdfSlot extends Component
|
||||
|
||||
$company_address = "";
|
||||
|
||||
foreach ($this->settings->pdf_variables->company_address as $variable) {
|
||||
foreach ($this->settings->pdf_variables?->company_address as $variable) {
|
||||
$company_address .= "<p>{$variable}</p>";
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,11 @@ class Account extends BaseModel
|
||||
return self::class;
|
||||
}
|
||||
|
||||
public function activities()
|
||||
{
|
||||
return $this->hasMany(Activity::class);
|
||||
}
|
||||
|
||||
public function users(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(User::class)->withTrashed();
|
||||
|
||||
@@ -496,7 +496,7 @@ class Task extends BaseModel
|
||||
{
|
||||
|
||||
return
|
||||
collect(json_decode($this->time_log, true))->map(function ($log) {
|
||||
collect(json_decode($this->time_log ?? '{}', true))->map(function ($log) {
|
||||
|
||||
$parent_entity = $this->client ?? $this->company;
|
||||
$logged = [];
|
||||
|
||||
@@ -517,7 +517,7 @@ class BaseDriver extends AbstractPaymentDriver
|
||||
if ($this->invitation) {
|
||||
return ClientContact::withTrashed()->find($this->invitation->client_contact_id);
|
||||
} elseif (auth()->guard('contact')->user()) {
|
||||
return auth()->guard('contact')->user();
|
||||
return $this->client->contacts()->where('email', auth()->guard('contact')->user()->email)->first() ?? $this->client->contacts()->first();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class BaseRepository
|
||||
$className = $this->getEventClass($entity, 'Archived');
|
||||
|
||||
if (class_exists($className)) {
|
||||
event(new $className($entity, $entity->company, Ninja::eventVars(auth()->guard('api')->user() ? auth()->guard('api')->user()->id : null)));
|
||||
event(new $className($entity, $entity->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class BaseRepository
|
||||
$className = $this->getEventClass($entity, 'Restored');
|
||||
|
||||
if (class_exists($className)) {
|
||||
event(new $className($entity, $fromDeleted, $entity->company, Ninja::eventVars(auth()->guard('api')->user() ? auth()->guard('api')->user()->id : null)));
|
||||
event(new $className($entity, $fromDeleted, $entity->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class BaseRepository
|
||||
$className = $this->getEventClass($entity, 'Deleted');
|
||||
|
||||
if (class_exists($className) && !($entity instanceof Company)) {
|
||||
event(new $className($entity, $entity->company, Ninja::eventVars(auth()->guard('api')->user() ? auth()->guard('api')->user()->id : null)));
|
||||
event(new $className($entity, $entity->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class EntityLevel implements EntityLevelInterface
|
||||
// 'state',
|
||||
// 'postal_code',
|
||||
// 'vat_number',
|
||||
'country_id',
|
||||
// 'country_id',
|
||||
];
|
||||
|
||||
private array $company_settings_fields = [
|
||||
@@ -206,9 +206,12 @@ class EntityLevel implements EntityLevelInterface
|
||||
// $errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
|
||||
// }
|
||||
|
||||
} elseif (empty($client->vat_number)) {
|
||||
$errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
// elseif (empty($client->vat_number)) {
|
||||
// $errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
|
||||
// }
|
||||
|
||||
|
||||
return $errors;
|
||||
|
||||
@@ -186,7 +186,7 @@ class RegistroAlta
|
||||
$destinatario->setNombreRazon($this->invoice->client->present()->name());
|
||||
$destinatario->setCodigoPais('ES')
|
||||
->setIdType('03')
|
||||
->setId($this->invoice->client->id_number);
|
||||
->setId($this->invoice->client->id_number ?? '');
|
||||
|
||||
} else {
|
||||
$locationData = $this->invoice->service()->location();
|
||||
|
||||
@@ -720,12 +720,14 @@ class InvoiceService
|
||||
*
|
||||
*/
|
||||
/** New Invoice - F1 Type */
|
||||
if (empty($this->invoice->client->vat_number) || !in_array($this->invoice->client->country->iso_3166_2, (new \App\DataMapper\Tax\BaseRule())->eu_country_codes)) {
|
||||
// if (empty($this->invoice->client->vat_number) || !in_array($this->invoice->client->country->iso_3166_2, (new \App\DataMapper\Tax\BaseRule())->eu_country_codes)) {
|
||||
|
||||
$this->invoice->backup->guid = 'exempt';
|
||||
$this->invoice->saveQuietly();
|
||||
return $this;
|
||||
} elseif ($new_model && $this->invoice->amount >= 0) {
|
||||
// $this->invoice->backup->guid = 'exempt';
|
||||
// $this->invoice->saveQuietly();
|
||||
// return $this;
|
||||
// } else
|
||||
|
||||
if ($new_model && $this->invoice->amount >= 0) {
|
||||
$this->invoice->backup->document_type = 'F1';
|
||||
$this->invoice->backup->adjustable_amount = (new \App\Services\EDocument\Standards\Verifactu($this->invoice))->run()->registro_alta->calc->getTotal();
|
||||
$this->invoice->backup->parent_invoice_number = $this->invoice->number;
|
||||
|
||||
@@ -51,7 +51,7 @@ class MarkInvoiceDeleted extends AbstractService
|
||||
|
||||
$this->invoice->delete();
|
||||
|
||||
event(new \App\Events\Invoice\InvoiceWasDeleted($this->invoice, $this->invoice->company, \App\Utils\Ninja::eventVars(auth()->guard('api')->user() ? auth()->guard('api')->user()->id : null)));
|
||||
event(new \App\Events\Invoice\InvoiceWasDeleted($this->invoice, $this->invoice->company, \App\Utils\Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
|
||||
return $this->invoice;
|
||||
}
|
||||
|
||||
@@ -824,7 +824,7 @@ class HtmlEngine
|
||||
|
||||
private function getVerifactuQrCode()
|
||||
{
|
||||
if(!($this->entity instanceof \App\Models\Invoice) || !$this->entity->verifactuEnabled() || strlen($this->entity->backup->guid ?? '') < 2 || $this->entity->backup->guid == 'exempt') {
|
||||
if(!($this->entity instanceof \App\Models\Invoice) || !$this->company->verifactuEnabled() || strlen($this->entity->backup->guid ?? '') < 2 || $this->entity->backup->guid == 'exempt') {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
54
composer.lock
generated
54
composer.lock
generated
@@ -497,16 +497,16 @@
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.363.2",
|
||||
"version": "3.363.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "f8b5f125248daa8942144b4771c041a63ec41900"
|
||||
"reference": "0ec2218d32e291b988b1602583032ca5d11f8e8d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f8b5f125248daa8942144b4771c041a63ec41900",
|
||||
"reference": "f8b5f125248daa8942144b4771c041a63ec41900",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0ec2218d32e291b988b1602583032ca5d11f8e8d",
|
||||
"reference": "0ec2218d32e291b988b1602583032ca5d11f8e8d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -588,9 +588,9 @@
|
||||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.363.2"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.363.3"
|
||||
},
|
||||
"time": "2025-11-25T19:04:55+00:00"
|
||||
"time": "2025-11-26T19:05:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "babenkoivan/elastic-adapter",
|
||||
@@ -4480,12 +4480,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/invoiceninja/einvoice.git",
|
||||
"reference": "3f8cc29ed06868495334321999aeb734c20f7e62"
|
||||
"reference": "811eed276e2de35e513a9b03ff14c50fbffcedf3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/3f8cc29ed06868495334321999aeb734c20f7e62",
|
||||
"reference": "3f8cc29ed06868495334321999aeb734c20f7e62",
|
||||
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/811eed276e2de35e513a9b03ff14c50fbffcedf3",
|
||||
"reference": "811eed276e2de35e513a9b03ff14c50fbffcedf3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4503,7 +4503,7 @@
|
||||
"milo/schematron": "^1.0",
|
||||
"nette/php-generator": "^4.1",
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"phpunit/phpunit": "^12",
|
||||
"phpunit/phpunit": "^11",
|
||||
"symfony/console": "^7"
|
||||
},
|
||||
"default-branch": true,
|
||||
@@ -4527,7 +4527,7 @@
|
||||
"source": "https://github.com/invoiceninja/einvoice/tree/main",
|
||||
"issues": "https://github.com/invoiceninja/einvoice/issues"
|
||||
},
|
||||
"time": "2025-10-24T03:07:29+00:00"
|
||||
"time": "2025-11-27T01:49:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "invoiceninja/ubl_invoice",
|
||||
@@ -5986,16 +5986,16 @@
|
||||
},
|
||||
{
|
||||
"name": "league/commonmark",
|
||||
"version": "2.7.1",
|
||||
"version": "2.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/commonmark.git",
|
||||
"reference": "10732241927d3971d28e7ea7b5712721fa2296ca"
|
||||
"reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca",
|
||||
"reference": "10732241927d3971d28e7ea7b5712721fa2296ca",
|
||||
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb",
|
||||
"reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6032,7 +6032,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.8-dev"
|
||||
"dev-main": "2.9-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -6089,7 +6089,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-20T12:47:49+00:00"
|
||||
"time": "2025-11-26T21:48:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/config",
|
||||
@@ -18351,16 +18351,16 @@
|
||||
},
|
||||
{
|
||||
"name": "illuminate/json-schema",
|
||||
"version": "v12.40.1",
|
||||
"version": "v12.40.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/json-schema.git",
|
||||
"reference": "c2b383a6dd66f41208f1443801fe01934c63d030"
|
||||
"reference": "5a8ab3e084c91305196888cb9964b238cce3055b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/json-schema/zipball/c2b383a6dd66f41208f1443801fe01934c63d030",
|
||||
"reference": "c2b383a6dd66f41208f1443801fe01934c63d030",
|
||||
"url": "https://api.github.com/repos/illuminate/json-schema/zipball/5a8ab3e084c91305196888cb9964b238cce3055b",
|
||||
"reference": "5a8ab3e084c91305196888cb9964b238cce3055b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -18393,7 +18393,7 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-11-03T22:27:03+00:00"
|
||||
"time": "2025-11-26T16:51:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laracasts/cypress",
|
||||
@@ -18545,16 +18545,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/boost",
|
||||
"version": "v1.8.2",
|
||||
"version": "v1.8.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/boost.git",
|
||||
"reference": "cf57ba510df44e0d4ed2c1c91360477e92d7d644"
|
||||
"reference": "26572e858e67334952779c0110ca4c378a44d28d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/boost/zipball/cf57ba510df44e0d4ed2c1c91360477e92d7d644",
|
||||
"reference": "cf57ba510df44e0d4ed2c1c91360477e92d7d644",
|
||||
"url": "https://api.github.com/repos/laravel/boost/zipball/26572e858e67334952779c0110ca4c378a44d28d",
|
||||
"reference": "26572e858e67334952779c0110ca4c378a44d28d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -18607,7 +18607,7 @@
|
||||
"issues": "https://github.com/laravel/boost/issues",
|
||||
"source": "https://github.com/laravel/boost"
|
||||
},
|
||||
"time": "2025-11-20T18:13:17+00:00"
|
||||
"time": "2025-11-26T14:12:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/mcp",
|
||||
|
||||
@@ -17,8 +17,8 @@ return [
|
||||
'require_https' => env('REQUIRE_HTTPS', true),
|
||||
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
||||
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
||||
'app_version' => env('APP_VERSION', '5.12.36'),
|
||||
'app_tag' => env('APP_TAG', '5.12.36'),
|
||||
'app_version' => env('APP_VERSION', '5.12.37'),
|
||||
'app_tag' => env('APP_TAG', '5.12.37'),
|
||||
'minimum_client_version' => '5.0.16',
|
||||
'terms_version' => '1.0.1',
|
||||
'api_secret' => env('API_SECRET', false),
|
||||
|
||||
@@ -5666,6 +5666,8 @@ $lang = array(
|
||||
'thank_you_for_feedback' => 'Thank you for your feedback!',
|
||||
'use_legacy_editor' => 'Use Legacy Wysiwyg Editor',
|
||||
'use_legacy_editor_help' => 'Use the TinyMCE editor.',
|
||||
'enable_e_invoice_received_notification' => 'Enable E-Invoice Received Notification',
|
||||
'enable_e_invoice_received_notification_help' => 'Receive an email notification when a new E-Invoice is received.',
|
||||
);
|
||||
|
||||
return $lang;
|
||||
|
||||
@@ -5662,6 +5662,9 @@ Développe automatiquement la section des notes dans le tableau de produits pour
|
||||
'invoice_period' => 'Période de facturation',
|
||||
'invoice_period_help' => 'Définit la période durant laquelle les services ont été fournis.',
|
||||
'paused_recurring_invoice_helper' => 'Attention ! Lors du redémarrage d\'une facture récurrente, assurez-vous que la prochaine date d\'envoi soit dans le futur.',
|
||||
'thank_you_for_feedback' => 'Merci pour vos commentaires !',
|
||||
'use_legacy_editor' => 'Utiliser l\'éditeur classique Wysiwyg',
|
||||
'use_legacy_editor_help' => 'Utiliser l\'éditeur TinyMCE.',
|
||||
);
|
||||
|
||||
return $lang;
|
||||
|
||||
@@ -5663,6 +5663,9 @@ $lang = array(
|
||||
'invoice_period' => 'Hóa đơn Period',
|
||||
'invoice_period_help' => 'Xác định khoảng thời gian cung cấp dịch vụ.',
|
||||
'paused_recurring_invoice_helper' => 'Thận trọng! Khi khởi động lại Định kỳ Hóa đơn , hãy đảm bảo ngày gửi tiếp theo là trong tương lai.',
|
||||
'thank_you_for_feedback' => 'Cảm ơn phản hồi của bạn!',
|
||||
'use_legacy_editor' => 'Sử dụng Legacy Wysiwyg Editor',
|
||||
'use_legacy_editor_help' => 'Sử dụng trình soạn thảo TinyMCE.',
|
||||
);
|
||||
|
||||
return $lang;
|
||||
|
||||
@@ -22,9 +22,6 @@ class ActivityApiTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@@ -22,9 +22,6 @@ class ApplePayDomainMerchantUrlTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
208
tests/Feature/AtomicCacheLockTest.php
Normal file
208
tests/Feature/AtomicCacheLockTest.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Helpers\Cache\Atomic;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class AtomicCacheLockTest extends TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* Test that Atomic::set() prevents duplicate requests.
|
||||
*/
|
||||
public function test_atomic_set_prevents_duplicate_lock()
|
||||
{
|
||||
$key = 'test-lock-key-' . uniqid();
|
||||
|
||||
// First request should succeed
|
||||
$result1 = Atomic::set($key, true, 1);
|
||||
$this->assertTrue($result1, 'First atomic set should succeed');
|
||||
|
||||
// Second request with same key should fail
|
||||
$result2 = Atomic::set($key, true, 1);
|
||||
$this->assertFalse($result2, 'Second atomic set should fail (key already exists)');
|
||||
|
||||
// Cleanup
|
||||
Atomic::del($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that Atomic::del() removes the lock.
|
||||
*/
|
||||
public function test_atomic_del_removes_lock()
|
||||
{
|
||||
$key = 'test-lock-key-' . uniqid();
|
||||
|
||||
// Set lock
|
||||
Atomic::set($key, true, 1);
|
||||
$this->assertNotNull(Atomic::get($key), 'Lock should exist after set');
|
||||
|
||||
// Delete lock
|
||||
Atomic::del($key);
|
||||
$this->assertNull(Atomic::get($key), 'Lock should not exist after delete');
|
||||
|
||||
// Should be able to set again after delete
|
||||
$result = Atomic::set($key, true, 1);
|
||||
$this->assertTrue($result, 'Should be able to set lock again after delete');
|
||||
|
||||
// Cleanup
|
||||
Atomic::del($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that lock expires after TTL.
|
||||
*/
|
||||
public function test_lock_expires_after_ttl()
|
||||
{
|
||||
$key = 'test-lock-key-' . uniqid();
|
||||
|
||||
// Set lock with 1 second TTL
|
||||
$result1 = Atomic::set($key, true, 1);
|
||||
$this->assertTrue($result1);
|
||||
|
||||
// Immediate retry should fail
|
||||
$result2 = Atomic::set($key, true, 1);
|
||||
$this->assertFalse($result2);
|
||||
|
||||
// Wait for TTL to expire
|
||||
sleep(2);
|
||||
|
||||
// Should succeed after TTL
|
||||
$result3 = Atomic::set($key, true, 1);
|
||||
$this->assertTrue($result3, 'Lock should be settable after TTL expires');
|
||||
|
||||
// Cleanup
|
||||
Atomic::del($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that Atomic::set() is truly atomic (no race condition).
|
||||
*/
|
||||
public function test_atomic_set_is_truly_atomic()
|
||||
{
|
||||
$key = 'race-condition-test-' . uniqid();
|
||||
$successCount = 0;
|
||||
|
||||
// Simulate 10 simultaneous attempts
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
if (Atomic::set($key, true, 1)) {
|
||||
$successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Only ONE should succeed
|
||||
$this->assertEquals(1, $successCount, 'Only one atomic set should succeed in race condition');
|
||||
|
||||
// Cleanup
|
||||
Atomic::del($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that Cache::add() fallback works when Redis fails.
|
||||
*/
|
||||
public function test_cache_fallback_when_redis_unavailable()
|
||||
{
|
||||
// This test validates the fallback mechanism in Atomic class
|
||||
// When Redis is unavailable, it should use Cache::add()
|
||||
|
||||
$key = 'fallback-test-' . uniqid();
|
||||
|
||||
// Clear any existing key
|
||||
Cache::forget($key);
|
||||
|
||||
// Test Cache::add directly (what Atomic uses as fallback)
|
||||
$result1 = Cache::add($key, true, 1);
|
||||
$this->assertTrue($result1, 'First Cache::add should succeed');
|
||||
|
||||
$result2 = Cache::add($key, true, 1);
|
||||
$this->assertFalse($result2, 'Second Cache::add should fail (atomic behavior)');
|
||||
|
||||
// Cleanup
|
||||
Cache::forget($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that payment requests with same hash are blocked.
|
||||
*/
|
||||
public function test_duplicate_payment_request_blocked()
|
||||
{
|
||||
// Simulate payment request hash
|
||||
$invoiceIds = ['inv_001', 'inv_002'];
|
||||
$clientId = 'client_123';
|
||||
$ip = '127.0.0.1';
|
||||
$companyKey = 'test_company';
|
||||
|
||||
$hashKey = implode(',', $invoiceIds);
|
||||
$hash = $ip . "|" . $hashKey . "|" . $clientId . "|" . $companyKey;
|
||||
|
||||
// First payment request should succeed
|
||||
$result1 = Atomic::set($hash, true, 1);
|
||||
$this->assertTrue($result1, 'First payment request should succeed');
|
||||
|
||||
// Duplicate payment request with same invoices should fail
|
||||
$result2 = Atomic::set($hash, true, 1);
|
||||
$this->assertFalse($result2, 'Duplicate payment request should be blocked');
|
||||
|
||||
// Cleanup
|
||||
Atomic::del($hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that different invoice combinations generate different keys.
|
||||
*/
|
||||
public function test_different_invoice_combinations_generate_different_keys()
|
||||
{
|
||||
$ip = '127.0.0.1';
|
||||
$clientId = 'client_123';
|
||||
$companyKey = 'test_company';
|
||||
|
||||
// First payment: invoices A and B
|
||||
$hash1 = $ip . "|" . implode(',', ['inv_A', 'inv_B']) . "|" . $clientId . "|" . $companyKey;
|
||||
|
||||
// Second payment: invoices C and D
|
||||
$hash2 = $ip . "|" . implode(',', ['inv_C', 'inv_D']) . "|" . $clientId . "|" . $companyKey;
|
||||
|
||||
// Both should succeed (different keys)
|
||||
$result1 = Atomic::set($hash1, true, 1);
|
||||
$this->assertTrue($result1, 'First payment should succeed');
|
||||
|
||||
$result2 = Atomic::set($hash2, true, 1);
|
||||
$this->assertTrue($result2, 'Second payment with different invoices should succeed');
|
||||
|
||||
// But duplicate of first should fail
|
||||
$result3 = Atomic::set($hash1, true, 1);
|
||||
$this->assertFalse($result3, 'Duplicate of first payment should fail');
|
||||
|
||||
// Cleanup
|
||||
Atomic::del($hash1);
|
||||
Atomic::del($hash2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that lock cleanup allows subsequent requests.
|
||||
*/
|
||||
public function test_lock_cleanup_allows_subsequent_requests()
|
||||
{
|
||||
$hash = 'payment-hash-' . uniqid();
|
||||
|
||||
// First request succeeds
|
||||
$result1 = Atomic::set($hash, true, 1);
|
||||
$this->assertTrue($result1);
|
||||
|
||||
// Second request fails (duplicate)
|
||||
$result2 = Atomic::set($hash, true, 1);
|
||||
$this->assertFalse($result2);
|
||||
|
||||
// Cleanup (simulating controller cleanup after payment created)
|
||||
Atomic::del($hash);
|
||||
|
||||
// Third request should succeed after cleanup
|
||||
$result3 = Atomic::set($hash, true, 1);
|
||||
$this->assertTrue($result3, 'Request should succeed after lock cleanup');
|
||||
|
||||
// Cleanup
|
||||
Atomic::del($hash);
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,6 @@ class BankTransactionRuleTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@@ -28,9 +28,6 @@ class BankTransactionTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@@ -24,9 +24,6 @@ class BankIntegrationApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -34,9 +31,6 @@ class BankIntegrationApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,6 @@ class BankTransactionApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -37,9 +34,6 @@ class BankTransactionApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@ class BankTransactionRuleApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -34,9 +31,6 @@ class BankTransactionRuleApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -98,9 +98,6 @@ class BaseApiTest extends TestCase
|
||||
public string $low_token;
|
||||
|
||||
public string $owner_token;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@@ -33,9 +33,6 @@ class CancelInvoiceTest extends TestCase
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -29,9 +29,6 @@ class ClassificationTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
}
|
||||
|
||||
@@ -41,8 +41,6 @@ class ClientApiTest extends TestCase
|
||||
use MockAccountData;
|
||||
use ClientGroupSettingsSaver;
|
||||
|
||||
public $faker;
|
||||
|
||||
public $settings;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -51,10 +49,6 @@ class ClientApiTest extends TestCase
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
@@ -1200,8 +1194,7 @@ class ClientApiTest extends TestCase
|
||||
$response->assertStatus(200);
|
||||
|
||||
$arr = $response->json();
|
||||
|
||||
nlog($arr);
|
||||
|
||||
$this->assertEquals('3', $arr['data']['settings']['language_id']);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,6 @@ class ClientDeletedInvoiceCreationTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -26,8 +26,6 @@ class ClientGatewayTokenApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
protected $faker;
|
||||
protected CompanyGateway $cg;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -41,9 +39,6 @@ class ClientGatewayTokenApiTest extends TestCase
|
||||
if (! config('ninja.testvars.stripe')) {
|
||||
$this->markTestSkipped('Skip test no company gateways installed');
|
||||
}
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -23,9 +23,6 @@ class ClientModelTest extends TestCase
|
||||
{
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@@ -24,9 +24,6 @@ class ClientPresenterTest extends TestCase
|
||||
{
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@@ -46,9 +46,6 @@ class ClientTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
public $client_id;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -56,9 +53,6 @@ class ClientTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
// $this->withoutExceptionHandling();
|
||||
|
||||
@@ -33,8 +33,6 @@ class CompanyGatewayApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
use CompanyGatewayFeesAndLimitsSaver;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -44,9 +42,6 @@ class CompanyGatewayApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +47,6 @@ class CompanyGatewayResolutionTest extends TestCase
|
||||
if (! config('ninja.testvars.stripe')) {
|
||||
$this->markTestSkipped('Skip test no company gateways installed');
|
||||
}
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -29,8 +29,6 @@ class CompanyGatewayTest extends TestCase
|
||||
{
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
|
||||
public $faker;
|
||||
// use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
|
||||
@@ -32,7 +32,6 @@ class CompanySettingsTest extends TestCase
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
// use RefreshDatabase;
|
||||
|
||||
public function setUp(): void
|
||||
@@ -43,7 +42,6 @@ class CompanySettingsTest extends TestCase
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
$this->withoutExceptionHandling();
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
@@ -36,15 +36,9 @@ class CompanyTest extends TestCase
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
// use DatabaseTransactions;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->makeTestData();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,6 @@ class CompanyTokenApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -41,9 +38,6 @@ class CompanyTokenApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
|
||||
@@ -30,17 +30,11 @@ class CreditTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -30,8 +30,6 @@ class DeleteInvoiceTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
use MakesHash;
|
||||
|
||||
protected function setUp(): void
|
||||
|
||||
@@ -38,17 +38,11 @@ class DesignApiTest extends TestCase
|
||||
use MockAccountData;
|
||||
|
||||
public $id;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
}
|
||||
|
||||
public function testSelectiveDefaultDesignUpdatesInvoice()
|
||||
|
||||
@@ -30,9 +30,6 @@ class DocumentsApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -40,9 +37,6 @@ class DocumentsApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -48,10 +48,6 @@ class PeppolTest extends TestCase
|
||||
use MockAccountData;
|
||||
|
||||
protected int $iterations = 10;
|
||||
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -184,6 +180,169 @@ class PeppolTest extends TestCase
|
||||
return compact('company', 'client', 'invoice');
|
||||
}
|
||||
|
||||
|
||||
public function testBeToBeWithSpecialLineItemConfiguration()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->address1 = 'Dudweilerstr. 34b';
|
||||
$settings->city = 'Ost Alessa';
|
||||
$settings->state = 'Bayern';
|
||||
$settings->postal_code = '98060';
|
||||
$settings->vat_number = 'BE923356489';
|
||||
$settings->id_number = '991-00110-12';
|
||||
$settings->country_id = '56';
|
||||
$settings->currency_id = '3';
|
||||
|
||||
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
|
||||
|
||||
$fib = new FinancialInstitutionBranch();
|
||||
$fib->ID = "DEUTDEMMXXX"; //BIC
|
||||
// $fib->Name = 'Deutsche Bank';
|
||||
|
||||
$pfa = new PayeeFinancialAccount();
|
||||
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
|
||||
$id->value = 'DE89370400440532013000';
|
||||
$pfa->ID = $id;
|
||||
$pfa->Name = 'PFA-NAME';
|
||||
|
||||
$pfa->FinancialInstitutionBranch = $fib;
|
||||
|
||||
$pm = new PaymentMeans();
|
||||
$pm->PayeeFinancialAccount = $pfa;
|
||||
|
||||
$pmc = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\PaymentMeansCode();
|
||||
$pmc->value = '30';
|
||||
|
||||
$pm->PaymentMeansCode = $pmc;
|
||||
|
||||
$einvoice->PaymentMeans[] = $pm;
|
||||
|
||||
$stub = new \stdClass();
|
||||
$stub->Invoice = $einvoice;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'e_invoice' => $stub,
|
||||
]);
|
||||
|
||||
$cu = CompanyUserFactory::create($this->user->id, $company->id, $this->account->id);
|
||||
$cu->is_owner = true;
|
||||
$cu->is_admin = true;
|
||||
$cu->is_locked = false;
|
||||
$cu->save();
|
||||
|
||||
$client_settings = ClientSettings::defaults();
|
||||
$client_settings->currency_id = '3';
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'name' => 'German Client Name',
|
||||
'address1' => 'Kinderhausen 96b',
|
||||
'address2' => 'Apt. 842',
|
||||
'city' => 'Süd Jessestadt',
|
||||
'state' => 'Bayern',
|
||||
'postal_code' => '33323',
|
||||
'country_id' => 56,
|
||||
'routing_id' => 'ABC1234',
|
||||
'settings' => $client_settings,
|
||||
'vat_number' => 'BE173655434',
|
||||
]);
|
||||
|
||||
$item = new InvoiceItem();
|
||||
$item->product_key = "Product Key";
|
||||
$item->notes = "Product Description";
|
||||
$item->cost = 795;
|
||||
$item->quantity = 13.5;
|
||||
$item->discount = 0;
|
||||
$item->is_amount_discount = false;
|
||||
$item->tax_rate1 = 21;
|
||||
$item->tax_name1 = 'TVA';
|
||||
|
||||
$item2 = new InvoiceItem();
|
||||
$item2->product_key = "Product Key 2";
|
||||
$item2->notes = "Product Description 2";
|
||||
$item2->cost = 795;
|
||||
$item2->quantity = 2;
|
||||
$item2->discount = 0;
|
||||
$item2->is_amount_discount = false;
|
||||
$item2->tax_rate1 = 21;
|
||||
$item2->tax_name1 = 'TVA';
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $client->id,
|
||||
'discount' => 0,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'status_id' => 1,
|
||||
'tax_rate1' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'line_items' => [$item, $item2],
|
||||
'number' => 'DE-'.rand(1000, 100000),
|
||||
'date' => now()->format('Y-m-d'),
|
||||
'due_date' => now()->addDays(30)->format('Y-m-d'),
|
||||
'is_amount_discount' => false,
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice();
|
||||
|
||||
$repo = new InvoiceRepository();
|
||||
$invoice = $repo->save([], $invoice);
|
||||
|
||||
$invoice->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(14910.23, $invoice->amount);
|
||||
$this->assertEquals(2587.73, $invoice->total_taxes);
|
||||
|
||||
$peppol = new Peppol($invoice);
|
||||
$peppol->setInvoiceDefaults();
|
||||
$peppol->run();
|
||||
|
||||
$be_invoice = $peppol->getInvoice();
|
||||
|
||||
$this->assertNotNull($be_invoice);
|
||||
|
||||
$e = new EInvoice();
|
||||
$xml = $e->encode($be_invoice, 'xml');
|
||||
|
||||
$this->assertNotNull($xml);
|
||||
|
||||
$errors = $e->validate($be_invoice);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
nlog($xml);
|
||||
nlog($errors);
|
||||
}
|
||||
|
||||
$this->assertCount(0, $errors);
|
||||
|
||||
$xml = $peppol->toXml();
|
||||
|
||||
try {
|
||||
$processor = new \Saxon\SaxonProcessor();
|
||||
} catch (\Throwable $e) {
|
||||
$this->markTestSkipped('saxon not installed');
|
||||
}
|
||||
|
||||
$validator = new XsltDocumentValidator($xml);
|
||||
$validator->validate();
|
||||
|
||||
if (count($validator->getErrors()) > 0) {
|
||||
nlog($xml);
|
||||
nlog($validator->getErrors());
|
||||
}
|
||||
|
||||
$this->assertCount(0, $validator->getErrors());
|
||||
|
||||
}
|
||||
|
||||
public function testInvoicePeriodValidation()
|
||||
{
|
||||
|
||||
|
||||
@@ -37,15 +37,9 @@ class VerifactuApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->makeTestData();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,6 @@ class EntityPaidToDateTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -32,9 +32,6 @@ class ExpenseApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -42,9 +39,6 @@ class ExpenseApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,6 @@ class ExpenseCategoryApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,15 +24,9 @@ class ReportApiTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
@@ -40,15 +40,9 @@ class ReportPreviewTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
@@ -23,17 +23,11 @@ class GroupSettingTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -33,9 +33,6 @@ class CsvImportTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@@ -35,8 +35,6 @@ class QuickbooksTest extends TestCase
|
||||
protected $quickbooks;
|
||||
protected $data;
|
||||
protected QuickbooksService $qb;
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -49,8 +47,6 @@ class QuickbooksTest extends TestCase
|
||||
// elseif(Company::whereNotNull('quickbooks')->count() == 0){
|
||||
// $this->markTestSkipped('No need to run this test on Travis');
|
||||
// }
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
}
|
||||
|
||||
public function createQbProduct()
|
||||
|
||||
@@ -40,7 +40,6 @@ class InvitationTest extends TestCase
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
}
|
||||
|
||||
public function testInvoiceCreationAfterInvoiceMarkedSent()
|
||||
@@ -53,7 +52,9 @@ class InvitationTest extends TestCase
|
||||
$account->default_company_id = $company->id;
|
||||
$account->save();
|
||||
|
||||
$fake_email = $this->faker->email();
|
||||
$faker = \Faker\Factory::create();
|
||||
|
||||
$fake_email = $faker->email();
|
||||
|
||||
$user = User::where('email', $fake_email)->first();
|
||||
|
||||
|
||||
@@ -34,17 +34,11 @@ class InvoiceEmailTest extends TestCase
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
use GeneratesCounter;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -33,9 +33,6 @@ class InvoiceLinkTasksTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -33,15 +33,9 @@ class InvoiceTaxReportTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->makeTestData();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,17 +37,11 @@ class InvoiceTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -35,9 +35,6 @@ class MigrationTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -32,8 +32,6 @@ class PlanTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
protected $faker;
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -41,9 +39,6 @@ class PlanTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@ class NotificationTest extends TestCase
|
||||
{
|
||||
use UserNotifies;
|
||||
use MockAccountData;
|
||||
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -48,6 +45,49 @@ class NotificationTest extends TestCase
|
||||
}
|
||||
|
||||
|
||||
public function testEInvoiceReceivedNotification()
|
||||
{
|
||||
|
||||
$u = User::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'email' => $this->faker->safeEmail(),
|
||||
'confirmation_code' => uniqid("st", true),
|
||||
]);
|
||||
|
||||
$company_token = new CompanyToken();
|
||||
$company_token->user_id = $u->id;
|
||||
$company_token->company_id = $this->company->id;
|
||||
$company_token->account_id = $this->account->id;
|
||||
$company_token->name = 'test token';
|
||||
$company_token->token = Str::random(64);
|
||||
$company_token->is_system = true;
|
||||
$company_token->save();
|
||||
|
||||
$u->companies()->attach($this->company->id, [
|
||||
'account_id' => $this->account->id,
|
||||
'is_owner' => 1,
|
||||
'is_admin' => 1,
|
||||
'is_locked' => 0,
|
||||
'notifications' => CompanySettings::notificationDefaults(),
|
||||
'settings' => null,
|
||||
]);
|
||||
|
||||
$company_user = CompanyUser::where('user_id', $u->id)->where('company_id', $this->company->id)->first();
|
||||
|
||||
$notifications = new \stdClass();
|
||||
$notifications->email = ["enable_e_invoice_received_notification"];
|
||||
$company_user->update(['notifications' => (array)$notifications]);
|
||||
|
||||
$notifications = $this->findCompanyUserNotificationType($company_user, ['enable_e_invoice_received_notification']);
|
||||
|
||||
$this->assertEquals(0, array_search('mail', $notifications));
|
||||
|
||||
$notifications = $this->findCompanyUserNotificationType($company_user, ['non_existant_notification']);
|
||||
|
||||
$this->assertFalse(array_search('mail', $notifications));
|
||||
|
||||
}
|
||||
|
||||
public function testEntityViewedNotificationWithEntityLate()
|
||||
{
|
||||
// ['all_notifications', 'all_user_notifications', 'invoice_created_user', 'invoice_sent_user', 'invoice_viewed_user', 'invoice_late_user'];
|
||||
|
||||
@@ -39,9 +39,6 @@ class PaymentTermsApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
|
||||
@@ -42,17 +42,11 @@ class PaymentTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -36,17 +36,11 @@ class PaymentV2Test extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -42,9 +42,6 @@ class AutoUnappliedPaymentTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -41,9 +41,6 @@ class CreditPaymentTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -35,9 +35,6 @@ class DeletePaymentTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -35,9 +35,6 @@ class StorePaymentValidationTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -35,9 +35,6 @@ class UnappliedPaymentDeleteTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->makeTestData();
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
|
||||
@@ -32,9 +32,6 @@ class UnappliedPaymentRefundTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->makeTestData();
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
|
||||
@@ -32,17 +32,11 @@ class ProductTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
|
||||
@@ -35,9 +35,6 @@ class ProjectApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -45,9 +42,6 @@ class ProjectApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -32,15 +32,9 @@ class PurchaseOrderTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
$this->makeTestData();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@ class QuoteReminderTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -50,9 +47,6 @@ class QuoteReminderTest extends TestCase
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -35,17 +35,11 @@ class QuoteTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -32,9 +32,6 @@ class RecurringExpenseApiTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -42,9 +39,6 @@ class RecurringExpenseApiTest extends TestCase
|
||||
$this->makeTestData();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,17 +42,11 @@ class RecurringInvoiceTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
|
||||
@@ -30,17 +30,11 @@ class RecurringQuoteTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
|
||||
@@ -40,9 +40,6 @@ class RecurringQuotesTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
|
||||
@@ -40,9 +40,6 @@ class RefundTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -52,9 +49,6 @@ class RefundTest extends TestCase
|
||||
);
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -40,9 +40,6 @@ class ReminderTest extends TestCase
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -50,9 +47,6 @@ class ReminderTest extends TestCase
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -44,9 +44,6 @@ class ReverseInvoiceTest extends TestCase
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -29,17 +29,11 @@ class ScheduleEntityTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
|
||||
public $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -44,17 +44,11 @@ class SchedulerTest extends TestCase
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
use MakesDates;
|
||||
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
@@ -28,9 +28,6 @@ class ShopInvoiceTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -38,9 +35,6 @@ class ShopInvoiceTest extends TestCase
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
|
||||
Model::reguard();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user