Fixes for negative invoice proxying for credits / peppol

This commit is contained in:
David Bomba
2026-02-12 11:37:32 +11:00
parent 2526eef90c
commit 212f8c04f9
8 changed files with 38 additions and 30 deletions

View File

@@ -67,7 +67,7 @@ class BatchPushToQuickbooks implements ShouldQueue
public int $company_id
) {
// Set queue to dedicated QB queue
$this->onQueue('quickbooks');
// $this->onQueue('quickbooks');
}
/**

View File

@@ -46,7 +46,7 @@ class FlushQuickbooksBatch implements ShouldQueue
private int $companyId,
private string $priority = QuickbooksBatchCollector::PRIORITY_NORMAL
) {
$this->onQueue('quickbooks');
// $this->onQueue('quickbooks');
}
/**

View File

@@ -1081,7 +1081,7 @@ class Company extends BaseModel
// 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')) || !$this->company->account->isPaid()) {
if (is_null($this->getRawOriginal('quickbooks')) || !$this->account->isPaid()) {
return false;
}

View File

@@ -299,7 +299,7 @@ class BaseRepository
$model = $model->calc()->getInvoice();
/* Check if the model has been changed in a way that is relevant to Quickbooks */
$qb_model_changes = $model->wasChanged(['amount', 'line_items', 'total_taxes']);
$qb_model_changes = $model->wasChanged(['amount', 'line_items', 'total_taxes', 'status_id']);
/* We use this to compare to our starting amount */
$state['finished_amount'] = $model->balance;
@@ -341,8 +341,10 @@ class BaseRepository
if ($qb_model_changes && $model->company->quickbooks && $model->company->shouldPushToQuickbooks('invoice')) {
nlog("base repo changes detected");
if($model->company->quickbooks->settings->automatic_taxes){
nlog("immediate sync");
try{
(new \App\Jobs\Quickbooks\PushToQuickbooks('invoice', $model->id, $model->company->db))->handle();
}
@@ -351,8 +353,8 @@ class BaseRepository
}
}
elseif($model->status_id != Invoice::STATUS_DRAFT){
\App\Services\Quickbooks\QuickbooksBatchCollector::collect('invoice', $model->id, $model->company->db, $model->company->id);
nlog("batch sync");
\App\Services\Quickbooks\QuickbooksBatchCollector::collect('invoice', $model->id, $model->company->db, $model->company_id);
// \App\Jobs\Quickbooks\PushToQuickbooks::dispatch(
// 'invoice',

View File

@@ -126,7 +126,11 @@ class StorecoveAdapter
$e = new \InvoiceNinja\EInvoice\EInvoice();
$peppolInvoice = $e->decode('Peppol', $p, 'xml');
$parent = $invoice instanceof \App\Models\Credit ? \App\Services\EDocument\Gateway\Storecove\Models\Credit::class : \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
// $parent = $invoice instanceof \App\Models\Credit ? \App\Services\EDocument\Gateway\Storecove\Models\Credit::class : \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
$parent = ($invoice instanceof \App\Models\Credit || $peppolInvoice instanceof \InvoiceNinja\EInvoice\Models\Peppol\CreditNote)
? \App\Services\EDocument\Gateway\Storecove\Models\Credit::class
: \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
$peppolInvoice = $e->encode($peppolInvoice, 'json');
$this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'json', $context);

View File

@@ -59,8 +59,14 @@ class SendEDocument implements ShouldQueue
nlog("trying");
$model = $this->entity::withTrashed()->firstOrFail($this->id);
$model = $this->entity::withTrashed()->find($this->id);
if(!$model){
nlog("model not found");
return;
}
if (isset($model->backup->guid) && is_string($model->backup->guid) && strlen($model->backup->guid) > 3) {
nlog("already sent!");
return;

View File

@@ -29,17 +29,17 @@ use App\Jobs\Quickbooks\BatchPushToQuickbooks;
class QuickbooksBatchCollector
{
/**
* Batch size thresholds
* Dispatch immediately if batch reaches this size (don't wait for timer)
*/
private const MAX_BATCH_SIZE = 50;
private const MIN_BATCH_SIZE = 5;
/**
* Time windows for collecting entities (seconds) by priority
* Time windows for collecting entities (seconds) by priority.
* After this window a FlushQuickbooksBatch job fires and pushes
* whatever has accumulated — even a single entity.
*/
private const COLLECTION_WINDOW_IMMEDIATE = 2; // High priority: 2 seconds max
private const COLLECTION_WINDOW_NORMAL = 10; // Normal priority: 10 seconds max
private const COLLECTION_WINDOW_LOW = 30; // Low priority: 30 seconds max
private const COLLECTION_WINDOW_NORMAL = 10;
private const COLLECTION_WINDOW_LOW = 30;
/**
* Priority levels
@@ -76,6 +76,8 @@ class QuickbooksBatchCollector
string $priority = self::PRIORITY_NORMAL,
bool $forceImmediate = false
): void {
nlog("QB Batch: Collecting {$entityType} {$entityId} for company {$companyId} with priority {$priority}");
// Force immediate dispatch for high-priority operations
if ($forceImmediate || $priority === self::PRIORITY_IMMEDIATE) {
nlog("QB Batch: Immediate dispatch for {$entityType} {$entityId}");
@@ -95,21 +97,18 @@ class QuickbooksBatchCollector
'priority' => $priority,
];
// Get collection window based on priority
$collectionWindow = self::getCollectionWindow($priority);
// Store batch with expiry
Cache::put($key, $batch, now()->addSeconds($collectionWindow));
// Store batch — TTL must outlive the flush job delay so data is
// still in cache when FlushQuickbooksBatch runs.
Cache::put($key, $batch, now()->addSeconds($collectionWindow + 30));
// Check if we should dispatch now
if (count($batch) >= self::MAX_BATCH_SIZE) {
// Batch is full, dispatch right now
self::dispatchBatch($entityType, $db, $companyId, $priority);
} elseif (count($batch) >= self::MIN_BATCH_SIZE) {
// Schedule dispatch if not already scheduled
} else {
// Schedule a delayed flush (idempotent — won't double-schedule)
self::scheduleDispatch($entityType, $db, $companyId, $priority, $collectionWindow);
} elseif ($priority === self::PRIORITY_IMMEDIATE && count($batch) >= 1) {
// For immediate priority, dispatch even small batches quickly
self::scheduleDispatch($entityType, $db, $companyId, $priority, 2);
}
}
@@ -186,7 +185,6 @@ class QuickbooksBatchCollector
private static function getCollectionWindow(string $priority): int
{
return match ($priority) {
self::PRIORITY_IMMEDIATE => self::COLLECTION_WINDOW_IMMEDIATE,
self::PRIORITY_LOW => self::COLLECTION_WINDOW_LOW,
default => self::COLLECTION_WINDOW_NORMAL,
};

View File

@@ -61,15 +61,13 @@ class InvoiceTransformer extends BaseTransformer
// Get product's QuickBooks ID (business logic handled by QbProduct)
$product_qb_id = $qb_service->product->findOrCreateProduct($line_item);
$tax_code_id = 'NON'; // Default to non-taxable
$tax_code_id = 'NON';
// Check if tax_id is set and is exempt/zero rate (5 = exempt, 8 = zero rate)
if (isset($line_item->tax_id) && in_array($line_item->tax_id, ['5', '8'])) {
$tax_code_id = 'NON';
} elseif ($ast) { // Automatic taxes are enabled
} elseif ($ast) {
$tax_code_id = 'TAX';
} elseif (isset($line_item->tax_id) && !in_array($line_item->tax_id, ['5', '8'])) {
// Only use 'TAX' if there are actual tax rates applied to this line item
} else {
$has_tax_rate = (
(isset($line_item->tax_rate1) && $line_item->tax_rate1 > 0)
|| (isset($line_item->tax_rate2) && $line_item->tax_rate2 > 0)