Merge pull request #11592 from turbo124/v5-develop

v5.12.49
This commit is contained in:
David Bomba
2026-01-24 15:41:32 +11:00
committed by GitHub
22 changed files with 1260 additions and 299 deletions

View File

@@ -1 +1 @@
5.12.48
5.12.49

View File

@@ -200,6 +200,40 @@ class LoginController extends BaseController
}
}
public function refreshReact(Request $request)
{
$truth = app()->make(TruthSource::class);
if ($truth->getCompanyToken()) {
$company_token = $truth->getCompanyToken();
} else {
$company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first();
}
$cu = CompanyUser::query()
->where('user_id', $company_token->user_id);
if ($cu->count() == 0) {
return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400);
}
$cu->first()->account->companies->each(function ($company) use ($cu, $request) {
if ($company->tokens()->where('is_system', true)->count() == 0) {
(new CreateCompanyToken($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')))->handle();
}
});
if ($request->has('current_company') && $request->input('current_company') == 'true') {
$cu->where('company_id', $company_token->company_id);
}
if (Ninja::isHosted() && !$cu->first()->is_owner && !$cu->first()->user->account->isEnterprisePaidClient()) {
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
}
return $this->refreshReactResponse($cu);
}
/**
* Refreshes the data feed with the current Company User.
*

View File

@@ -297,6 +297,44 @@ class BaseController extends Controller
return response()->make($error, $httpErrorCode, $headers);
}
/**
* Heavily reduced refresh query to reduce DB burden
*
* @param Builder $query
* @return Response| \Illuminate\Http\JsonResponse
*/
protected function refreshReactResponse($query)
{
$this->manager->parseIncludes([
'account',
'user.company_user',
'token',
'company.tax_rates',
]);
$this->serializer = request()->input('serializer') ?: EntityTransformer::API_SERIALIZER_ARRAY;
if ($this->serializer === EntityTransformer::API_SERIALIZER_JSON) {
$this->manager->setSerializer(new JsonApiSerializer());
} else {
$this->manager->setSerializer(new ArraySerializer());
}
$transformer = new $this->entity_transformer($this->serializer);
$limit = $this->resolveQueryLimit();
$paginator = $query->paginate($limit);
/** @phpstan-ignore-next-line */
$query = $paginator->getCollection(); // @phpstan-ignore-line
$resource = new Collection($query, $transformer, $this->entity_type);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return $this->response($this->manager->createData($resource)->toArray());
}
/**
* Refresh API response with latest cahnges
*
@@ -681,7 +719,10 @@ class BaseController extends Controller
// Set created_at to current time to filter out all existing related records
// (designs, documents, groups, etc.) for a minimal response payload
request()->merge(['created_at' => time()]);
return $this->miniLoadResponse($query);
//2026-01-23: Improve Login Performance for react.
return $this->refreshReactResponse($query);
// return $this->miniLoadResponse($query);
}
elseif ($user->getCompany()->is_large) {
$this->manager->parseIncludes($this->mini_load);

View File

@@ -607,7 +607,7 @@ class ProjectController extends BaseController
$this->entity_transformer = InvoiceTransformer::class;
$this->entity_type = Invoice::class;
$invoice = $this->project_repo->invoice($project);
$invoice = $this->project_repo->invoice(collect([$project]));
return $this->itemResponse($invoice);
}

View File

@@ -44,6 +44,9 @@ class InvoiceTaxSummary implements ShouldQueue
public function handle()
{
nlog("InvoiceTaxSummary:: Starting job @ " . now()->toDateTimeString());
$start = now();
$currentUtcHour = now()->hour;
$transitioningTimezones = $this->getTransitioningTimezones($currentUtcHour);
@@ -56,6 +59,8 @@ class InvoiceTaxSummary implements ShouldQueue
$this->processCompanyTaxSummary($company);
}
}
nlog("InvoiceTaxSummary:: Job completed in " . now()->diffInSeconds($start) . " seconds");
}
private function getTransitioningTimezones($utcHour)
@@ -117,7 +122,11 @@ class InvoiceTaxSummary implements ShouldQueue
}
// Get companies that have timezone_id in their JSON settings matching the transitioning timezones
return Company::whereRaw("JSON_EXTRACT(settings, '$.timezone_id') IN (" . implode(',', $timezoneIds) . ")")->get();
$companies = Company::whereRaw("JSON_EXTRACT(settings, '$.timezone_id') IN (" . implode(',', $timezoneIds) . ")")->get();
nlog("InvoiceTaxSummary:: Found " . $companies->count() . " companies in timezones: " . implode(',', $timezoneIds));
return $companies;
}
private function processCompanyTaxSummary($company)

View File

@@ -64,7 +64,12 @@ class TaskScheduler implements ShouldQueue
//@var \App\Models\Schedule $scheduler
$scheduler->service()->runTask();
} catch (\Throwable $e) {
nlog("Exception:: TaskScheduler:: Doing job :: {$scheduler->id} :: {$scheduler->name}" . $e->getMessage());
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
}
});

View File

@@ -46,6 +46,31 @@ class EntityFailedSendObject
{
$this->invitation = $invitation;
$this->entity_type = $entity_type;
// Load relationships if they're not already loaded (e.g., when withoutRelations() was called)
if (!$invitation->relationLoaded('contact')) {
$invitation->load('contact');
}
if (!$invitation->relationLoaded('company')) {
$invitation->load('company.account');
} else {
// If company is loaded, ensure account is also loaded
if ($invitation->company && !$invitation->company->relationLoaded('account')) {
$invitation->company->load('account');
}
}
if (!$invitation->relationLoaded($entity_type)) {
$invitation->load([$entity_type => function ($query) {
$query->with('client');
}]);
} else {
// If entity is loaded, ensure client is also loaded
$entity = $invitation->{$entity_type};
if ($entity && !$entity->relationLoaded('client')) {
$entity->load('client');
}
}
$this->entity = $invitation->{$entity_type};
$this->contact = $invitation->contact;
$this->company = $invitation->company;

View File

@@ -21,13 +21,23 @@ use App\Models\Project;
* Class for project repository.
*/
class ProjectRepository extends BaseRepository
{
{
/**
* Invoices a collection of projects into a single invoice.
*
* @param mixed $projects
* @return App\Models\Invoice
*/
public function invoice(mixed $projects)
{
$_project = $projects->first();
$invoice = InvoiceFactory::create($_project->company_id, $_project->user_id);
$invoice->client_id = $_project->client_id;
if(count($projects) == 1) {
$invoice->project_id = $_project->id;
}
// $invoice->project_id = $project->id;
$lines = [];

View File

@@ -178,7 +178,7 @@ class ClientService
$credits = Credit::withTrashed()->where('client_id', $this->client->id)
->where('is_deleted', false)
->where(function ($query) {
$query->whereDate('due_date', '<=', now()->format('Y-m-d'))
$query->where('due_date', '>=', now()->format('Y-m-d'))
->orWhereNull('due_date');
})
->orderBy('created_at', 'ASC');
@@ -192,7 +192,7 @@ class ClientService
->where('is_deleted', false)
->where('balance', '>', 0)
->where(function ($query) {
$query->whereDate('due_date', '<=', now()->format('Y-m-d'))
$query->where('due_date', '>=', now()->format('Y-m-d'))
->orWhereNull('due_date');
})
->orderBy('created_at', 'ASC')->get();

View File

@@ -1407,11 +1407,13 @@ class Peppol extends AbstractService
$party->PartyIdentification[] = $pi;
$pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme();
//// If this is intracommunity supply, ensure that the country prefix is on the party tax scheme
$pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme();
$companyID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CompanyID();
$companyID->value = preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->client->vat_number);
$companyID->value = $this->ensureVatNumberPrefix($this->invoice->client->vat_number, $this->invoice->client->country->iso_3166_2);
$pts->CompanyID = $companyID;
//// If this is intracommunity supply, ensure that the country prefix is on the party tax scheme
$ts = new TaxScheme();
$id = new ID();
@@ -1512,8 +1514,13 @@ class Peppol extends AbstractService
$location->Address = $address;
$delivery->DeliveryLocation = $location;
if (isset($this->invoice->e_invoice->Invoice->Delivery[0]->ActualDeliveryDate->date)) {
$delivery->ActualDeliveryDate = new \DateTime($this->invoice->e_invoice->Invoice->Delivery[0]->ActualDeliveryDate->date);
// Safely extract delivery date using data_get to handle missing properties
$delivery_date = data_get($this->invoice->e_invoice, 'Invoice.Delivery.0.ActualDeliveryDate.date')
?? data_get($this->invoice->e_invoice, 'Invoice.Delivery.0.ActualDeliveryDate')
?? null;
if ($delivery_date) {
$delivery->ActualDeliveryDate = new \DateTime($delivery_date);
}
return [$delivery];
@@ -1915,6 +1922,31 @@ class Peppol extends AbstractService
return '0037';
}
/**
* Ensures the VAT number has the correct country code prefix.
*
* @param string $vatNumber The raw VAT number.
* @param string $countryCode The 2-letter ISO country code.
* @return string The formatted VAT number with prefix.
*/
private function ensureVatNumberPrefix(string $vatNumber, string $countryCode): string
{
// Handle Greece special case
$prefix = ($countryCode === 'GR') ? 'EL' : $countryCode;
// Clean the VAT number by removing non-alphanumeric characters
$cleanedVat = preg_replace("/[^a-zA-Z0-9]/", "", $vatNumber);
// Check if the VAT number already starts with the country prefix
// If it does, return it as-is (preserving any check digits like "AA" in "FRAA123456789")
if (str_starts_with(strtoupper($cleanedVat), strtoupper($prefix))) {
return $cleanedVat;
}
// If the prefix is missing, clean and prepend it
return $prefix . $cleanedVat;
}
public function getErrors(): array
{
return $this->errors;

View File

@@ -58,6 +58,10 @@ class EmailReport
$start_end_dates = $this->calculateStartAndEndDates($this->scheduler->parameters, $this->scheduler->company);
$data = $this->scheduler->parameters;
if(!isset($data['user_id'])) {
$data['user_id'] = $this->scheduler->user_id;
}
$data['start_date'] = $start_end_dates[0];
$data['end_date'] = $start_end_dates[1];

View File

@@ -161,7 +161,7 @@ class TemplateService
$this->twig->addFilter($filter);
$allowedTags = ['if', 'for', 'set', 'filter'];
$allowedFilters = ['default', 'groupBy','capitalize', 'abs', 'date_modify', 'keys', 'join', 'reduce', 'format_date','json_decode','date_modify','trim','round','format_spellout_number','split', 'reduce','replace', 'escape', 'e', 'reverse', 'shuffle', 'slice', 'batch', 'title', 'sort', 'split', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br','striptags','markdown_to_html'];
$allowedFilters = ['url_encode','default', 'groupBy','capitalize', 'abs', 'date_modify', 'keys', 'join', 'reduce', 'format_date','json_decode','date_modify','trim','round','format_spellout_number','split', 'reduce','replace', 'escape', 'e', 'reverse', 'shuffle', 'slice', 'batch', 'title', 'sort', 'split', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br','striptags','markdown_to_html'];
$allowedFunctions = ['range', 'cycle', 'constant', 'date','img','t'];
$allowedProperties = ['type_id'];
// $allowedMethods = ['img','t'];

225
composer.lock generated
View File

@@ -497,16 +497,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.369.15",
"version": "3.369.19",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "7c62f41fb0460c3e5d5c1f70e93e726f1daa75f5"
"reference": "32fee3a25290186724ede9ca177d5090f7c5a837"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7c62f41fb0460c3e5d5c1f70e93e726f1daa75f5",
"reference": "7c62f41fb0460c3e5d5c1f70e93e726f1daa75f5",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/32fee3a25290186724ede9ca177d5090f7c5a837",
"reference": "32fee3a25290186724ede9ca177d5090f7c5a837",
"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.369.15"
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.19"
},
"time": "2026-01-16T19:18:57+00:00"
"time": "2026-01-23T19:05:51+00:00"
},
{
"name": "babenkoivan/elastic-adapter",
@@ -1179,16 +1179,16 @@
},
{
"name": "btcpayserver/btcpayserver-greenfield-php",
"version": "v2.8.1",
"version": "v2.9.0",
"source": {
"type": "git",
"url": "https://github.com/btcpayserver/btcpayserver-greenfield-php.git",
"reference": "3118f9e4e04590f53b2560866238af463153b2cf"
"reference": "60e6be57f9cd08dbe8f851d056358d1df9f07968"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/3118f9e4e04590f53b2560866238af463153b2cf",
"reference": "3118f9e4e04590f53b2560866238af463153b2cf",
"url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/60e6be57f9cd08dbe8f851d056358d1df9f07968",
"reference": "60e6be57f9cd08dbe8f851d056358d1df9f07968",
"shasum": ""
},
"require": {
@@ -1227,9 +1227,9 @@
"description": "BTCPay Server Greenfield API PHP client library.",
"support": {
"issues": "https://github.com/btcpayserver/btcpayserver-greenfield-php/issues",
"source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v2.8.1"
"source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v2.9.0"
},
"time": "2024-11-22T16:34:09+00:00"
"time": "2026-01-21T11:31:48+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
@@ -2124,16 +2124,16 @@
},
{
"name": "dompdf/php-font-lib",
"version": "1.0.1",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d"
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"shasum": ""
},
"require": {
@@ -2141,7 +2141,7 @@
"php": "^7.1 || ^8.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
},
"type": "library",
"autoload": {
@@ -2163,9 +2163,9 @@
"homepage": "https://github.com/dompdf/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.1"
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
},
"time": "2024-12-02T14:37:59+00:00"
"time": "2026-01-20T14:10:26+00:00"
},
{
"name": "dompdf/php-svg-lib",
@@ -3036,16 +3036,16 @@
},
{
"name": "google/apiclient-services",
"version": "v0.428.0",
"version": "v0.429.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "94a3c50a80a36cafb76e32fb76b8007e9f572deb"
"reference": "9dd334c8e6d3f25f91efcab86454c6bc0bc928c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/94a3c50a80a36cafb76e32fb76b8007e9f572deb",
"reference": "94a3c50a80a36cafb76e32fb76b8007e9f572deb",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/9dd334c8e6d3f25f91efcab86454c6bc0bc928c1",
"reference": "9dd334c8e6d3f25f91efcab86454c6bc0bc928c1",
"shasum": ""
},
"require": {
@@ -3074,9 +3074,9 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.428.0"
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.429.0"
},
"time": "2026-01-12T00:58:26+00:00"
"time": "2026-01-19T01:08:26+00:00"
},
{
"name": "google/auth",
@@ -5085,16 +5085,16 @@
},
{
"name": "laravel/framework",
"version": "v11.47.0",
"version": "v11.48.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "86693ffa1ba32f56f8c44e31416c6665095a62c5"
"reference": "5b23ab29087dbcb13077e5c049c431ec4b82f236"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/86693ffa1ba32f56f8c44e31416c6665095a62c5",
"reference": "86693ffa1ba32f56f8c44e31416c6665095a62c5",
"url": "https://api.github.com/repos/laravel/framework/zipball/5b23ab29087dbcb13077e5c049c431ec4b82f236",
"reference": "5b23ab29087dbcb13077e5c049c431ec4b82f236",
"shasum": ""
},
"require": {
@@ -5296,7 +5296,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-11-28T18:20:11+00:00"
"time": "2026-01-20T15:26:20+00:00"
},
{
"name": "laravel/octane",
@@ -6273,16 +6273,16 @@
},
{
"name": "league/flysystem",
"version": "3.30.2",
"version": "3.31.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277"
"reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277",
"reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1717e0b3642b0df65ecb0cc89cdd99fa840672ff",
"reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff",
"shasum": ""
},
"require": {
@@ -6350,22 +6350,22 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/3.30.2"
"source": "https://github.com/thephpleague/flysystem/tree/3.31.0"
},
"time": "2025-11-10T17:13:11+00:00"
"time": "2026-01-23T15:38:47+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
"version": "3.30.1",
"version": "3.31.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "d286e896083bed3190574b8b088b557b59eb66f5"
"reference": "e36a2bc60b06332c92e4435047797ded352b446f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d286e896083bed3190574b8b088b557b59eb66f5",
"reference": "d286e896083bed3190574b8b088b557b59eb66f5",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/e36a2bc60b06332c92e4435047797ded352b446f",
"reference": "e36a2bc60b06332c92e4435047797ded352b446f",
"shasum": ""
},
"require": {
@@ -6405,22 +6405,22 @@
"storage"
],
"support": {
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.30.1"
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.31.0"
},
"time": "2025-10-20T15:27:33+00:00"
"time": "2026-01-23T15:30:45+00:00"
},
{
"name": "league/flysystem-local",
"version": "3.30.2",
"version": "3.31.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git",
"reference": "ab4f9d0d672f601b102936aa728801dd1a11968d"
"reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d",
"reference": "ab4f9d0d672f601b102936aa728801dd1a11968d",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079",
"reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079",
"shasum": ""
},
"require": {
@@ -6454,9 +6454,9 @@
"local"
],
"support": {
"source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2"
"source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0"
},
"time": "2025-11-10T11:23:37+00:00"
"time": "2026-01-23T15:30:45+00:00"
},
{
"name": "league/fractal",
@@ -6844,16 +6844,16 @@
},
{
"name": "livewire/livewire",
"version": "v3.7.4",
"version": "v3.7.6",
"source": {
"type": "git",
"url": "https://github.com/livewire/livewire.git",
"reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0"
"reference": "276ac156f6ae414990784854a2673e3d23c68b24"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/livewire/livewire/zipball/5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0",
"reference": "5a8dffd4c0ab357ff7ed5b39e7c2453d962a68e0",
"url": "https://api.github.com/repos/livewire/livewire/zipball/276ac156f6ae414990784854a2673e3d23c68b24",
"reference": "276ac156f6ae414990784854a2673e3d23c68b24",
"shasum": ""
},
"require": {
@@ -6908,7 +6908,7 @@
"description": "A front-end framework for Laravel.",
"support": {
"issues": "https://github.com/livewire/livewire/issues",
"source": "https://github.com/livewire/livewire/tree/v3.7.4"
"source": "https://github.com/livewire/livewire/tree/v3.7.6"
},
"funding": [
{
@@ -6916,7 +6916,7 @@
"type": "github"
}
],
"time": "2026-01-13T09:37:21+00:00"
"time": "2026-01-23T05:41:38+00:00"
},
{
"name": "maennchen/zipstream-php",
@@ -16978,16 +16978,16 @@
},
{
"name": "twig/extra-bundle",
"version": "v3.22.2",
"version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/twig-extra-bundle.git",
"reference": "09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e"
"reference": "7a27e784dc56eddfef5e9295829b290ce06f1682"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e",
"reference": "09de9be7f6c0d19ede7b5a1dbfcfb2e9d1e0ea9e",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/7a27e784dc56eddfef5e9295829b290ce06f1682",
"reference": "7a27e784dc56eddfef5e9295829b290ce06f1682",
"shasum": ""
},
"require": {
@@ -17036,7 +17036,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.22.2"
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.23.0"
},
"funding": [
{
@@ -17048,20 +17048,20 @@
"type": "tidelift"
}
],
"time": "2025-12-05T08:51:53+00:00"
"time": "2025-12-18T20:46:15+00:00"
},
{
"name": "twig/intl-extra",
"version": "v3.22.1",
"version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/intl-extra.git",
"reference": "93ac31e53cdd3f2e541f42690cd0c54ca8138ab1"
"reference": "32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/93ac31e53cdd3f2e541f42690cd0c54ca8138ab1",
"reference": "93ac31e53cdd3f2e541f42690cd0c54ca8138ab1",
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f",
"reference": "32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f",
"shasum": ""
},
"require": {
@@ -17100,7 +17100,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/intl-extra/tree/v3.22.1"
"source": "https://github.com/twigphp/intl-extra/tree/v3.23.0"
},
"funding": [
{
@@ -17112,20 +17112,20 @@
"type": "tidelift"
}
],
"time": "2025-11-02T11:00:49+00:00"
"time": "2026-01-17T13:57:47+00:00"
},
{
"name": "twig/markdown-extra",
"version": "v3.22.0",
"version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/markdown-extra.git",
"reference": "fb6f952082e3a7d62a75c8be2c8c47242d3925fb"
"reference": "faf069b259e2d3930c73c2f53e2dec8440bd90a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/fb6f952082e3a7d62a75c8be2c8c47242d3925fb",
"reference": "fb6f952082e3a7d62a75c8be2c8c47242d3925fb",
"url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/faf069b259e2d3930c73c2f53e2dec8440bd90a2",
"reference": "faf069b259e2d3930c73c2f53e2dec8440bd90a2",
"shasum": ""
},
"require": {
@@ -17172,7 +17172,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/markdown-extra/tree/v3.22.0"
"source": "https://github.com/twigphp/markdown-extra/tree/v3.23.0"
},
"funding": [
{
@@ -17184,20 +17184,20 @@
"type": "tidelift"
}
],
"time": "2025-09-15T05:57:37+00:00"
"time": "2025-12-02T14:45:16+00:00"
},
{
"name": "twig/twig",
"version": "v3.22.2",
"version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2"
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/946ddeafa3c9f4ce279d1f34051af041db0e16f2",
"reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
"shasum": ""
},
"require": {
@@ -17251,7 +17251,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.22.2"
"source": "https://github.com/twigphp/Twig/tree/v3.23.0"
},
"funding": [
{
@@ -17263,7 +17263,7 @@
"type": "tidelift"
}
],
"time": "2025-12-14T11:28:47+00:00"
"time": "2026-01-23T21:00:41+00:00"
},
{
"name": "twilio/sdk",
@@ -17632,16 +17632,16 @@
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.16.3",
"version": "v3.16.5",
"source": {
"type": "git",
"url": "https://github.com/fruitcake/laravel-debugbar.git",
"reference": "c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e"
"reference": "e85c0a8464da67e5b4a53a42796d46a43fc06c9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e",
"reference": "c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e",
"url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/e85c0a8464da67e5b4a53a42796d46a43fc06c9a",
"reference": "e85c0a8464da67e5b4a53a42796d46a43fc06c9a",
"shasum": ""
},
"require": {
@@ -17701,7 +17701,7 @@
],
"support": {
"issues": "https://github.com/fruitcake/laravel-debugbar/issues",
"source": "https://github.com/fruitcake/laravel-debugbar/tree/v3.16.3"
"source": "https://github.com/fruitcake/laravel-debugbar/tree/v3.16.5"
},
"funding": [
{
@@ -17713,7 +17713,7 @@
"type": "github"
}
],
"time": "2025-12-23T17:37:00+00:00"
"time": "2026-01-23T15:03:22+00:00"
},
{
"name": "barryvdh/laravel-ide-helper",
@@ -18411,16 +18411,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.92.5",
"version": "v3.93.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58"
"reference": "50895a07cface1385082e4caa6a6786c4e033468"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58",
"reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/50895a07cface1385082e4caa6a6786c4e033468",
"reference": "50895a07cface1385082e4caa6a6786c4e033468",
"shasum": ""
},
"require": {
@@ -18452,14 +18452,14 @@
},
"require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.7",
"infection/infection": "^0.31",
"infection/infection": "^0.32",
"justinrainbow/json-schema": "^6.6",
"keradus/cli-executor": "^2.3",
"mikey179/vfsstream": "^1.6.12",
"php-coveralls/php-coveralls": "^2.9",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.46",
"phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.48",
"symfony/polyfill-php85": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0",
"symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0"
@@ -18503,7 +18503,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.5"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.0"
},
"funding": [
{
@@ -18511,7 +18511,7 @@
"type": "github"
}
],
"time": "2026-01-08T21:57:37+00:00"
"time": "2026-01-23T17:33:21+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@@ -18607,7 +18607,7 @@
},
{
"name": "illuminate/json-schema",
"version": "v12.47.0",
"version": "v12.48.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/json-schema.git",
@@ -21675,38 +21675,39 @@
},
{
"name": "spatie/laravel-ignition",
"version": "2.9.1",
"version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ignition.git",
"reference": "1baee07216d6748ebd3a65ba97381b051838707a"
"reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/1baee07216d6748ebd3a65ba97381b051838707a",
"reference": "1baee07216d6748ebd3a65ba97381b051838707a",
"url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/2abefdcca6074a9155f90b4ccb3345af8889d5f5",
"reference": "2abefdcca6074a9155f90b4ccb3345af8889d5f5",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.1",
"spatie/ignition": "^1.15",
"symfony/console": "^6.2.3|^7.0",
"symfony/var-dumper": "^6.2.3|^7.0"
"illuminate/support": "^11.0|^12.0",
"nesbot/carbon": "^2.72|^3.0",
"php": "^8.2",
"spatie/ignition": "^1.15.1",
"symfony/console": "^7.4|^8.0",
"symfony/var-dumper": "^7.4|^8.0"
},
"require-dev": {
"livewire/livewire": "^2.11|^3.3.5",
"mockery/mockery": "^1.5.1",
"openai-php/client": "^0.8.1|^0.10",
"orchestra/testbench": "8.22.3|^9.0|^10.0",
"pestphp/pest": "^2.34|^3.7",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan-deprecation-rules": "^1.1.1|^2.0",
"phpstan/phpstan-phpunit": "^1.3.16|^2.0",
"vlucas/phpdotenv": "^5.5"
"livewire/livewire": "^3.7.0|^4.0",
"mockery/mockery": "^1.6.12",
"openai-php/client": "^0.10.3",
"orchestra/testbench": "^v9.16.0|^10.6",
"pestphp/pest": "^3.7|^4.0",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.8",
"vlucas/phpdotenv": "^5.6.2"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
@@ -21762,7 +21763,7 @@
"type": "github"
}
],
"time": "2025-02-20T13:13:55+00:00"
"time": "2026-01-20T13:16:11+00:00"
},
{
"name": "spaze/phpstan-stripe",

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.48'),
'app_tag' => env('APP_TAG', '5.12.48'),
'app_version' => env('APP_VERSION', '5.12.49'),
'app_tag' => env('APP_TAG', '5.12.49'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

@@ -5689,6 +5689,7 @@ Développe automatiquement la section des notes dans le tableau de produits pour
'peppol_sending_failed' => 'Problème technique de livraison. Réessai impossible.',
'peppol_sending_success' => 'La facture électronique a été envoyée!',
'auto_generate' => 'Auto générer',
'mollie_payment_pending' => 'Votre paiement est en attente. Veuillez patienter pendant son traitement. Nous vous enverrons un e-mail une fois qu\'il sera terminé.',
);
return $lang;

View File

@@ -5690,6 +5690,7 @@ $lang = array(
'peppol_sending_failed' => 'Sự cố giao hàng kỹ thuật. Không thể thử lại',
'peppol_sending_success' => 'E- Hóa đơn gửi Thành công !',
'auto_generate' => 'Tự động tạo',
'mollie_payment_pending' => 'Sự chi trả của bạn đang chờ xử lý. Vui lòng chờ đến xử lý. Chúng tôi sẽ email cho bạn khi hoàn tất.',
);
return $lang;

File diff suppressed because it is too large Load Diff

View File

@@ -348,6 +348,7 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
Route::put('recurring_quotes/{recurring_quote}/upload', [RecurringQuoteController::class, 'upload']);
Route::post('refresh', [LoginController::class, 'refresh'])->middleware('throttle:refresh');
Route::post('refresh_react', [LoginController::class, 'refreshReact'])->middleware('throttle:refresh');
Route::post('reports/clients', ClientReportController::class)->middleware('throttle:20,1');
Route::post('reports/activities', ActivityReportController::class)->middleware('throttle:20,1');

View File

@@ -36,6 +36,7 @@ use App\Services\EDocument\Gateway\Storecove\Storecove;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronica;
use App\Services\EDocument\Standards\Validation\Peppol\InvoiceLevel;
use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel;
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
use InvoiceNinja\EInvoice\Models\Peppol\BranchType\FinancialInstitutionBranch;
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
@@ -79,6 +80,7 @@ class PeppolTest extends TestCase
$settings->country_id = Country::where('iso_3166_2', 'DE')->first()->id;
$settings->email = $this->faker->safeEmail();
$settings->currency_id = '3';
$settings->e_invoice_type = 'PEPPOL'; // Required for validation endpoint to run EntityLevel validation
$tax_data = new TaxModel();
$tax_data->regions->EU->has_sales_above_threshold = $params['over_threshold'] ?? false;
@@ -581,20 +583,71 @@ class PeppolTest extends TestCase
$client->city = '';
$client->save();
// Reload the client to ensure changes are persisted
$client = $client->refresh();
// Direct EntityLevel test to debug validation
$entityLevel = new EntityLevel();
$directResult = $entityLevel->checkClient($client);
// Assert direct validation fails
$this->assertFalse($directResult['passes'], 'Direct EntityLevel validation should fail when address1 and city are empty');
$this->assertNotEmpty($directResult['client'], 'Direct EntityLevel should have client validation errors');
$data = [
'entity' => 'clients',
'entity_id' => $client->hashed_id
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/einvoice/validateEntity', $data);
// Log the response for debugging
$response->assertStatus(422);
}
public function testEntityLevelDirectlyValidatesClientWithMissingAddress()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'FR',
'client_vat' => 'FRAA123456789',
'client_id_number' => '123456789',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'is_tax_exempt' => false,
];
$entity_data = $this->setupTestData($scenario);
$client = $entity_data['client'];
// Clear required address fields
$client->address1 = '';
$client->city = '';
$client->save();
// Directly instantiate and test EntityLevel
$entityLevel = new EntityLevel();
$result = $entityLevel->checkClient($client);
// Assert validation fails
$this->assertFalse($result['passes'], 'Validation should fail when address1 and city are empty');
$this->assertNotEmpty($result['client'], 'Should have client validation errors');
// Check that address errors are present
$errorFields = array_column($result['client'], 'field');
$this->assertContains('address1', $errorFields, 'Should have address1 error');
$this->assertContains('city', $errorFields, 'Should have city error');
}
public function testEntityValidationFailsForClientViaInvoice()
{
$scenario = [

View File

@@ -0,0 +1,378 @@
<?php
namespace Tests\Feature\EInvoice;
use Tests\TestCase;
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
class PeppolXmlValidationTest extends TestCase
{
private string $xml = '<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>INV-20xx-0001</cbc:ID>
<cbc:IssueDate>2026-01-23</cbc:IssueDate>
<cbc:DueDate>2026-02-23</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:Note>Autoliquidation Following art.</cbc:Note>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cbc:BuyerReference>REF-12345/001/0001</cbc:BuyerReference>
<cac:OrderReference>
<cbc:ID>REF-12345/001/0001</cbc:ID>
</cac:OrderReference>
<cac:AdditionalDocumentReference>
<cbc:ID>Invoice_INV-20xx-0001.pdf</cbc:ID>
</cac:AdditionalDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0037">BE0123456789</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID schemeID="0037">BE0123456789</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Example Company S.A.</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Example Street 123</cbc:StreetName>
<cbc:CityName>Brussels</cbc:CityName>
<cbc:PostalZone>1000</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>BE0123456789</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Example Company S.A.</cbc:RegistrationName>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>John Doe</cbc:Name>
<cbc:Telephone>+31 2 123 45 67</cbc:Telephone>
<cbc:ElectronicMail>contact@example.com</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0037">987654321</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID schemeID="0037">987654321</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Customer Company GmbH</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Customer Street 456</cbc:StreetName>
<cbc:CityName>Berlin</cbc:CityName>
<cbc:PostalZone>10115</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>DE987654321</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Customer Company GmbH</cbc:RegistrationName>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:ElectronicMail>contact@customer.com</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:Delivery>
<cbc:ActualDeliveryDate>2026-01-21</cbc:ActualDeliveryDate>
<cac:DeliveryLocation>
<cac:Address>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:Address>
</cac:DeliveryLocation>
</cac:Delivery>
<cac:PaymentMeans>
<cbc:PaymentMeansCode>1</cbc:PaymentMeansCode>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30 Days</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">10000</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cbc:TaxExemptionReasonCode>vatex-eu-ic</cbc:TaxExemptionReasonCode>
<cbc:TaxExemptionReason>Intra-Community supply</cbc:TaxExemptionReason>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">10000</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">10000</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">10000</cbc:TaxInclusiveAmount>
<cbc:AllowanceTotalAmount currencyID="EUR">0</cbc:AllowanceTotalAmount>
<cbc:ChargeTotalAmount currencyID="EUR">0</cbc:ChargeTotalAmount>
<cbc:PayableAmount currencyID="EUR">10000</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">10</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">1000</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Service Support Package A</cbc:Description>
<cbc:Name>SVC-001</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>2</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">5</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">500</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Service Support Package B</cbc:Description>
<cbc:Name>SVC-002</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>3</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">20</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">2000</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Service Support Package C</cbc:Description>
<cbc:Name>SVC-003</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>4</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">8</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">800</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Service Support D</cbc:Description>
<cbc:Name>SVC-004</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>5</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">8</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">800</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Service Support Package E</cbc:Description>
<cbc:Name>SVC-005</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>6</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">2</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">1000</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Software License A</cbc:Description>
<cbc:Name>SW-001</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">500</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>7</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">2</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">1000</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Software License B</cbc:Description>
<cbc:Name>SW-002</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">500</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>8</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">5</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">1000</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Software License C</cbc:Description>
<cbc:Name>SW-003</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">200</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>9</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">10</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">500</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Additional Service Package</cbc:Description>
<cbc:Name>SVC-006</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">50</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>10</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">2</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">500</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Additional Feature Package</cbc:Description>
<cbc:Name>SVC-007</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">250</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>11</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">3</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">900</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Professional Services - Remote</cbc:Description>
<cbc:Name>PS-001</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>K</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">300</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
';
public function setUp(): void
{
parent::setUp();
try {
$processor = new \Saxon\SaxonProcessor();
} catch (\Throwable $e) {
$this->markTestSkipped('saxon not installed');
}
}
public function testPeppolXmlValidation()
{
try {
$processor = new \Saxon\SaxonProcessor();
} catch (\Throwable $e) {
$this->markTestSkipped('saxon not installed');
}
$validator = new XsltDocumentValidator($this->xml);
$validator->validate();
if (count($validator->getErrors()) > 0) {
// nlog($this->xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
}

View File

@@ -43,6 +43,7 @@ class CreditBalanceTest extends TestCase
'company_id' => $this->company->id,
'client_id' => $this->client->id,
'balance' => 10,
'due_date' => null,
'number' => 'testing-number-01',
'status_id' => Credit::STATUS_SENT,
]);
@@ -50,6 +51,22 @@ class CreditBalanceTest extends TestCase
$this->assertEquals($this->client->service()->getCreditBalance(), 10);
}
public function testCreditBalance2()
{
$credit = Credit::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->client->id,
'balance' => 10,
'due_date' => now()->addDays(10),
'number' => 'testing-number-01',
'status_id' => Credit::STATUS_SENT,
]);
$this->assertEquals($this->client->service()->getCreditBalance(), 10);
}
public function testExpiredCreditBalance()
{
$credit = Credit::factory()->create([
@@ -57,7 +74,7 @@ class CreditBalanceTest extends TestCase
'company_id' => $this->company->id,
'client_id' => $this->client->id,
'balance' => 10,
'due_date' => now()->addDays(5),
'due_date' => now()->subDays(5),
'number' => 'testing-number-02',
'status_id' => Credit::STATUS_SENT,
]);

View File

@@ -0,0 +1,326 @@
<?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 Tests\Unit\Storecove;
use Tests\TestCase;
use ReflectionClass;
use ReflectionMethod;
use Modules\Admin\Jobs\Storecove\DocumentSubmission;
class DocumentSubmissionExtractUblTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
if(!class_exists(DocumentSubmission::class)) {
$this->markTestSkipped('DocumentSubmission class does not exist');
}
}
/**
* Test extracting CreditNote from StandardBusinessDocument wrapper
*/
public function testExtractCreditNoteFromSbdWrapper(): void
{
$xml = '<?xml version="1.0" encoding="UTF-8"?><sh:StandardBusinessDocument xmlns:sh="http://www.unece.org/cefact/namespaces/StandardBusinessDocumentHeader"><sh:StandardBusinessDocumentHeader><sh:HeaderVersion>1.0</sh:HeaderVersion><sh:Sender><sh:Identifier Authority="iso6523-actorid-upis">0208:1234567890</sh:Identifier></sh:Sender><sh:Receiver><sh:Identifier Authority="iso6523-actorid-upis">0208:0987654321</sh:Identifier></sh:Receiver><sh:DocumentIdentification><sh:Standard>urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2</sh:Standard><sh:TypeVersion>2.1</sh:TypeVersion><sh:InstanceIdentifier>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</sh:InstanceIdentifier><sh:Type>CreditNote</sh:Type><sh:CreationDateAndTime>2026-01-22T15:53:41.44Z</sh:CreationDateAndTime></sh:DocumentIdentification><sh:BusinessScope><sh:Scope><sh:Type>DOCUMENTID</sh:Type><sh:InstanceIdentifier>urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1</sh:InstanceIdentifier><sh:Identifier>busdox-docid-qns</sh:Identifier></sh:Scope><sh:Scope><sh:Type>PROCESSID</sh:Type><sh:InstanceIdentifier>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</sh:InstanceIdentifier><sh:Identifier>cenbii-procid-ubl</sh:Identifier></sh:Scope><sh:Scope><sh:Type>COUNTRY_C1</sh:Type><sh:InstanceIdentifier>BE</sh:InstanceIdentifier></sh:Scope></sh:BusinessScope></sh:StandardBusinessDocumentHeader><CreditNote xmlns:cec="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>TEST/2026/0001</cbc:ID>
<cbc:IssueDate>2026-01-22</cbc:IssueDate>
<cbc:CreditNoteTypeCode listAgencyID="6" listID="UNCL1001">381</cbc:CreditNoteTypeCode>
<cbc:DocumentCurrencyCode listAgencyID="6" listID="ISO4217">EUR</cbc:DocumentCurrencyCode>
<cac:OrderReference>
<cbc:ID>TEST/2026/0001</cbc:ID>
</cac:OrderReference>
<cac:AdditionalDocumentReference>
<cbc:ID>test20260001</cbc:ID>
</cac:AdditionalDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">1234567890</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID schemeAgencyID="ZZZ" schemeID="0208">1234567890</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>Test Supplier Company</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>123 Test Street</cbc:StreetName>
<cbc:CityName>Test City</cbc:CityName>
<cbc:PostalZone>1000</cbc:PostalZone>
<cbc:CountrySubentity>Test Region</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode listAgencyID="6" listID="ISO3166-1:Alpha2">BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID schemeAgencyID="ZZZ" schemeID="9925">BE1234567890</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5153">VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Test Supplier Company</cbc:RegistrationName>
<cbc:CompanyID schemeAgencyID="ZZZ" schemeID="0208">1234567890</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>Test Contact</cbc:Name>
<cbc:ElectronicMail>supplier@example.com</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">0987654321</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Test Customer Company</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>456 Customer Street</cbc:StreetName>
<cbc:CityName>Customer City</cbc:CityName>
<cbc:PostalZone>2000</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode listAgencyID="6" listID="ISO3166-1:Alpha2">BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID schemeAgencyID="ZZZ" schemeID="9925">BE0987654321</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5153">VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Test Customer Company</cbc:RegistrationName>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Telephone>123456789</cbc:Telephone>
<cbc:ElectronicMail>customer@example.com</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:PaymentMeans>
<cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
<cbc:PaymentID>TEST/2026/0001</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID>BE12345678901234</cbc:ID>
<cbc:Name>TEST SUPPLIER COMPANY</cbc:Name>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">0.00</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">49.50</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">0.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID schemeAgencyID="6" schemeID="UNCL5305">E</cbc:ID>
<cbc:Percent>0.0</cbc:Percent>
<cbc:TaxExemptionReason>Exempt</cbc:TaxExemptionReason>
<cac:TaxScheme>
<cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5153">VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">49.50</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">49.50</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">49.50</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="EUR">49.50</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:CreditNoteLine>
<cbc:ID>1</cbc:ID>
<cbc:CreditedQuantity unitCode="C62" unitCodeListID="UNECERec20">1.000000</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">22.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Credit note on TEST/2025/0001</cbc:Description>
<cbc:Name>DOMAIN .COM/.NET/.ORG</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID schemeAgencyID="6" schemeID="UNCL5305">E</cbc:ID>
<cbc:Percent>0.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5153">VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">22.00</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62" unitCodeListID="UNECERec20">1.0</cbc:BaseQuantity>
</cac:Price>
</cac:CreditNoteLine>
<cac:CreditNoteLine>
<cbc:ID>2</cbc:ID>
<cbc:CreditedQuantity unitCode="C62" unitCodeListID="UNECERec20">0.250000</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">27.50</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Credit note on TEST/2025/0001</cbc:Description>
<cbc:Name>PRESTATION DE SERVICES</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID schemeAgencyID="6" schemeID="UNCL5305">E</cbc:ID>
<cbc:Percent>0.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5153">VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">110.00</cbc:PriceAmount>
<cbc:BaseQuantity unitCode="C62" unitCodeListID="UNECERec20">1.0</cbc:BaseQuantity>
</cac:Price>
</cac:CreditNoteLine>
</CreditNote></sh:StandardBusinessDocument>';
$job = new DocumentSubmission([]);
$reflection = new ReflectionClass($job);
$method = $reflection->getMethod('extractInvoiceUbl');
$method->setAccessible(true);
$result = $method->invoke($job, $xml);
// Assert that the result is valid XML
$this->assertNotEmpty($result);
// Assert that the result contains CreditNote
$this->assertStringContainsString('<CreditNote', $result);
$this->assertStringContainsString('TEST/2026/0001', $result);
// Assert that the result does NOT contain the SBD wrapper
$this->assertStringNotContainsString('StandardBusinessDocument', $result);
$this->assertStringNotContainsString('StandardBusinessDocumentHeader', $result);
// Assert that the result is valid XML that can be parsed
$dom = new \DOMDocument();
$this->assertTrue($dom->loadXML($result), 'Extracted XML should be valid');
// Assert that the root element is CreditNote
$this->assertEquals('CreditNote', $dom->documentElement->localName);
$this->assertEquals('urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2', $dom->documentElement->namespaceURI);
}
/**
* Test extracting Invoice from StandardBusinessDocument wrapper
*/
public function testExtractInvoiceFromSbdWrapper(): void
{
$xml = '<?xml version="1.0" encoding="UTF-8"?><sh:StandardBusinessDocument xmlns:sh="http://www.unece.org/cefact/namespaces/StandardBusinessDocumentHeader"><sh:StandardBusinessDocumentHeader><sh:HeaderVersion>1.0</sh:HeaderVersion><sh:Sender><sh:Identifier Authority="iso6523-actorid-upis">0208:0769867026</sh:Identifier></sh:Sender><sh:Receiver><sh:Identifier Authority="iso6523-actorid-upis">0208:0821894064</sh:Identifier></sh:Receiver><sh:DocumentIdentification><sh:Standard>urn:oasis:names:specification:ubl:schema:xsd:Invoice-2</sh:Standard><sh:TypeVersion>2.1</sh:TypeVersion><sh:InstanceIdentifier>507dcfe6-7f6e-473a-bd20-f1c8dce2e2c8</sh:InstanceIdentifier><sh:Type>Invoice</sh:Type><sh:CreationDateAndTime>2026-01-22T15:53:41.44Z</sh:CreationDateAndTime></sh:DocumentIdentification></sh:StandardBusinessDocumentHeader><Invoice xmlns:cec="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>INV/2026/0001</cbc:ID>
<cbc:IssueDate>2026-01-22</cbc:IssueDate>
<cbc:DocumentCurrencyCode listAgencyID="6" listID="ISO4217">EUR</cbc:DocumentCurrencyCode>
</Invoice></sh:StandardBusinessDocument>';
$job = new DocumentSubmission([]);
$reflection = new ReflectionClass($job);
$method = $reflection->getMethod('extractInvoiceUbl');
$method->setAccessible(true);
$result = $method->invoke($job, $xml);
// Assert that the result is valid XML
$this->assertNotEmpty($result);
// Assert that the result contains Invoice
$this->assertStringContainsString('<Invoice', $result);
$this->assertStringContainsString('INV/2026/0001', $result);
// Assert that the result does NOT contain the SBD wrapper
$this->assertStringNotContainsString('StandardBusinessDocument', $result);
// Assert that the result is valid XML that can be parsed
$dom = new \DOMDocument();
$this->assertTrue($dom->loadXML($result), 'Extracted XML should be valid');
// Assert that the root element is Invoice
$this->assertEquals('Invoice', $dom->documentElement->localName);
$this->assertEquals('urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', $dom->documentElement->namespaceURI);
}
/**
* Test that exception is thrown when neither Invoice nor CreditNote is found
*/
public function testThrowsExceptionWhenNoInvoiceOrCreditNoteFound(): void
{
$xml = '<?xml version="1.0" encoding="UTF-8"?><sh:StandardBusinessDocument xmlns:sh="http://www.unece.org/cefact/namespaces/StandardBusinessDocumentHeader"><sh:StandardBusinessDocumentHeader><sh:HeaderVersion>1.0</sh:HeaderVersion></sh:StandardBusinessDocumentHeader><OtherDocument xmlns="urn:example:other:document"><SomeElement>Test</SomeElement></OtherDocument></sh:StandardBusinessDocument>';
$job = new DocumentSubmission([]);
$reflection = new ReflectionClass($job);
$method = $reflection->getMethod('extractInvoiceUbl');
$method->setAccessible(true);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('No Invoice or CreditNote tag found in XML');
$method->invoke($job, $xml);
}
/**
* Test that method handles XML without SBD wrapper (direct Invoice)
*/
public function testExtractDirectInvoiceWithoutWrapper(): void
{
$xml = '<?xml version="1.0" encoding="UTF-8"?><Invoice xmlns:cec="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:ID>DIRECT/2026/0001</cbc:ID>
<cbc:IssueDate>2026-01-22</cbc:IssueDate>
</Invoice>';
$job = new DocumentSubmission([]);
$reflection = new ReflectionClass($job);
$method = $reflection->getMethod('extractInvoiceUbl');
$method->setAccessible(true);
$result = $method->invoke($job, $xml);
// Assert that the result is valid XML
$this->assertNotEmpty($result);
$this->assertStringContainsString('<Invoice', $result);
$this->assertStringContainsString('DIRECT/2026/0001', $result);
// Assert that the result is valid XML that can be parsed
$dom = new \DOMDocument();
$this->assertTrue($dom->loadXML($result), 'Extracted XML should be valid');
$this->assertEquals('Invoice', $dom->documentElement->localName);
}
/**
* Test that method handles XML without SBD wrapper (direct CreditNote)
*/
public function testExtractDirectCreditNoteWithoutWrapper(): void
{
$xml = '<?xml version="1.0" encoding="UTF-8"?><CreditNote xmlns:cec="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:ID>CN/2026/0001</cbc:ID>
<cbc:IssueDate>2026-01-22</cbc:IssueDate>
</CreditNote>';
$job = new DocumentSubmission([]);
$reflection = new ReflectionClass($job);
$method = $reflection->getMethod('extractInvoiceUbl');
$method->setAccessible(true);
$result = $method->invoke($job, $xml);
// Assert that the result is valid XML
$this->assertNotEmpty($result);
$this->assertStringContainsString('<CreditNote', $result);
$this->assertStringContainsString('CN/2026/0001', $result);
// Assert that the result is valid XML that can be parsed
$dom = new \DOMDocument();
$this->assertTrue($dom->loadXML($result), 'Extracted XML should be valid');
$this->assertEquals('CreditNote', $dom->documentElement->localName);
}
}