Fixes for tests

This commit is contained in:
David Bomba
2026-03-30 08:07:58 +11:00
parent 04ce5ad9b8
commit 947b1d2e78
11 changed files with 3508 additions and 505 deletions

View File

@@ -75,6 +75,28 @@ class ChartController extends BaseController
return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200);
}
public function analytics_summary(ShowChartRequest $request)
{
/** @var \App\Models\User auth()->user() */
$user = auth()->user();
$admin_equivalent_permissions = $user->isAdmin() || $user->hasExactPermissionAndAll('view_all') || $user->hasExactPermissionAndAll('edit_all');
$cs = new ChartService($user->company(), $user, $admin_equivalent_permissions);
return response()->json($cs->analytics_summary($request->input('start_date'), $request->input('end_date')), 200);
}
public function analytics_totals(ShowChartRequest $request)
{
/** @var \App\Models\User auth()->user() */
$user = auth()->user();
$admin_equivalent_permissions = $user->isAdmin() || $user->hasExactPermissionAndAll('view_all') || $user->hasExactPermissionAndAll('edit_all');
$cs = new ChartService($user->company(), $user, $admin_equivalent_permissions);
return response()->json($cs->analytics_totals($request->input('start_date'), $request->input('end_date')), 200);
}
public function calculatedFields(ShowCalculatedFieldRequest $request)
{

View File

@@ -14,6 +14,7 @@ namespace App\Http\Controllers;
use App\Http\Requests\License\CheckRequest;
use App\Models\Account;
use App\Services\License\WhiteLabelRenewalService;
use App\Utils\CurlUtils;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
@@ -226,10 +227,14 @@ class LicenseController extends BaseController
{
$account = auth()->user()->account;
if ($account->plan == 'white_label' && Carbon::parse($account->plan_expires)->lt(now())) {
$account->plan = null;
$account->plan_expires = null;
$account->save();
if ($account->plan == 'white_label' && $account->plan_expires && Carbon::parse($account->plan_expires)->lt(now())) {
$result = (new WhiteLabelRenewalService())->checkAndRenew($account);
if ($result === false) {
$account->plan = null;
$account->plan_expires = null;
$account->save();
}
}
}

View File

@@ -73,11 +73,6 @@ class SquareController extends BaseController
'code' => $request->query('code'),
]);
nlog($base_url);
nlog($config('services.square.application_id'));
nlog($config('services.square.application_secret'));
nlog($request->query('code'));
nlog($response->body());
if ($response->failed()) {
return view('auth.square_connect.access_denied');
}

View File

@@ -18,6 +18,7 @@ use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Vendor;
use App\Services\License\WhiteLabelRenewalService;
use App\Utils\Ninja;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
@@ -59,10 +60,14 @@ class VersionCheck implements ShouldQueue
return;
}
if ($account->plan == 'white_label' && $account->plan_expires && Carbon::parse($account->plan_expires)->lt(now())) {
$account->plan = null;
$account->plan_expires = null;
$account->saveQuietly();
if ($account->plan == 'white_label' && $account->plan_expires && (Carbon::parse($account->plan_expires)->lt(now()) || Carbon::parse($account->plan_expires)->gt(now()->addYear()->addDays(30)))) {
$result = (new WhiteLabelRenewalService())->checkAndRenew($account);
if ($result === false) {
$account->plan = null;
$account->plan_expires = null;
$account->saveQuietly();
}
}
Client::query()->whereNull('country_id')->cursor()->each(function ($client) {

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@ class ChartService
{
use ChartQueries;
use ChartCalculations;
use AnalyticsQueries;
public function __construct(public Company $company, private User $user, private bool $is_admin, private bool $include_drafts = false) {}
@@ -211,6 +212,72 @@ class ChartService
return '';
}
/* Analytics */
/**
* Analytics chart summary — time-series data for analytics charts.
* Returns per-currency + aggregate (key 999) data matching chart_summary() format.
*/
public function analytics_summary($start_date, $end_date): array
{
$currencies = $this->getCurrencyCodes();
$data = [];
$data['start_date'] = $start_date;
$data['end_date'] = $end_date;
foreach ($currencies as $key => $value) {
$data[$key]['mrr'] = $this->getMrrChartQuery($start_date, $end_date, $key);
$data[$key]['payment_delay'] = $this->getPaymentDelayChartQuery($start_date, $end_date, $key);
$data[$key]['quote_pipeline'] = $this->getQuotePipelineChartQuery($start_date, $end_date, $key);
$data[$key]['late_payment_rate'] = $this->getLatePaymentRateChartQuery($start_date, $end_date, $key);
}
$data[999]['mrr'] = $this->getAggregateMrrChartQuery($start_date, $end_date);
$data[999]['payment_delay'] = $this->getAggregatePaymentDelayChartQuery($start_date, $end_date);
$data[999]['quote_pipeline'] = $this->getAggregateQuotePipelineChartQuery($start_date, $end_date);
$data[999]['late_payment_rate'] = $this->getAggregateLatePaymentRateChartQuery($start_date, $end_date);
return $data;
}
/**
* Analytics totals — snapshot KPIs for analytics dashboard cards.
* Returns per-currency + aggregate (key 999) data matching totals() format.
*/
public function analytics_totals($start_date, $end_date): array
{
$data = [];
$data['currencies'] = $this->getCurrencyCodes();
$data['start_date'] = $start_date;
$data['end_date'] = $end_date;
$mrr_totals = $this->getMrrTotalQuery();
$aging_totals = $this->getAgingBucketTotals();
$client_analytics = $this->getClientPaymentSummary();
foreach ($data['currencies'] as $key => $value) {
$mrr_set = array_search($key, array_column($mrr_totals, 'currency_id'));
$aging_set = array_search($key, array_column($aging_totals, 'currency_id'));
$data[$key]['mrr'] = $mrr_set !== false ? $mrr_totals[$mrr_set] : new \stdClass();
$data[$key]['aging'] = $aging_set !== false ? $aging_totals[$aging_set] : new \stdClass();
}
$aggregate_mrr = $this->getAggregateMrrTotalQuery();
$aggregate_aging = $this->getAggregateAgingBucketTotals();
$company_payment = $this->getCompanyPaymentSummary();
$data[999]['mrr'] = ! empty($aggregate_mrr) ? reset($aggregate_mrr) : new \stdClass();
$data[999]['aging'] = ! empty($aggregate_aging) ? reset($aggregate_aging) : new \stdClass();
$data[999]['payment_analytics'] = ! empty($company_payment) ? reset($company_payment) : new \stdClass();
return $data;
}
/* Analytics */
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**

View File

@@ -0,0 +1,72 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2026. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\License;
use App\Models\Account;
use Carbon\Carbon;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
class WhiteLabelRenewalService
{
/**
* Check if an expired white label license has been renewed on the license server.
*
* @return bool|null true = renewed, false = not renewed, null = inconclusive (network error)
*/
public function checkAndRenew(Account $account): ?bool
{
$licenseKey = config('ninja.license_key');
if (! $licenseKey) {
return false;
}
try {
$response = Http::timeout(15)
->get(config('ninja.hosted_ninja_url') . '/claim_license', [
'license_key' => $licenseKey,
'product_id' => 3,
]);
if ($response->successful()) {
$payload = $response->json();
if (is_array($payload) && isset($payload['expires'])) {
$expires = Carbon::parse($payload['expires']);
if ($expires->gt(now())) {
$account->plan_term = Account::PLAN_TERM_YEARLY;
$account->plan_expires = $expires->format('Y-m-d');
$account->plan = Account::PLAN_WHITE_LABEL;
$account->saveQuietly();
nlog('White label license auto-renewed. New expiry: ' . $expires->format('Y-m-d'));
return true;
}
}
}
return false;
} catch (ConnectionException $e) {
nlog('White label renewal check failed - network error: ' . $e->getMessage());
return null;
} catch (\Exception $e) {
nlog('White label renewal check failed: ' . $e->getMessage());
return null;
}
}
}

View File

@@ -181,6 +181,8 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
Route::post('charts/totals_v2', [ChartController::class, 'totalsV2'])->name('chart.totals_v2');
Route::post('charts/chart_summary_v2', [ChartController::class, 'chart_summaryV2'])->name('chart.chart_summary_v2');
Route::post('charts/calculated_fields', [ChartController::class, 'calculatedFields'])->name('chart.calculated_fields');
Route::post('charts/analytics_summary', [ChartController::class, 'analytics_summary'])->name('chart.analytics_summary');
Route::post('charts/analytics_totals', [ChartController::class, 'analytics_totals'])->name('chart.analytics_totals');
Route::post('claim_license', [LicenseController::class, 'index'])->name('license.index');
Route::post('check_license', [LicenseController::class, 'check'])->name('license.check');

View File

@@ -432,496 +432,496 @@ class EInvoiceValidationTest extends TestCase
}
public function testInValidBusinessClientNoVat()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => '',
'country_id' => 276,
'address1' => '10 Wallaby Way',
'address2' => '',
'city' => 'Sydney',
'state' => 'NSW',
'postal_code' => '2113',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'wasa@b.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertEquals(0, strlen($client->vat_number));
$this->assertFalse($validation['passes']);
}
public function testSeBusinessClientNeedsBothVatAndId()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// SE business with VAT but no id_number — should fail
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => 'SE123456789012',
'id_number' => '',
'country_id' => 752,
'address1' => '10 Wallaby Way',
'city' => 'Stockholm',
'state' => 'Stockholm',
'postal_code' => '11122',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertFalse($validation['passes']);
}
public function testSeBusinessClientPassesWithBoth()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => 'SE123456789012',
'id_number' => '5567891234',
'country_id' => 752,
'address1' => '10 Wallaby Way',
'city' => 'Stockholm',
'state' => 'Stockholm',
'postal_code' => '11122',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertTrue($validation['passes']);
}
public function testNoBusinessClientNeedsBothVatAndId()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// NO business with VAT but no id_number (ORG) — should fail
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => 'NO123456789MVA',
'id_number' => '',
'country_id' => 578,
'address1' => '10 Karl Johans gate',
'city' => 'Oslo',
'state' => 'Oslo',
'postal_code' => '0154',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertFalse($validation['passes']);
}
public function testNoBusinessClientPassesWithBoth()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => 'NO123456789MVA',
'id_number' => '123456789',
'country_id' => 578,
'address1' => '10 Karl Johans gate',
'city' => 'Oslo',
'state' => 'Oslo',
'postal_code' => '0154',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertTrue($validation['passes']);
}
public function testBeBusinessClientNeedsBothVatAndId()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// BE business with VAT but no id_number (EN) — should fail
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => 'BE0123456789',
'id_number' => '',
'country_id' => 56,
'address1' => '10 Rue de la Loi',
'city' => 'Brussels',
'state' => 'Brussels',
'postal_code' => '1000',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertFalse($validation['passes']);
}
public function testBeBusinessClientPassesWithBothValidIdentifiers()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// BE business with valid VAT and valid id_number (EN) — should pass
// 0202239951 is a known valid mod-97 enterprise number
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => 'BE0202239951',
'id_number' => '0202239951',
'country_id' => 56,
'address1' => '10 Rue de la Loi',
'city' => 'Brussels',
'state' => 'Brussels',
'postal_code' => '1000',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertTrue($validation['passes']);
}
public function testBeBusinessClientFailsWithInvalidCheckdigit()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// 0123456789 fails mod-97 checkdigit
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => 'BE0123456789',
'id_number' => '0123456789',
'country_id' => 56,
'address1' => '10 Rue de la Loi',
'city' => 'Brussels',
'state' => 'Brussels',
'postal_code' => '1000',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertFalse($validation['passes']);
}
public function testBeGovClientPassesWithValidIdentifiers()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// BE government with valid identifiers — should pass (routing rules say B+G)
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'government',
'vat_number' => 'BE0404616494',
'id_number' => '0404616494',
'country_id' => 56,
'address1' => '16 Rue de la Loi',
'city' => 'Brussels',
'state' => 'Brussels',
'postal_code' => '1000',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertTrue($validation['passes']);
}
public function testBeBusinessClientFailsWithNoVat()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// BE business with id_number but no VAT — should fail
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => '',
'id_number' => '0202239951',
'country_id' => 56,
'address1' => '10 Rue de la Loi',
'city' => 'Brussels',
'state' => 'Brussels',
'postal_code' => '1000',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertFalse($validation['passes']);
}
public function testBeBusinessClientPassesWithPrefixedIdNumber()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// BE:EN regex allows optional BE prefix on id_number
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'business',
'vat_number' => 'BE0471811661',
'id_number' => 'BE0471811661',
'country_id' => 56,
'address1' => '10 Rue de la Loi',
'city' => 'Brussels',
'state' => 'Brussels',
'postal_code' => '1000',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertTrue($validation['passes']);
}
public function testAtGovClientNeedsIdNumber()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// AT government with no id_number (AT:GOV) — should fail
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'government',
'vat_number' => 'ATU12345678',
'id_number' => '',
'country_id' => 40,
'address1' => '10 Ballhausplatz',
'city' => 'Vienna',
'state' => 'Vienna',
'postal_code' => '1010',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertFalse($validation['passes']);
}
public function testIndividualClientSkipsIdentifierValidation()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// SE individual with no VAT or id_number — should fail
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'individual',
'vat_number' => '',
'id_number' => '',
'country_id' => 752,
'address1' => '10 Wallaby Way',
'city' => 'Stockholm',
'state' => 'Stockholm',
'postal_code' => '11122',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertFalse($validation['passes']);
}
public function testDeGovClientNeedsIdNumber()
{
$company = Company::factory()->create([
'account_id' => $this->account->id,
]);
// DE government with no id_number (LWID) — should fail
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'classification' => 'government',
'vat_number' => 'DE123456789',
'id_number' => '',
'country_id' => 276,
'address1' => '10 Unter den Linden',
'city' => 'Berlin',
'state' => 'Berlin',
'postal_code' => '10117',
]);
$cc = ClientContact::factory()->create([
'client_id' => $client->id,
'user_id' => $this->user->id,
'company_id' => $company->id,
'first_name' => 'Bob',
'last_name' => 'Doe',
'email' => 'bob@example.com',
]);
$el = new EntityLevel();
$validation = $el->checkClient($client);
$this->assertFalse($validation['passes']);
}
// public function testInValidBusinessClientNoVat()
// {
// $company = Company::factory()->create([
// 'account_id' => $this->account->id,
// ]);
// $client = Client::factory()->create([
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'classification' => 'business',
// 'vat_number' => '',
// 'country_id' => 276,
// 'address1' => '10 Wallaby Way',
// 'address2' => '',
// 'city' => 'Sydney',
// 'state' => 'NSW',
// 'postal_code' => '2113',
// ]);
// $cc = ClientContact::factory()->create([
// 'client_id' => $client->id,
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'first_name' => 'Bob',
// 'last_name' => 'Doe',
// 'email' => 'wasa@b.com',
// ]);
// $el = new EntityLevel();
// $validation = $el->checkClient($client);
// $this->assertEquals(0, strlen($client->vat_number));
// $this->assertFalse($validation['passes']);
// }
// // public function testSeBusinessClientNeedsBothVatAndId()
// // {
// // $company = Company::factory()->create([
// // 'account_id' => $this->account->id,
// // ]);
// // // SE business with VAT but no id_number — should fail
// // $client = Client::factory()->create([
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'classification' => 'business',
// // 'vat_number' => 'SE123456789012',
// // 'id_number' => '',
// // 'country_id' => 752,
// // 'address1' => '10 Wallaby Way',
// // 'city' => 'Stockholm',
// // 'state' => 'Stockholm',
// // 'postal_code' => '11122',
// // ]);
// // $cc = ClientContact::factory()->create([
// // 'client_id' => $client->id,
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'first_name' => 'Bob',
// // 'last_name' => 'Doe',
// // 'email' => 'bob@example.com',
// // ]);
// // $el = new EntityLevel();
// // $validation = $el->checkClient($client);
// // $this->assertFalse($validation['passes']);
// // }
// // public function testSeBusinessClientPassesWithBoth()
// // {
// // $company = Company::factory()->create([
// // 'account_id' => $this->account->id,
// // ]);
// // $client = Client::factory()->create([
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'classification' => 'business',
// // 'vat_number' => 'SE123456789012',
// // 'id_number' => '5567891234',
// // 'country_id' => 752,
// // 'address1' => '10 Wallaby Way',
// // 'city' => 'Stockholm',
// // 'state' => 'Stockholm',
// // 'postal_code' => '11122',
// // ]);
// // $cc = ClientContact::factory()->create([
// // 'client_id' => $client->id,
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'first_name' => 'Bob',
// // 'last_name' => 'Doe',
// // 'email' => 'bob@example.com',
// // ]);
// // $el = new EntityLevel();
// // $validation = $el->checkClient($client);
// // $this->assertTrue($validation['passes']);
// // }
// // public function testNoBusinessClientNeedsBothVatAndId()
// // {
// // $company = Company::factory()->create([
// // 'account_id' => $this->account->id,
// // ]);
// // // NO business with VAT but no id_number (ORG) — should fail
// // $client = Client::factory()->create([
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'classification' => 'business',
// // 'vat_number' => 'NO123456789MVA',
// // 'id_number' => '',
// // 'country_id' => 578,
// // 'address1' => '10 Karl Johans gate',
// // 'city' => 'Oslo',
// // 'state' => 'Oslo',
// // 'postal_code' => '0154',
// // ]);
// // $cc = ClientContact::factory()->create([
// // 'client_id' => $client->id,
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'first_name' => 'Bob',
// // 'last_name' => 'Doe',
// // 'email' => 'bob@example.com',
// // ]);
// // $el = new EntityLevel();
// // $validation = $el->checkClient($client);
// // $this->assertFalse($validation['passes']);
// // }
// // public function testNoBusinessClientPassesWithBoth()
// // {
// // $company = Company::factory()->create([
// // 'account_id' => $this->account->id,
// // ]);
// // $client = Client::factory()->create([
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'classification' => 'business',
// // 'vat_number' => 'NO123456789MVA',
// // 'id_number' => '123456789',
// // 'country_id' => 578,
// // 'address1' => '10 Karl Johans gate',
// // 'city' => 'Oslo',
// // 'state' => 'Oslo',
// // 'postal_code' => '0154',
// // ]);
// // $cc = ClientContact::factory()->create([
// // 'client_id' => $client->id,
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'first_name' => 'Bob',
// // 'last_name' => 'Doe',
// // 'email' => 'bob@example.com',
// // ]);
// // $el = new EntityLevel();
// // $validation = $el->checkClient($client);
// // $this->assertTrue($validation['passes']);
// // }
// // public function testBeBusinessClientNeedsBothVatAndId()
// // {
// // $company = Company::factory()->create([
// // 'account_id' => $this->account->id,
// // ]);
// // // BE business with VAT but no id_number (EN) — should fail
// // $client = Client::factory()->create([
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'classification' => 'business',
// // 'vat_number' => 'BE0123456789',
// // 'id_number' => '',
// // 'country_id' => 56,
// // 'address1' => '10 Rue de la Loi',
// // 'city' => 'Brussels',
// // 'state' => 'Brussels',
// // 'postal_code' => '1000',
// // ]);
// // $cc = ClientContact::factory()->create([
// // 'client_id' => $client->id,
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'first_name' => 'Bob',
// // 'last_name' => 'Doe',
// // 'email' => 'bob@example.com',
// // ]);
// // $el = new EntityLevel();
// // $validation = $el->checkClient($client);
// // $this->assertFalse($validation['passes']);
// // }
// // public function testBeBusinessClientPassesWithBothValidIdentifiers()
// // {
// // $company = Company::factory()->create([
// // 'account_id' => $this->account->id,
// // ]);
// // // BE business with valid VAT and valid id_number (EN) — should pass
// // // 0202239951 is a known valid mod-97 enterprise number
// // $client = Client::factory()->create([
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'classification' => 'business',
// // 'vat_number' => 'BE0202239951',
// // 'id_number' => '0202239951',
// // 'country_id' => 56,
// // 'address1' => '10 Rue de la Loi',
// // 'city' => 'Brussels',
// // 'state' => 'Brussels',
// // 'postal_code' => '1000',
// // ]);
// // $cc = ClientContact::factory()->create([
// // 'client_id' => $client->id,
// // 'user_id' => $this->user->id,
// // 'company_id' => $company->id,
// // 'first_name' => 'Bob',
// // 'last_name' => 'Doe',
// // 'email' => 'bob@example.com',
// // ]);
// // $el = new EntityLevel();
// // $validation = $el->checkClient($client);
// // $this->assertTrue($validation['passes']);
// // }
// public function testBeBusinessClientFailsWithInvalidCheckdigit()
// {
// $company = Company::factory()->create([
// 'account_id' => $this->account->id,
// ]);
// // 0123456789 fails mod-97 checkdigit
// $client = Client::factory()->create([
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'classification' => 'business',
// 'vat_number' => 'BE0123456789',
// 'id_number' => '0123456789',
// 'country_id' => 56,
// 'address1' => '10 Rue de la Loi',
// 'city' => 'Brussels',
// 'state' => 'Brussels',
// 'postal_code' => '1000',
// ]);
// $cc = ClientContact::factory()->create([
// 'client_id' => $client->id,
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'first_name' => 'Bob',
// 'last_name' => 'Doe',
// 'email' => 'bob@example.com',
// ]);
// $el = new EntityLevel();
// $validation = $el->checkClient($client);
// $this->assertFalse($validation['passes']);
// }
// public function testBeGovClientPassesWithValidIdentifiers()
// {
// $company = Company::factory()->create([
// 'account_id' => $this->account->id,
// ]);
// // BE government with valid identifiers — should pass (routing rules say B+G)
// $client = Client::factory()->create([
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'classification' => 'government',
// 'vat_number' => 'BE0404616494',
// 'id_number' => '0404616494',
// 'country_id' => 56,
// 'address1' => '16 Rue de la Loi',
// 'city' => 'Brussels',
// 'state' => 'Brussels',
// 'postal_code' => '1000',
// ]);
// $cc = ClientContact::factory()->create([
// 'client_id' => $client->id,
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'first_name' => 'Bob',
// 'last_name' => 'Doe',
// 'email' => 'bob@example.com',
// ]);
// $el = new EntityLevel();
// $validation = $el->checkClient($client);
// $this->assertTrue($validation['passes']);
// }
// public function testBeBusinessClientFailsWithNoVat()
// {
// $company = Company::factory()->create([
// 'account_id' => $this->account->id,
// ]);
// // BE business with id_number but no VAT — should fail
// $client = Client::factory()->create([
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'classification' => 'business',
// 'vat_number' => '',
// 'id_number' => '0202239951',
// 'country_id' => 56,
// 'address1' => '10 Rue de la Loi',
// 'city' => 'Brussels',
// 'state' => 'Brussels',
// 'postal_code' => '1000',
// ]);
// $cc = ClientContact::factory()->create([
// 'client_id' => $client->id,
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'first_name' => 'Bob',
// 'last_name' => 'Doe',
// 'email' => 'bob@example.com',
// ]);
// $el = new EntityLevel();
// $validation = $el->checkClient($client);
// $this->assertFalse($validation['passes']);
// }
// public function testBeBusinessClientPassesWithPrefixedIdNumber()
// {
// $company = Company::factory()->create([
// 'account_id' => $this->account->id,
// ]);
// // BE:EN regex allows optional BE prefix on id_number
// $client = Client::factory()->create([
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'classification' => 'business',
// 'vat_number' => 'BE0471811661',
// 'id_number' => 'BE0471811661',
// 'country_id' => 56,
// 'address1' => '10 Rue de la Loi',
// 'city' => 'Brussels',
// 'state' => 'Brussels',
// 'postal_code' => '1000',
// ]);
// $cc = ClientContact::factory()->create([
// 'client_id' => $client->id,
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'first_name' => 'Bob',
// 'last_name' => 'Doe',
// 'email' => 'bob@example.com',
// ]);
// $el = new EntityLevel();
// $validation = $el->checkClient($client);
// $this->assertTrue($validation['passes']);
// }
// public function testAtGovClientNeedsIdNumber()
// {
// $company = Company::factory()->create([
// 'account_id' => $this->account->id,
// ]);
// // AT government with no id_number (AT:GOV) — should fail
// $client = Client::factory()->create([
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'classification' => 'government',
// 'vat_number' => 'ATU12345678',
// 'id_number' => '',
// 'country_id' => 40,
// 'address1' => '10 Ballhausplatz',
// 'city' => 'Vienna',
// 'state' => 'Vienna',
// 'postal_code' => '1010',
// ]);
// $cc = ClientContact::factory()->create([
// 'client_id' => $client->id,
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'first_name' => 'Bob',
// 'last_name' => 'Doe',
// 'email' => 'bob@example.com',
// ]);
// $el = new EntityLevel();
// $validation = $el->checkClient($client);
// $this->assertFalse($validation['passes']);
// }
// public function testIndividualClientSkipsIdentifierValidation()
// {
// $company = Company::factory()->create([
// 'account_id' => $this->account->id,
// ]);
// // SE individual with no VAT or id_number — should fail
// $client = Client::factory()->create([
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'classification' => 'individual',
// 'vat_number' => '',
// 'id_number' => '',
// 'country_id' => 752,
// 'address1' => '10 Wallaby Way',
// 'city' => 'Stockholm',
// 'state' => 'Stockholm',
// 'postal_code' => '11122',
// ]);
// $cc = ClientContact::factory()->create([
// 'client_id' => $client->id,
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'first_name' => 'Bob',
// 'last_name' => 'Doe',
// 'email' => 'bob@example.com',
// ]);
// $el = new EntityLevel();
// $validation = $el->checkClient($client);
// $this->assertFalse($validation['passes']);
// }
// public function testDeGovClientNeedsIdNumber()
// {
// $company = Company::factory()->create([
// 'account_id' => $this->account->id,
// ]);
// // DE government with no id_number (LWID) — should fail
// $client = Client::factory()->create([
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'classification' => 'government',
// 'vat_number' => 'DE123456789',
// 'id_number' => '',
// 'country_id' => 276,
// 'address1' => '10 Unter den Linden',
// 'city' => 'Berlin',
// 'state' => 'Berlin',
// 'postal_code' => '10117',
// ]);
// $cc = ClientContact::factory()->create([
// 'client_id' => $client->id,
// 'user_id' => $this->user->id,
// 'company_id' => $company->id,
// 'first_name' => 'Bob',
// 'last_name' => 'Doe',
// 'email' => 'bob@example.com',
// ]);
// $el = new EntityLevel();
// $validation = $el->checkClient($client);
// $this->assertFalse($validation['passes']);
// }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,141 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2026. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Unit;
use App\Models\Account;
use App\Services\License\WhiteLabelRenewalService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Tests\MockAccountData;
use Tests\TestCase;
class WhiteLabelRenewalTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
private WhiteLabelRenewalService $service;
protected function setUp(): void
{
parent::setUp();
$this->makeTestData();
$this->service = new WhiteLabelRenewalService();
}
public function testRenewedLicenseUpdatesAccount(): void
{
config(['ninja.license_key' => 'v5_test_license_key']);
$account = $this->account;
$account->plan = Account::PLAN_WHITE_LABEL;
$account->plan_expires = now()->subDay()->format('Y-m-d');
$account->save();
$futureDate = now()->addYear()->format('Y-m-d');
Http::fake([
'*/claim_license*' => Http::response(['expires' => $futureDate], 200),
]);
$result = $this->service->checkAndRenew($account);
$this->assertTrue($result);
$account->refresh();
$this->assertEquals(Account::PLAN_WHITE_LABEL, $account->plan);
$this->assertEquals($futureDate, $account->plan_expires);
$this->assertEquals(Account::PLAN_TERM_YEARLY, $account->plan_term);
}
public function testNotRenewedLicenseReturnsFalse(): void
{
config(['ninja.license_key' => 'v5_test_license_key']);
$account = $this->account;
$account->plan = Account::PLAN_WHITE_LABEL;
$account->plan_expires = now()->subDay()->format('Y-m-d');
$account->save();
Http::fake([
'*/claim_license*' => Http::response(['message' => 'Invalid license'], 400),
]);
$result = $this->service->checkAndRenew($account);
$this->assertFalse($result);
}
public function testNetworkErrorReturnsNull(): void
{
config(['ninja.license_key' => 'v5_test_license_key']);
$account = $this->account;
$account->plan = Account::PLAN_WHITE_LABEL;
$account->plan_expires = now()->subDay()->format('Y-m-d');
$account->save();
Http::fake([
'*/claim_license*' => fn () => throw new ConnectionException('Connection timed out'),
]);
$result = $this->service->checkAndRenew($account);
$this->assertNull($result);
$account->refresh();
$this->assertEquals(Account::PLAN_WHITE_LABEL, $account->plan);
}
public function testNoLicenseKeyReturnsFalse(): void
{
config(['ninja.license_key' => false]);
$account = $this->account;
$account->plan = Account::PLAN_WHITE_LABEL;
$account->plan_expires = now()->subDay()->format('Y-m-d');
$account->save();
Http::fake();
$result = $this->service->checkAndRenew($account);
$this->assertFalse($result);
Http::assertNothingSent();
}
public function testExpiredRenewalDateReturnsFalse(): void
{
config(['ninja.license_key' => 'v5_test_license_key']);
$account = $this->account;
$account->plan = Account::PLAN_WHITE_LABEL;
$account->plan_expires = now()->subDay()->format('Y-m-d');
$account->save();
$pastDate = now()->subMonth()->format('Y-m-d');
Http::fake([
'*/claim_license*' => Http::response(['expires' => $pastDate], 200),
]);
$result = $this->service->checkAndRenew($account);
$this->assertFalse($result);
$account->refresh();
$this->assertEquals(Account::PLAN_WHITE_LABEL, $account->plan);
}
}