mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 00:57:02 +00:00
Quickbooks validation tests
This commit is contained in:
@@ -1 +1 @@
|
|||||||
5.12.49
|
5.12.50
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoice Ninja (https://invoiceninja.com).
|
|
||||||
*
|
|
||||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
|
||||||
*
|
|
||||||
* @license https://www.elastic.co/licensing/elastic-license
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\DataMapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* QuickbooksPushEvents.
|
|
||||||
*
|
|
||||||
* Stores push event configuration for QuickBooks integration.
|
|
||||||
* This class provides a clean separation of push event settings.
|
|
||||||
*/
|
|
||||||
class QuickbooksPushEvents
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Push when a new client is created.
|
|
||||||
*/
|
|
||||||
public bool $push_on_new_client = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push when an existing client is updated.
|
|
||||||
*/
|
|
||||||
public bool $push_on_updated_client = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push when an invoice status matches one of these values.
|
|
||||||
*
|
|
||||||
* Valid values: 'draft', 'sent', 'paid', 'deleted'
|
|
||||||
*
|
|
||||||
* @var array<string>
|
|
||||||
*/
|
|
||||||
public array $push_invoice_statuses = [];
|
|
||||||
|
|
||||||
public function __construct(array $attributes = [])
|
|
||||||
{
|
|
||||||
$this->push_on_new_client = $attributes['push_on_new_client'] ?? false;
|
|
||||||
$this->push_on_updated_client = $attributes['push_on_updated_client'] ?? false;
|
|
||||||
$this->push_invoice_statuses = $attributes['push_invoice_statuses'] ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'push_on_new_client' => $this->push_on_new_client,
|
|
||||||
'push_on_updated_client' => $this->push_on_updated_client,
|
|
||||||
'push_invoice_statuses' => $this->push_invoice_statuses,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,6 +32,8 @@ class QuickbooksSettings implements Castable
|
|||||||
|
|
||||||
public string $baseURL;
|
public string $baseURL;
|
||||||
|
|
||||||
|
public string $companyName;
|
||||||
|
|
||||||
public QuickbooksSync $settings;
|
public QuickbooksSync $settings;
|
||||||
|
|
||||||
public function __construct(array $attributes = [])
|
public function __construct(array $attributes = [])
|
||||||
@@ -42,6 +44,7 @@ class QuickbooksSettings implements Castable
|
|||||||
$this->accessTokenExpiresAt = $attributes['accessTokenExpiresAt'] ?? 0;
|
$this->accessTokenExpiresAt = $attributes['accessTokenExpiresAt'] ?? 0;
|
||||||
$this->refreshTokenExpiresAt = $attributes['refreshTokenExpiresAt'] ?? 0;
|
$this->refreshTokenExpiresAt = $attributes['refreshTokenExpiresAt'] ?? 0;
|
||||||
$this->baseURL = $attributes['baseURL'] ?? '';
|
$this->baseURL = $attributes['baseURL'] ?? '';
|
||||||
|
$this->companyName = $attributes['companyName'] ?? '';
|
||||||
$this->settings = new QuickbooksSync($attributes['settings'] ?? []);
|
$this->settings = new QuickbooksSync($attributes['settings'] ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +67,7 @@ class QuickbooksSettings implements Castable
|
|||||||
'accessTokenExpiresAt' => $this->accessTokenExpiresAt,
|
'accessTokenExpiresAt' => $this->accessTokenExpiresAt,
|
||||||
'refreshTokenExpiresAt' => $this->refreshTokenExpiresAt,
|
'refreshTokenExpiresAt' => $this->refreshTokenExpiresAt,
|
||||||
'baseURL' => $this->baseURL,
|
'baseURL' => $this->baseURL,
|
||||||
|
'companyName' => $this->companyName,
|
||||||
'settings' => $this->settings->toArray(),
|
'settings' => $this->settings->toArray(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ class QuickbooksSync
|
|||||||
|
|
||||||
public string $default_expense_account = '';
|
public string $default_expense_account = '';
|
||||||
|
|
||||||
public QuickbooksPushEvents $push_events;
|
|
||||||
|
|
||||||
public function __construct(array $attributes = [])
|
public function __construct(array $attributes = [])
|
||||||
{
|
{
|
||||||
$this->client = new QuickbooksSyncMap($attributes['client'] ?? []);
|
$this->client = new QuickbooksSyncMap($attributes['client'] ?? []);
|
||||||
@@ -54,7 +52,6 @@ class QuickbooksSync
|
|||||||
$this->expense = new QuickbooksSyncMap($attributes['expense'] ?? []);
|
$this->expense = new QuickbooksSyncMap($attributes['expense'] ?? []);
|
||||||
$this->default_income_account = $attributes['default_income_account'] ?? '';
|
$this->default_income_account = $attributes['default_income_account'] ?? '';
|
||||||
$this->default_expense_account = $attributes['default_expense_account'] ?? '';
|
$this->default_expense_account = $attributes['default_expense_account'] ?? '';
|
||||||
$this->push_events = new QuickbooksPushEvents($attributes['push_events'] ?? []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toArray(): array
|
public function toArray(): array
|
||||||
@@ -71,7 +68,6 @@ class QuickbooksSync
|
|||||||
'expense' => $this->expense->toArray(),
|
'expense' => $this->expense->toArray(),
|
||||||
'default_income_account' => $this->default_income_account,
|
'default_income_account' => $this->default_income_account,
|
||||||
'default_expense_account' => $this->default_expense_account,
|
'default_expense_account' => $this->default_expense_account,
|
||||||
'push_events' => $this->push_events->toArray(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ use App\Enum\SyncDirection;
|
|||||||
*/
|
*/
|
||||||
class QuickbooksSyncMap
|
class QuickbooksSyncMap
|
||||||
{
|
{
|
||||||
public SyncDirection $direction = SyncDirection::BIDIRECTIONAL;
|
public SyncDirection $direction = SyncDirection::NONE;
|
||||||
|
|
||||||
public function __construct(array $attributes = [])
|
public function __construct(array $attributes = [])
|
||||||
{
|
{
|
||||||
$this->direction = isset($attributes['direction'])
|
$this->direction = isset($attributes['direction'])
|
||||||
? SyncDirection::from($attributes['direction'])
|
? SyncDirection::from($attributes['direction'])
|
||||||
: SyncDirection::BIDIRECTIONAL;
|
: SyncDirection::NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toArray(): array
|
public function toArray(): array
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ class ImportQuickbooksController extends BaseController
|
|||||||
|
|
||||||
$authorizationUrl = $qb->sdk()->getAuthorizationUrl();
|
$authorizationUrl = $qb->sdk()->getAuthorizationUrl();
|
||||||
|
|
||||||
nlog($authorizationUrl);
|
|
||||||
|
|
||||||
$state = $qb->sdk()->getState();
|
$state = $qb->sdk()->getState();
|
||||||
|
|
||||||
Cache::put($state, $token, 190);
|
Cache::put($state, $token, 190);
|
||||||
|
|||||||
63
app/Http/Controllers/QuickbooksController.php
Normal file
63
app/Http/Controllers/QuickbooksController.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Libraries\MultiDB;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use App\Http\Requests\Quickbooks\ConfigQuickbooksRequest;
|
||||||
|
use App\Http\Requests\Quickbooks\DisconnectQuickbooksRequest;
|
||||||
|
use App\Http\Requests\Quickbooks\SyncQuickbooksRequest;
|
||||||
|
|
||||||
|
class QuickbooksController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
public function sync(SyncQuickbooksRequest $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configuration(ConfigQuickbooksRequest $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
$company = $user->company();
|
||||||
|
|
||||||
|
$quickbooks = $company->quickbooks;
|
||||||
|
$quickbooks->settings->client->direction = $request->clients ? SyncDirection::PUSH : SyncDirection::NONE;
|
||||||
|
$quickbooks->settings->vendor->direction = $request->vendors ? SyncDirection::PUSH : SyncDirection::NONE;
|
||||||
|
$quickbooks->settings->product->direction = $request->products ? SyncDirection::PUSH : SyncDirection::NONE;
|
||||||
|
$quickbooks->settings->invoice->direction = $request->invoices ? SyncDirection::PUSH : SyncDirection::NONE;
|
||||||
|
$quickbooks->settings->quote->direction = $request->quotes ? SyncDirection::PUSH : SyncDirection::NONE;
|
||||||
|
$quickbooks->settings->payment->direction = $request->payments ? SyncDirection::PUSH : SyncDirection::NONE;
|
||||||
|
$company->quickbooks = $quickbooks;
|
||||||
|
$company->save();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disconnect(DisconnectQuickbooksRequest $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
$company = $user->company();
|
||||||
|
|
||||||
|
$qb = new QuickbooksService($company);
|
||||||
|
$qb->sdk()->revokeAccessToken();
|
||||||
|
|
||||||
|
$company->quickbooks = null;
|
||||||
|
$company->save();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/Http/Requests/Quickbooks/ConfigQuickbooksRequest.php
Normal file
49
app/Http/Requests/Quickbooks/ConfigQuickbooksRequest.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Quickbooks;
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class ConfigQuickbooksRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'invoices' => 'required|boolean|bail',
|
||||||
|
'quotes' => 'required|boolean|bail',
|
||||||
|
'payments' => 'required|boolean|bail',
|
||||||
|
'products' => 'required|boolean|bail',
|
||||||
|
'vendors' => 'required|boolean|bail',
|
||||||
|
'clients' => 'required|boolean|bail',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
app/Http/Requests/Quickbooks/DisconnectQuickbooksRequest.php
Normal file
44
app/Http/Requests/Quickbooks/DisconnectQuickbooksRequest.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Quickbooks;
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class DisconnectQuickbooksRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
97
app/Http/Requests/Quickbooks/SyncQuickbooksRequest.php
Normal file
97
app/Http/Requests/Quickbooks/SyncQuickbooksRequest.php
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Quickbooks;
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class SyncQuickbooksRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'clients' => [
|
||||||
|
'present_with:invoices,quotes,payments',
|
||||||
|
'nullable',
|
||||||
|
function ($attribute, $value, $fail) {
|
||||||
|
// If value is provided and not empty, validate it
|
||||||
|
if ($value !== null && $value !== '' && !in_array($value, ['email', 'name'])) {
|
||||||
|
$fail('The ' . $attribute . ' must be one of: email, name.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'products' => ['sometimes', 'nullable', function ($attribute, $value, $fail) {
|
||||||
|
if ($value !== null && $value !== '' && $value !== 'product_key') {
|
||||||
|
$fail('The ' . $attribute . ' must be product_key.');
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
'invoices' => ['sometimes', 'nullable', function ($attribute, $value, $fail) {
|
||||||
|
if ($value !== null && $value !== '' && $value !== 'number') {
|
||||||
|
$fail('The ' . $attribute . ' must be number.');
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
'quotes' => ['sometimes', 'nullable', function ($attribute, $value, $fail) {
|
||||||
|
if ($value !== null && $value !== '' && $value !== 'number') {
|
||||||
|
$fail('The ' . $attribute . ' must be number.');
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
'payments' => 'sometimes|nullable',
|
||||||
|
'vendors' => ['sometimes', 'nullable', function ($attribute, $value, $fail) {
|
||||||
|
if ($value !== null && $value !== '' && !in_array($value, ['email', 'name'])) {
|
||||||
|
$fail('The ' . $attribute . ' must be one of: email, name.');
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the data for validation.
|
||||||
|
* Convert empty strings to null for nullable fields.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$input = $this->all();
|
||||||
|
|
||||||
|
// Convert empty strings to null for nullable fields
|
||||||
|
$nullableFields = ['clients', 'products', 'invoices', 'quotes', 'payments', 'vendors'];
|
||||||
|
|
||||||
|
foreach ($nullableFields as $field) {
|
||||||
|
if (isset($input[$field]) && $input[$field] === '') {
|
||||||
|
$input[$field] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->replace($input);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1089,7 +1089,7 @@ class Company extends BaseModel
|
|||||||
|
|
||||||
// Cache the detailed check for this request lifecycle
|
// Cache the detailed check for this request lifecycle
|
||||||
// This prevents re-checking if called multiple times in the same request
|
// This prevents re-checking if called multiple times in the same request
|
||||||
return once(function () use ($entity, $action, $status) {
|
return once(function () use ($entity) {
|
||||||
// Check if QuickBooks is actually configured (has token)
|
// Check if QuickBooks is actually configured (has token)
|
||||||
if (!$this->quickbooks->isConfigured()) {
|
if (!$this->quickbooks->isConfigured()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1104,26 +1104,7 @@ class Company extends BaseModel
|
|||||||
$direction = $entitySettings->direction->value;
|
$direction = $entitySettings->direction->value;
|
||||||
|
|
||||||
// Check if sync direction allows push
|
// Check if sync direction allows push
|
||||||
if ($direction !== 'push' && $direction !== 'bidirectional') {
|
return $direction === 'push' || $direction === 'bidirectional';
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get push events from settings
|
|
||||||
$pushEvents = $this->quickbooks->settings->push_events;
|
|
||||||
|
|
||||||
// Check action-specific settings from QuickbooksPushEvents
|
|
||||||
return match($action) {
|
|
||||||
'create' => match($entity) {
|
|
||||||
'client' => $pushEvents->push_on_new_client ?? false,
|
|
||||||
default => false, // Other entities can be added here
|
|
||||||
},
|
|
||||||
'update' => match($entity) {
|
|
||||||
'client' => $pushEvents->push_on_updated_client ?? false,
|
|
||||||
default => false, // Other entities can be added here
|
|
||||||
},
|
|
||||||
'status' => $status && in_array($status, $pushEvents->push_invoice_statuses ?? []),
|
|
||||||
default => false,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ return [
|
|||||||
'require_https' => env('REQUIRE_HTTPS', true),
|
'require_https' => env('REQUIRE_HTTPS', true),
|
||||||
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
'app_url' => rtrim(env('APP_URL', ''), '/'),
|
||||||
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
|
||||||
'app_version' => env('APP_VERSION', '5.12.49'),
|
'app_version' => env('APP_VERSION', '5.12.50'),
|
||||||
'app_tag' => env('APP_TAG', '5.12.49'),
|
'app_tag' => env('APP_TAG', '5.12.50'),
|
||||||
'minimum_client_version' => '5.0.16',
|
'minimum_client_version' => '5.0.16',
|
||||||
'terms_version' => '1.0.1',
|
'terms_version' => '1.0.1',
|
||||||
'api_secret' => env('API_SECRET', false),
|
'api_secret' => env('API_SECRET', false),
|
||||||
|
|||||||
@@ -336,6 +336,9 @@ Route::group(['middleware' => ['throttle:api', 'token_auth', 'valid_json','local
|
|||||||
Route::get('quote/{invitation_key}/download', [QuoteController::class, 'downloadPdf'])->name('quotes.downloadPdf');
|
Route::get('quote/{invitation_key}/download', [QuoteController::class, 'downloadPdf'])->name('quotes.downloadPdf');
|
||||||
Route::get('quote/{invitation_key}/download_e_quote', [QuoteController::class, 'downloadEQuote'])->name('quotes.downloadEQuote');
|
Route::get('quote/{invitation_key}/download_e_quote', [QuoteController::class, 'downloadEQuote'])->name('quotes.downloadEQuote');
|
||||||
|
|
||||||
|
Route::post('quickbooks/sync', [ImportQuickbooksController::class, 'sync'])->name('quickbooks.sync');
|
||||||
|
Route::post('quickbooks/configuration', [ImportQuickbooksController::class, 'configuration'])->name('quickbooks.configuration');
|
||||||
|
|
||||||
Route::resource('recurring_expenses', RecurringExpenseController::class);
|
Route::resource('recurring_expenses', RecurringExpenseController::class);
|
||||||
Route::post('recurring_expenses/bulk', [RecurringExpenseController::class, 'bulk'])->name('recurring_expenses.bulk');
|
Route::post('recurring_expenses/bulk', [RecurringExpenseController::class, 'bulk'])->name('recurring_expenses.bulk');
|
||||||
Route::put('recurring_expenses/{recurring_expense}/upload', [RecurringExpenseController::class, 'upload']);
|
Route::put('recurring_expenses/{recurring_expense}/upload', [RecurringExpenseController::class, 'upload']);
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
<?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\Feature\Quickbooks\Validation;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use App\Http\Requests\Quickbooks\SyncQuickbooksRequest;
|
||||||
|
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||||
|
use Tests\MockAccountData;
|
||||||
|
|
||||||
|
class SyncQuickbooksRequestTest extends TestCase
|
||||||
|
{
|
||||||
|
use MockAccountData;
|
||||||
|
|
||||||
|
protected SyncQuickbooksRequest $request;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->request = new SyncQuickbooksRequest();
|
||||||
|
|
||||||
|
$this->withoutMiddleware(
|
||||||
|
ThrottleRequests::class
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->makeTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients can be provided on its own (without invoices/quotes/payments)
|
||||||
|
*/
|
||||||
|
public function testClientsCanBeProvidedAlone(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'email',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Clients should be valid when provided alone');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testClientsCanBeProvidedAloneWithEmptyString(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Clients should be valid when provided alone');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients can be null/empty when provided alone
|
||||||
|
*/
|
||||||
|
public function testClientsCanBeNullWhenAlone(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Clients should be valid when null and no invoices/quotes/payments');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients can be empty string when provided alone
|
||||||
|
*/
|
||||||
|
public function testClientsCanBeEmptyStringWhenAlone(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Clients should be valid when empty string and no invoices/quotes/payments');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients is required when invoices is present
|
||||||
|
*/
|
||||||
|
public function testClientsIsRequiredWhenInvoicesPresent(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'invoices' => 'number',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Clients should be required when invoices is present');
|
||||||
|
$this->assertArrayHasKey('clients', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients is required when quotes is present
|
||||||
|
*/
|
||||||
|
public function testClientsIsRequiredWhenQuotesPresent(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'quotes' => 'number',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Clients should be required when quotes is present');
|
||||||
|
$this->assertArrayHasKey('clients', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients is required when payments is present
|
||||||
|
*/
|
||||||
|
public function testClientsIsRequiredWhenPaymentsPresent(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'payments' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Clients should be required when payments is present');
|
||||||
|
$this->assertArrayHasKey('clients', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients is required when multiple dependent fields are present
|
||||||
|
*/
|
||||||
|
public function testClientsIsRequiredWhenMultipleDependentFieldsPresent(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'invoices' => 'number',
|
||||||
|
'quotes' => 'number',
|
||||||
|
'payments' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Clients should be required when invoices, quotes, and payments are present');
|
||||||
|
$this->assertArrayHasKey('clients', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients with valid value 'email' passes when invoices is present
|
||||||
|
*/
|
||||||
|
public function testClientsWithEmailPassesWhenInvoicesPresent(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'email',
|
||||||
|
'invoices' => 'number',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Clients with email should be valid when invoices is present');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients with valid value 'name' passes when invoices is present
|
||||||
|
*/
|
||||||
|
public function testClientsWithNamePassesWhenInvoicesPresent(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'name',
|
||||||
|
'invoices' => 'number',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Clients with name should be valid when invoices is present');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients with empty string passes when invoices is present (nullable)
|
||||||
|
*/
|
||||||
|
public function testClientsWithEmptyStringPassesWhenInvoicesPresent(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => '',
|
||||||
|
'invoices' => 'number',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Clients with empty string should be valid when invoices is present (nullable)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that clients with invalid value fails
|
||||||
|
*/
|
||||||
|
public function testClientsWithInvalidValueFails(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'invalid_value',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Clients with invalid value should fail');
|
||||||
|
$this->assertArrayHasKey('clients', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that products with valid value passes
|
||||||
|
*/
|
||||||
|
public function testProductsWithValidValuePasses(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'products' => 'product_key',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Products with product_key should be valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that products with invalid value fails
|
||||||
|
*/
|
||||||
|
public function testProductsWithInvalidValueFails(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'products' => 'invalid_value',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Products with invalid value should fail');
|
||||||
|
$this->assertArrayHasKey('products', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that invoices with valid value passes
|
||||||
|
*/
|
||||||
|
public function testInvoicesWithValidValuePasses(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'email',
|
||||||
|
'invoices' => 'number',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Invoices with number should be valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that invoices with invalid value fails
|
||||||
|
*/
|
||||||
|
public function testInvoicesWithInvalidValueFails(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'email',
|
||||||
|
'invoices' => 'invalid_value',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Invoices with invalid value should fail');
|
||||||
|
$this->assertArrayHasKey('invoices', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that quotes with valid value passes
|
||||||
|
*/
|
||||||
|
public function testQuotesWithValidValuePasses(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'email',
|
||||||
|
'quotes' => 'number',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Quotes with number should be valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that quotes with invalid value fails
|
||||||
|
*/
|
||||||
|
public function testQuotesWithInvalidValueFails(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'email',
|
||||||
|
'quotes' => 'invalid_value',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Quotes with invalid value should fail');
|
||||||
|
$this->assertArrayHasKey('quotes', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that vendors with valid value passes
|
||||||
|
*/
|
||||||
|
public function testVendorsWithValidValuePasses(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'vendors' => 'email',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Vendors with email should be valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that vendors with name value passes
|
||||||
|
*/
|
||||||
|
public function testVendorsWithNameValuePasses(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'vendors' => 'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Vendors with name should be valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that vendors with invalid value fails
|
||||||
|
*/
|
||||||
|
public function testVendorsWithInvalidValueFails(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'vendors' => 'invalid_value',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertFalse($validator->passes(), 'Vendors with invalid value should fail');
|
||||||
|
$this->assertArrayHasKey('vendors', $validator->errors()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that all fields can be provided together with valid values
|
||||||
|
*/
|
||||||
|
public function testAllFieldsWithValidValuesPasses(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'email',
|
||||||
|
'products' => 'product_key',
|
||||||
|
'invoices' => 'number',
|
||||||
|
'quotes' => 'number',
|
||||||
|
'payments' => true,
|
||||||
|
'vendors' => 'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'All fields with valid values should pass');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that empty request passes (all fields are optional)
|
||||||
|
*/
|
||||||
|
public function testEmptyRequestPasses(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Empty request should pass (all fields are optional)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that payments can be any value (no validation on payments field itself)
|
||||||
|
*/
|
||||||
|
public function testPaymentsCanBeAnyValue(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'clients' => 'email',
|
||||||
|
'payments' => 'any_value_here',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->request->initialize($data);
|
||||||
|
$validator = Validator::make($data, $this->request->rules());
|
||||||
|
|
||||||
|
$this->assertTrue($validator->passes(), 'Payments can be any value when clients is provided');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user