Merge pull request #11458 from turbo124/v5-develop

v5.12.37
This commit is contained in:
David Bomba
2025-12-02 09:24:26 +11:00
committed by GitHub
150 changed files with 812 additions and 616 deletions

View File

@@ -1 +1 @@
5.12.36
5.12.37

View File

@@ -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();

View 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);
}
}
}

View File

@@ -144,6 +144,7 @@ class PaymentController extends Controller
$data = [
'invoice' => $invoice,
'key' => false,
'_key' => false,
'invitation' => $invitation,
'variables' => $variables,
];

View File

@@ -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());
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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 : '';

View File

@@ -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]);

View File

@@ -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'));
}

View File

@@ -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;

View File

@@ -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'] ?? '');
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}
});

View File

@@ -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();
});
}
}
}

View File

@@ -197,6 +197,5 @@ class PaymentNotification implements ShouldQueue
curl_setopt_array($curl, $opts);
curl_exec($curl);
curl_close($curl);
}
}

View File

@@ -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>";
}

View File

@@ -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();

View File

@@ -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 = [];

View File

@@ -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;
}

View File

@@ -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)));
}
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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),

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -22,9 +22,6 @@ class ActivityApiTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
public $faker;
protected function setUp(): void
{
parent::setUp();

View File

@@ -22,9 +22,6 @@ class ApplePayDomainMerchantUrlTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
public $faker;
protected function setUp(): void
{
parent::setUp();

View 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);
}
}

View File

@@ -29,9 +29,6 @@ class BankTransactionRuleTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
public $faker;
protected function setUp(): void
{
parent::setUp();

View File

@@ -28,9 +28,6 @@ class BankTransactionTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
public $faker;
protected function setUp(): void
{
parent::setUp();

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -98,9 +98,6 @@ class BaseApiTest extends TestCase
public string $low_token;
public string $owner_token;
public $faker;
protected function setUp(): void
{
parent::setUp();

View File

@@ -33,9 +33,6 @@ class CancelInvoiceTest extends TestCase
$this->withoutMiddleware(
ThrottleRequests::class
);
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -29,9 +29,6 @@ class ClassificationTest extends TestCase
protected function setUp(): void
{
parent::setUp();
$this->faker = \Faker\Factory::create();
$this->makeTestData();
}

View File

@@ -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']);
}

View File

@@ -31,9 +31,6 @@ class ClientDeletedInvoiceCreationTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -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();

View File

@@ -23,9 +23,6 @@ class ClientModelTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
public $faker;
protected function setUp(): void
{
parent::setUp();

View File

@@ -24,9 +24,6 @@ class ClientPresenterTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
public $faker;
protected function setUp(): void
{
parent::setUp();

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -29,8 +29,6 @@ class CompanyGatewayTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
public $faker;
// use RefreshDatabase;
protected function setUp(): void

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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(

View File

@@ -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();

View File

@@ -30,8 +30,6 @@ class DeleteInvoiceTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
public $faker;
use MakesHash;
protected function setUp(): void

View File

@@ -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()

View File

@@ -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();
}

View File

@@ -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()
{

View File

@@ -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();
}

View File

@@ -37,9 +37,6 @@ class EntityPaidToDateTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -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();
}

View File

@@ -36,9 +36,6 @@ class ExpenseCategoryApiTest extends TestCase
$this->makeTestData();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
}

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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();

View File

@@ -33,9 +33,6 @@ class CsvImportTest extends TestCase
{
use MakesHash;
use MockAccountData;
public $faker;
protected function setUp(): void
{
parent::setUp();

View File

@@ -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()

View File

@@ -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();

View File

@@ -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();

View File

@@ -33,9 +33,6 @@ class InvoiceLinkTasksTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -35,9 +35,6 @@ class MigrationTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -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();
}

View File

@@ -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'];

View File

@@ -39,9 +39,6 @@ class PaymentTermsApiTest extends TestCase
$this->makeTestData();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->withoutMiddleware(

View File

@@ -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();

View File

@@ -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();

View File

@@ -42,9 +42,6 @@ class AutoUnappliedPaymentTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -41,9 +41,6 @@ class CreditPaymentTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -35,9 +35,6 @@ class DeletePaymentTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -35,9 +35,6 @@ class StorePaymentValidationTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -35,9 +35,6 @@ class UnappliedPaymentDeleteTest extends TestCase
protected function setUp(): void
{
parent::setUp();
$this->faker = \Faker\Factory::create();
$this->makeTestData();
$this->withoutExceptionHandling();

View File

@@ -32,9 +32,6 @@ class UnappliedPaymentRefundTest extends TestCase
protected function setUp(): void
{
parent::setUp();
$this->faker = \Faker\Factory::create();
$this->makeTestData();
$this->withoutExceptionHandling();

View File

@@ -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(

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -40,9 +40,6 @@ class RecurringQuotesTest extends TestCase
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->withoutMiddleware(

View File

@@ -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();

View File

@@ -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();

View File

@@ -44,9 +44,6 @@ class ReverseInvoiceTest extends TestCase
$this->withoutMiddleware(
ThrottleRequests::class
);
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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