mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 03:07:01 +00:00
Fixes for throttle middleware
This commit is contained in:
@@ -21,18 +21,34 @@ class QuickbooksSyncMap
|
||||
{
|
||||
public SyncDirection $direction = SyncDirection::BIDIRECTIONAL;
|
||||
|
||||
// Push event settings (for PUSH direction)
|
||||
public bool $push_on_create = false; // Push when entity is created (e.g., new client)
|
||||
public bool $push_on_update = false; // Push when entity is updated (e.g., updated client)
|
||||
public array $push_on_statuses = []; // Push when entity status matches (e.g., invoice statuses: ['draft', 'sent', 'paid', 'deleted'])
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->direction = isset($attributes['direction'])
|
||||
? SyncDirection::from($attributes['direction'])
|
||||
: SyncDirection::BIDIRECTIONAL;
|
||||
|
||||
$this->push_on_create = $attributes['push_on_create'] ?? false;
|
||||
$this->push_on_update = $attributes['push_on_update'] ?? false;
|
||||
$this->push_on_statuses = $attributes['push_on_statuses'] ?? [];
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
// Ensure direction is always returned as a string value, not the enum object
|
||||
$directionValue = $this->direction instanceof \App\Enum\SyncDirection
|
||||
? $this->direction->value
|
||||
: (string) $this->direction;
|
||||
|
||||
return [
|
||||
'direction' => $this->direction->value,
|
||||
'direction' => $directionValue,
|
||||
'push_on_create' => $this->push_on_create,
|
||||
'push_on_update' => $this->push_on_update,
|
||||
'push_on_statuses' => $this->push_on_statuses,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1062,4 +1062,60 @@ class Company extends BaseModel
|
||||
return $this->getSetting('e_invoice_type') == 'VERIFACTU';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if QuickBooks push should be triggered for an entity/action.
|
||||
*
|
||||
* Uses efficient checks to avoid overhead for companies not using QuickBooks.
|
||||
* Uses once() to cache the result for the request lifecycle.
|
||||
*
|
||||
* This method is designed to be called from model observers to efficiently
|
||||
* determine if a push job should be dispatched, with zero overhead for
|
||||
* companies that don't use QuickBooks.
|
||||
*
|
||||
* @param string $entity Entity type: 'client', 'invoice', etc.
|
||||
* @param string $action Action type: 'create', 'update', 'status'
|
||||
* @param string|null $status Optional status for status-based pushes (e.g., invoice status: 'draft', 'sent', 'paid', 'deleted')
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldPushToQuickbooks(string $entity, string $action, ?string $status = null): bool
|
||||
{
|
||||
// FASTEST CHECK: Raw database column (no object instantiation, no JSON decode)
|
||||
// This is the cheapest possible check - just a null comparison
|
||||
// For companies without QuickBooks, this returns immediately with ~0.001ms overhead
|
||||
if (is_null($this->getRawOriginal('quickbooks'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cache the detailed check for this request lifecycle
|
||||
// This prevents re-checking if called multiple times in the same request
|
||||
// Note: once() caches per closure, so we need separate closures for different entity/action combinations
|
||||
return once(function () use ($entity, $action, $status) {
|
||||
// Check if QuickBooks is actually configured (has token)
|
||||
if (!$this->quickbooks->isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify entity exists in settings
|
||||
if (!isset($this->quickbooks->settings->{$entity})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entitySettings = $this->quickbooks->settings->{$entity};
|
||||
$direction = $entitySettings->direction->value;
|
||||
|
||||
// Check if sync direction allows push
|
||||
if ($direction !== 'push' && $direction !== 'bidirectional') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check action-specific settings
|
||||
return match($action) {
|
||||
'create' => $entitySettings->push_on_create ?? false,
|
||||
'update' => $entitySettings->push_on_update ?? false,
|
||||
'status' => $status && in_array($status, $entitySettings->push_on_statuses ?? []),
|
||||
default => false,
|
||||
};
|
||||
}, $cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,16 @@ class ClientObserver
|
||||
if ($subscriptions) {
|
||||
WebhookHandler::dispatch(Webhook::EVENT_CREATE_CLIENT, $client, $client->company)->delay(0);
|
||||
}
|
||||
|
||||
// QuickBooks push - efficient check in observer (zero overhead if not configured)
|
||||
if ($client->company->shouldPushToQuickbooks('client', 'create')) {
|
||||
\App\Jobs\Quickbooks\PushClientToQuickbooks::dispatch(
|
||||
$client->id,
|
||||
$client->company->id,
|
||||
$client->company->db,
|
||||
'create'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +125,16 @@ class ClientObserver
|
||||
if ($subscriptions) {
|
||||
WebhookHandler::dispatch($event, $client, $client->company, 'client')->delay(0);
|
||||
}
|
||||
|
||||
// QuickBooks push - efficient check in observer (zero overhead if not configured)
|
||||
if ($client->company->shouldPushToQuickbooks('client', 'update')) {
|
||||
\App\Jobs\Quickbooks\PushClientToQuickbooks::dispatch(
|
||||
$client->id,
|
||||
$client->company->id,
|
||||
$client->company->db,
|
||||
'update'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,16 @@ class InvoiceObserver
|
||||
if ($subscriptions) {
|
||||
WebhookHandler::dispatch(Webhook::EVENT_CREATE_INVOICE, $invoice, $invoice->company, 'client')->delay(0);
|
||||
}
|
||||
|
||||
// QuickBooks push - efficient check in observer (zero overhead if not configured)
|
||||
if ($invoice->company->shouldPushToQuickbooks('invoice', 'create')) {
|
||||
\App\Jobs\Quickbooks\PushInvoiceToQuickbooks::dispatch(
|
||||
$invoice->id,
|
||||
$invoice->company->id,
|
||||
$invoice->company->db,
|
||||
'create'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,6 +73,43 @@ class InvoiceObserver
|
||||
if ($subscriptions) {
|
||||
WebhookHandler::dispatch($event, $invoice, $invoice->company, 'client')->delay(0);
|
||||
}
|
||||
|
||||
// QuickBooks push - check push_on_update OR push_on_statuses
|
||||
// Map invoice status to string for status-based push check
|
||||
$invoiceStatus = $this->mapInvoiceStatusToString($invoice->status_id, $invoice->is_deleted);
|
||||
|
||||
$shouldPush = $invoice->company->shouldPushToQuickbooks('invoice', 'update') ||
|
||||
$invoice->company->shouldPushToQuickbooks('invoice', 'status', $invoiceStatus);
|
||||
|
||||
if ($shouldPush) {
|
||||
\App\Jobs\Quickbooks\PushInvoiceToQuickbooks::dispatch(
|
||||
$invoice->id,
|
||||
$invoice->company->id,
|
||||
$invoice->company->db,
|
||||
'update'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map invoice status_id and is_deleted to status string for QuickBooks push.
|
||||
*
|
||||
* @param int $statusId
|
||||
* @param bool $isDeleted
|
||||
* @return string
|
||||
*/
|
||||
private function mapInvoiceStatusToString(int $statusId, bool $isDeleted): string
|
||||
{
|
||||
if ($isDeleted) {
|
||||
return 'deleted';
|
||||
}
|
||||
|
||||
return match($statusId) {
|
||||
\App\Models\Invoice::STATUS_DRAFT => 'draft',
|
||||
\App\Models\Invoice::STATUS_SENT => 'sent',
|
||||
\App\Models\Invoice::STATUS_PAID => 'paid',
|
||||
default => 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -201,4 +201,81 @@ class QuickbooksService
|
||||
return isset($this->settings->{$entity}->direction) && ($this->settings->{$entity}->direction === $direction || $this->settings->{$entity}->direction === \App\Enum\SyncDirection::BIDIRECTIONAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch income accounts from QuickBooks.
|
||||
*
|
||||
* @return array Array of account objects with 'Id', 'Name', 'AccountType', etc.
|
||||
*/
|
||||
public function fetchIncomeAccounts(): array
|
||||
{
|
||||
try {
|
||||
if (!$this->sdk) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = "SELECT * FROM Account WHERE AccountType = 'Income' AND Active = true";
|
||||
$accounts = $this->sdk->Query($query);
|
||||
|
||||
return is_array($accounts) ? $accounts : [];
|
||||
} catch (\Exception $e) {
|
||||
nlog("Error fetching income accounts: {$e->getMessage()}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch expense accounts from QuickBooks.
|
||||
*
|
||||
* @return array Array of account objects with 'Id', 'Name', 'AccountType', etc.
|
||||
*/
|
||||
public function fetchExpenseAccounts(): array
|
||||
{
|
||||
try {
|
||||
if (!$this->sdk) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = "SELECT * FROM Account WHERE AccountType IN ('Expense', 'Cost of Goods Sold') AND Active = true";
|
||||
$accounts = $this->sdk->Query($query);
|
||||
|
||||
return is_array($accounts) ? $accounts : [];
|
||||
} catch (\Exception $e) {
|
||||
nlog("Error fetching expense accounts: {$e->getMessage()}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format accounts for UI dropdown consumption.
|
||||
*
|
||||
* @param array $accounts Raw account objects from QuickBooks API
|
||||
* @return array Formatted array with 'value' (ID) and 'label' (Name) for each account
|
||||
*/
|
||||
public function formatAccountsForDropdown(array $accounts): array
|
||||
{
|
||||
$formatted = [];
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
$id = is_object($account) && isset($account->Id)
|
||||
? (string) $account->Id
|
||||
: (is_array($account) && isset($account['Id']) ? (string) $account['Id'] : null);
|
||||
|
||||
$name = is_object($account) && isset($account->Name)
|
||||
? (string) $account->Name
|
||||
: (is_array($account) && isset($account['Name']) ? (string) $account['Name'] : '');
|
||||
|
||||
if ($id && $name) {
|
||||
$formatted[] = [
|
||||
'value' => $id,
|
||||
'label' => $name,
|
||||
'account_type' => is_object($account) && isset($account->AccountType)
|
||||
? (string) $account->AccountType
|
||||
: (is_array($account) && isset($account['AccountType']) ? (string) $account['AccountType'] : ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -224,16 +224,9 @@ class CompanyTransformer extends EntityTransformer
|
||||
'smtp_verify_peer' => (bool) $company->smtp_verify_peer,
|
||||
'e_invoice' => $company->e_invoice ?: new \stdClass(),
|
||||
'legal_entity_id' => $company->legal_entity_id ? (int) $company->legal_entity_id : null,
|
||||
'quickbooks' => $company->getRawOriginal('quickbooks') ? $company->quickbooks->toArray() : null,
|
||||
];
|
||||
|
||||
// Only include QuickBooks flags if column is not null AND has actual connection data
|
||||
// This prevents API bloat for users who don't use QuickBooks
|
||||
$quickbooksRaw = $company->getRawOriginal('quickbooks');
|
||||
if (!is_null($quickbooksRaw) && $company->quickbooks->isConfigured()) {
|
||||
$data['has_quickbooks_token'] = true;
|
||||
$data['is_quickbooks_token_active'] = (bool) $company->quickbooks->accessTokenKey;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,12 +36,12 @@ Route::post('password/reset', [ResetPasswordController::class, 'reset'])->middle
|
||||
Route::get('auth/{provider}', [LoginController::class, 'redirectToProvider']);
|
||||
|
||||
Route::middleware(['url_db'])->group(function () {
|
||||
Route::get('/user/confirm/{confirmation_code}', [UserController::class, 'confirm'])->throttle(10, 1);
|
||||
Route::post('/user/confirm/{confirmation_code}', [UserController::class, 'confirmWithPassword'])->throttle(10, 1);
|
||||
Route::get('/user/confirm/{confirmation_code}', [UserController::class, 'confirm'])->middleware('throttle:10,1');
|
||||
Route::post('/user/confirm/{confirmation_code}', [UserController::class, 'confirmWithPassword'])->middleware('throttle:10,1');
|
||||
});
|
||||
|
||||
Route::get('stripe/signup/{token}', [StripeConnectController::class, 'initialize'])->throttle(10, 1)->name('stripe_connect.initialization');
|
||||
Route::get('stripe/completed', [StripeConnectController::class, 'completed'])->throttle(10, 1)->name('stripe_connect.return');
|
||||
Route::get('stripe/signup/{token}', [StripeConnectController::class, 'initialize'])->middleware('throttle:10,1')->name('stripe_connect.initialization');
|
||||
Route::get('stripe/completed', [StripeConnectController::class, 'completed'])->middleware('throttle:10,1')->name('stripe_connect.return');
|
||||
|
||||
Route::get('yodlee/onboard/{token}', [YodleeController::class, 'auth'])->name('yodlee.auth');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user