Wiring up verifactu sending

This commit is contained in:
David Bomba 2025-08-13 14:26:03 +10:00
parent 3d3b5f6938
commit c5c9c4325e
7 changed files with 118 additions and 69 deletions

View File

@ -39,13 +39,13 @@ class VerifactuAmountCheck implements ValidationRule
$company = $user->company(); $company = $user->company();
if ($company->verifactuEnabled()) { if ($company->verifactuEnabled()) { // Company level check if Verifactu is enabled
$client = Client::withTrashed()->find($this->input['client_id']); $client = Client::withTrashed()->find($this->input['client_id']);
if($client->country->iso_3166_2 !== 'ES') { if($client->country->iso_3166_2 !== 'ES') { // Client level check if client is in Spain
return; return;
} }
$invoice = false; $invoice = false;
$child_invoices = false; $child_invoices = false;

View File

@ -278,6 +278,14 @@ class Activity extends StaticModel
public const E_EXPENSE_CREATED = 148; public const E_EXPENSE_CREATED = 148;
public const EMAIL_CREDIT = 149; public const EMAIL_CREDIT = 149;
public const VERIFACTU_INVOICE_SENT = 150;
public const VERIFACTU_INVOICE_SENT_FAILURE = 151;
public const VERIFACTU_CANCELLATION_SENT = 152;
public const VERIFACTU_CANCELLATION_SENT_FAILURE = 153;
protected $casts = [ protected $casts = [
'is_system' => 'boolean', 'is_system' => 'boolean',

View File

@ -78,6 +78,8 @@ class SystemLog extends Model
public const CATEGORY_LOG = 6; public const CATEGORY_LOG = 6;
public const CATEGORY_VERIFACTU = 7;
/* Event IDs*/ /* Event IDs*/
public const EVENT_PAYMENT_RECONCILIATION_FAILURE = 10; public const EVENT_PAYMENT_RECONCILIATION_FAILURE = 10;
@ -115,6 +117,10 @@ class SystemLog extends Model
public const EVENT_INBOUND_MAIL_BLOCKED = 62; public const EVENT_INBOUND_MAIL_BLOCKED = 62;
public const EVENT_VERIFACTU_FAILURE = 70;
public const EVENT_VERIFACTU_SUCCESS = 71;
/*Type IDs*/ /*Type IDs*/
public const TYPE_PAYPAL = 300; public const TYPE_PAYPAL = 300;
@ -180,6 +186,12 @@ class SystemLog extends Model
public const TYPE_GENERIC = 900; public const TYPE_GENERIC = 900;
public const TYPE_VERIFACTU_CANCELLATION = 1000;
public const TYPE_VERIFACTU_INVOICE = 1001;
public const TYPE_VERIFACTU_RECTIFICATION = 1002;
protected $fillable = [ protected $fillable = [
'client_id', 'client_id',
'company_id', 'company_id',

View File

@ -15,12 +15,16 @@ use Mail;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Models\Company; use App\Models\Company;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Activity;
use App\Models\SystemLog;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Jobs\Util\SystemLogger;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Mail\Mailables\Address; use Illuminate\Mail\Mailables\Address;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use App\Repositories\ActivityRepository;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -63,16 +67,13 @@ class SendToAeat implements ShouldQueue
return [5, 30, 240, 3600, 7200]; return [5, 30, 240, 3600, 7200];
} }
public function handle() public function handle(ActivityRepository $activity_repository)
{ {
MultiDB::setDB($this->company->db); MultiDB::setDB($this->company->db);
$invoice = Invoice::withTrashed()->find($this->invoice_id); $invoice = Invoice::withTrashed()->find($this->invoice_id);
switch($this->action) { switch($this->action) {
case 'modify':
$this->modifyInvoice($invoice);
break;
case 'create': case 'create':
$this->createInvoice($invoice); $this->createInvoice($invoice);
break; break;
@ -92,27 +93,7 @@ class SendToAeat implements ShouldQueue
* @param Invoice $invoice * @param Invoice $invoice
* @return void * @return void
*/ */
public function modifyInvoice(Invoice $invoice)
{
$verifactu = new Verifactu($invoice);
$verifactu->run();
$envelope = $verifactu->getEnvelope();
$response = $verifactu->send($envelope);
nlog($response);
// if($invoice->amount >= 0) {
// $document = (new RegistroAlta($invoice))->run()->getInvoice();
// }
// else {
// $document = (new RegistroRectificacion($invoice))->run()->getInvoice();
// }
}
public function createInvoice(Invoice $invoice) public function createInvoice(Invoice $invoice)
{ {
$verifactu = new Verifactu($invoice); $verifactu = new Verifactu($invoice);
@ -124,6 +105,9 @@ class SendToAeat implements ShouldQueue
nlog($response); nlog($response);
$this->writeActivity($invoice, $response['success'] ? Activity::VERIFACTU_INVOICE_SENT : Activity::VERIFACTU_INVOICE_SENT_FAILURE, $response['message']);
$this->systemLog($invoice, $response, $response['success'] ? SystemLog::EVENT_VERIFACTU_SUCCESS : SystemLog::EVENT_VERIFACTU_FAILURE, SystemLog::TYPE_VERIFACTU_INVOICE);
} }
public function cancelInvoice(Invoice $invoice) public function cancelInvoice(Invoice $invoice)
@ -153,12 +137,15 @@ class SendToAeat implements ShouldQueue
$parent->saveQuietly(); $parent->saveQuietly();
} }
} }
//@todo - verifactu logging //@todo - verifactu logging
$this->writeActivity($invoice, $response['success'] ? Activity::VERIFACTU_CANCELLATION_SENT : Activity::VERIFACTU_CANCELLATION_SENT_FAILURE, $response['message']);
$this->systemLog($invoice, $response, $response['success'] ? SystemLog::EVENT_VERIFACTU_SUCCESS : SystemLog::EVENT_VERIFACTU_FAILURE, SystemLog::TYPE_VERIFACTU_CANCELLATION);
} }
public function middleware() public function middleware()
{ {
return [new WithoutOverlapping("send_to_aeat_{$this->company->company_key}")]; return [(new WithoutOverlapping("send_to_aeat_{$this->company->company_key}"))->releaseAfter(30)->expireAfter(30)];
} }
public function failed($exception = null) public function failed($exception = null)
@ -166,7 +153,34 @@ class SendToAeat implements ShouldQueue
nlog($exception); nlog($exception);
} }
private function writeActivity(Invoice $invoice, int $activity_id, string $notes = ''): void
{
$activity = new Activity();
$activity->user_id = $invoice->user_id;
$activity->client_id = $invoice->client_id;
$activity->company_id = $invoice->company_id;
$activity->account_id = $invoice->company->account_id;
$activity->activity_type_id = $activity_id;
$activity->invoice_id = $invoice->id;
$activity->notes = str_replace('"', '', $notes);
$activity->is_system = true;
$activity->save();
}
private function systemLog(Invoice $invoice, array $data, int $event_id, int $type_id): void
{
(new SystemLogger(
$data,
SystemLog::CATEGORY_VERIFACTU,
$event_id,
$type_id,
$invoice->client,
$invoice->company
)
)->handle();
}
/** /**
* cancellationHash * cancellationHash

View File

@ -76,39 +76,56 @@ class HandleCancellation extends AbstractService
private function verifactuCancellation(): Invoice private function verifactuCancellation(): Invoice
{ {
$replicated_invoice = $this->invoice->replicate();
$this->invoice = $this->invoice->service()->setStatus(Invoice::STATUS_CANCELLED)->save(); $this->invoice = $this->invoice->service()->setStatus(Invoice::STATUS_CANCELLED)->save();
$this->invoice->service()->workFlow()->save(); $this->invoice->service()->workFlow()->save();
$replicated_invoice->status_id = Invoice::STATUS_DRAFT; // R2 Cancellation - do not create a separate document
$replicated_invoice->date = now()->format('Y-m-d'); if($this->invoice->backup->document_type === 'R2'){
$replicated_invoice->due_date = null;
$replicated_invoice->partial = 0;
$replicated_invoice->partial_due_date = null;
$replicated_invoice->number = null;
$replicated_invoice->amount = 0;
$replicated_invoice->balance = 0;
$replicated_invoice->paid_to_date = 0;
$items = $replicated_invoice->line_items; $parent = Invoice::withTrashed()->find($this->decodePrimaryKey($this->invoice->backup->parent_invoice_id));
foreach($items as &$item) { if(!$parent) {
$item->quantity = $item->quantity * -1; return $this->invoice;
}
$parent->backup->adjustable_amount -= $this->invoice->amount;
$parent->backup->child_invoice_ids->reject(fn($id) => $id === $this->invoice->hashed_id);
$parent->save();
$this->invoice->service()->cancelVerifactu();
}
else {
$replicated_invoice = $this->invoice->replicate();
$replicated_invoice->status_id = Invoice::STATUS_DRAFT;
$replicated_invoice->date = now()->format('Y-m-d');
$replicated_invoice->due_date = null;
$replicated_invoice->partial = 0;
$replicated_invoice->partial_due_date = null;
$replicated_invoice->number = null;
$replicated_invoice->amount = 0;
$replicated_invoice->balance = 0;
$replicated_invoice->paid_to_date = 0;
$items = $replicated_invoice->line_items;
foreach($items as &$item) {
$item->quantity = $item->quantity * -1;
}
$replicated_invoice->line_items = $items;
$replicated_invoice->backup->parent_invoice_id = $this->invoice->hashed_id;
$replicated_invoice->backup->parent_invoice_number = $this->invoice->number;
$replicated_invoice->backup->document_type = 'R2'; // Full Credit Note Generated for the invoice
$invoice_repository = new InvoiceRepository();
$replicated_invoice = $invoice_repository->save([], $replicated_invoice);
$replicated_invoice->service()->markSent()->sendVerifactu()->save();
$this->invoice->backup->child_invoice_ids->push($replicated_invoice->hashed_id);
$this->invoice->saveQuietly();
} }
$replicated_invoice->line_items = $items;
$replicated_invoice->backup->parent_invoice_id = $this->invoice->hashed_id;
$replicated_invoice->backup->parent_invoice_number = $this->invoice->number;
$replicated_invoice->backup->document_type = 'R2'; // Full Credit Note Generated for the invoice
$invoice_repository = new InvoiceRepository();
$replicated_invoice = $invoice_repository->save([], $replicated_invoice);
$replicated_invoice->service()->markSent()->sendVerifactu()->save();
$this->invoice->backup->child_invoice_ids->push($replicated_invoice->hashed_id);
$this->invoice->saveQuietly();
$this->invoice->fresh(); $this->invoice->fresh();
event(new InvoiceWasCancelled($this->invoice, $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new InvoiceWasCancelled($this->invoice, $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));

View File

@ -27,6 +27,7 @@ use Illuminate\Support\Facades\Storage;
use App\Events\Invoice\InvoiceWasArchived; use App\Events\Invoice\InvoiceWasArchived;
use App\Jobs\Inventory\AdjustProductInventory; use App\Jobs\Inventory\AdjustProductInventory;
use App\Libraries\Currency\Conversion\CurrencyApi; use App\Libraries\Currency\Conversion\CurrencyApi;
use App\Services\EDocument\Standards\Verifactu\SendToAeat;
class InvoiceService class InvoiceService
{ {
@ -248,7 +249,7 @@ class InvoiceService
$this->invoice = (new MarkInvoiceDeleted($this->invoice))->run(); $this->invoice = (new MarkInvoiceDeleted($this->invoice))->run();
if($this->invoice->company->verifactuEnabled()) { if($this->invoice->company->verifactuEnabled()) {
$this->deleteVerifactu(); $this->cancelVerifactu();
} }
return $this; return $this;
@ -680,31 +681,24 @@ class InvoiceService
/** /**
* sendVerifactu * sendVerifactu
* @todo - send the invoice to AEAT
* ONLY send when the transaction is ES => ES
* Ensure we run all sending syncronously to ensure chronology
* *
* @return self * @return self
*/ */
public function sendVerifactu(): self public function sendVerifactu(): self
{ {
// if($this->invoice->company->verifactuEnabled()) { SendToAeat::dispatch($this->invoice->id, $this->invoice->company, 'create');
// (new SendVerifactu($this->invoice))->handle();
// }
return $this; return $this;
} }
/** /**
* deleteVerifactu * cancelVerifactu
* @todo - handle "cancelling" the invoice in AEAT *
* @return self * @return self
*/ */
public function deleteVerifactu(): self public function cancelVerifactu(): self
{ {
// if($this->invoice->company->verifactuEnabled()) { SendToAeat::dispatch($this->invoice->id, $this->invoice->company, 'cancel');
// (new DeleteVerifactu($this->invoice))->handle();
// }
return $this; return $this;
} }

View File

@ -5576,6 +5576,10 @@ $lang = array(
'restore_disabled_verifactu' => 'You cannot restore an invoice once it has been deleted', 'restore_disabled_verifactu' => 'You cannot restore an invoice once it has been deleted',
'delete_disabled_verifactu' => 'You cannot delete an invoice once it has been cancelled or modified', 'delete_disabled_verifactu' => 'You cannot delete an invoice once it has been cancelled or modified',
'rectify' => 'Rectificar', 'rectify' => 'Rectificar',
'verifactu_invoice_send_success' => 'Invoice :invoice for :client sent to AEAT successfully',
'verifactu_invoice_sent_failure' => 'Invoice :invoice for :client failed to send to AEAT :notes',
'verifactu_cancellation_send_success' => 'Invoice cancellation for :invoice sent to AEAT successfully',
'verifactu_cancellation_send_failure' => 'Invoice cancellation for :invoice failed to send to AEAT :notes',
); );
return $lang; return $lang;