From 2b458eccd7e48c49f3a07a2631907fb60ea3b606 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20G=C3=BCnther?=
Date: Sat, 14 Feb 2026 00:04:00 +0100
Subject: [PATCH] Invoice upload by robot
---
.../ChangeStatus/ChangeStatusCommand.php | 2 +-
.../UploadInvoice/UploadInvoiceCommand.php | 50 +++++++++++
.../UploadInvoice/UploadInvoiceRequest.php | 13 +++
.../UploadInvoice/UploadInvoiceResponse.php | 11 +++
app/Enumerations/CronTaskType.php | 11 +++
app/Installer/ProductionDataSeeder.php | 10 ++-
app/Models/CronTask.php | 12 +++
app/Providers/CronTaskHandleProvider.php | 88 +++++++++++++++++++
app/Providers/WebDavProvider.php | 4 +-
app/Repositories/InvoiceRepository.php | 4 +
app/Tasks/CronTask.php | 8 ++
app/Tasks/UploadInvoices.php | 31 +++++++
config/logging.php | 1 -
.../2026_02_01_140010_create_cron_tasks.php | 35 ++++++++
routes/web.php | 4 +-
15 files changed, 278 insertions(+), 6 deletions(-)
create mode 100644 app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceCommand.php
create mode 100644 app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceRequest.php
create mode 100644 app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceResponse.php
create mode 100644 app/Enumerations/CronTaskType.php
create mode 100644 app/Models/CronTask.php
create mode 100644 app/Providers/CronTaskHandleProvider.php
create mode 100644 app/Tasks/CronTask.php
create mode 100644 app/Tasks/UploadInvoices.php
create mode 100644 database/migrations/2026_02_01_140010_create_cron_tasks.php
diff --git a/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusCommand.php b/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusCommand.php
index e6326d0..01848b0 100644
--- a/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusCommand.php
+++ b/app/Domains/Invoice/Actions/ChangeStatus/ChangeStatusCommand.php
@@ -40,7 +40,7 @@ class ChangeStatusCommand {
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_DELETED;
break;
case InvoiceStatus::INVOICE_STATUS_EXPORTED:
- //$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_EXPORTED;
+ $this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_EXPORTED;
$this->request->invoice->upload_required = true;
break;
}
diff --git a/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceCommand.php b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceCommand.php
new file mode 100644
index 0000000..044e57a
--- /dev/null
+++ b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceCommand.php
@@ -0,0 +1,50 @@
+request = $request;
+ }
+
+ public function execute() : UploadInvoiceResponse {
+ $uploadResponse = new UploadInvoiceResponse();
+
+ $uploadDir = sprintf(
+ '%1$s%2$s/%3$s',
+ WebDavProvider::INVOICE_PREFIX,
+ app('tenant')->url,
+ $this->request->invoice->costUnit()->first()->name
+ );
+
+ $webDavProvider = new WebDavProvider($uploadDir);
+
+ $createInvoiceReceiptRequest = new CreateInvoiceReceiptRequest($this->request->invoice);
+ $createInvoiceReceiptCommand = new CreateInvoiceReceiptCommand($createInvoiceReceiptRequest);
+ $response = $createInvoiceReceiptCommand->execute();
+ if ('' === $response->fileName) {
+ app('taskLogger')->error('PDF oder ZIP zur Abrechnung konnte nicht erstellt werden.');
+ return $uploadResponse;
+ }
+
+ if ($webDavProvider->uploadFile($response->fileName)) {
+ $this->request->invoice->upload_required = false;
+ $this->request->invoice->save();
+ $uploadResponse->success = true;
+ } else {
+ app('taskLogger')->error('PDF oder ZIP zur Abrechnung konnte nicht hochgeladen werden.');
+ }
+
+ if (Storage::exists($response->fileName)) {
+ Storage::delete($response->fileName);
+ }
+
+ return $uploadResponse;
+ }
+}
diff --git a/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceRequest.php b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceRequest.php
new file mode 100644
index 0000000..5a0de48
--- /dev/null
+++ b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceRequest.php
@@ -0,0 +1,13 @@
+invoice = $invoice;
+ }
+}
diff --git a/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceResponse.php b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceResponse.php
new file mode 100644
index 0000000..a32c685
--- /dev/null
+++ b/app/Domains/Invoice/Actions/UploadInvoice/UploadInvoiceResponse.php
@@ -0,0 +1,11 @@
+success = false;
+ }
+}
diff --git a/app/Enumerations/CronTaskType.php b/app/Enumerations/CronTaskType.php
new file mode 100644
index 0000000..0c7608a
--- /dev/null
+++ b/app/Enumerations/CronTaskType.php
@@ -0,0 +1,11 @@
+installCronTypes();
$this->installUserRoles();
$this->installCostUnitTypes();
$this->installSwimmingPermissions();
@@ -21,6 +23,7 @@ class ProductionDataSeeder {
$this->installTenants();
$this->installInvoiceMetaData();
+
}
private function installUserRoles() {
@@ -105,6 +108,11 @@ class ProductionDataSeeder {
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_APPROVED]);
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_EXPORTED]);
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_DENIED]);
- InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STAUTS_DELETED]);
+ InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_DELETED]);
+ }
+
+ private function installCronTypes() {
+ CronTaskType::creata(['slug' => CronTaskType::CRON_TASK_TYPE_REALTIME]);
+ CronTaskType::creata(['slug' => CronTaskType::CRON_TASK_TYPE_DAILY]);
}
}
diff --git a/app/Models/CronTask.php b/app/Models/CronTask.php
new file mode 100644
index 0000000..01930ca
--- /dev/null
+++ b/app/Models/CronTask.php
@@ -0,0 +1,12 @@
+get();
+
+ foreach ($tenants as $tenant) {
+ app()->instance('tenant', $tenant);
+ $this->runTenantTasks($tenant, $now);
+ }
+
+ return response()->json([
+ 'status' => 'ok',
+ 'time' => $now->toDateTimeString(),
+ ]);
+ }
+
+ private function runTenantTasks(Tenant $tenant, Carbon $now) {
+ $tasks = CronTask::all();
+
+ foreach ($tasks as $task) {
+
+ // --- Every-Time Tasks ---
+ if ($task->execution_type === CronTaskType::CRON_TASK_TYPE_REALTIME) {
+ $this->runTask($task);
+ }
+
+ // --- Daily Tasks ---
+ if ($task->execution_type === CronTaskType::CRON_TASK_TYPE_DAILY) {
+ $scheduledTime = $task->schedule_time;
+ $alreadyRunToday = $task->last_run?->isToday() ?? false;
+
+ if (!$alreadyRunToday && $now->format('H:i') === $scheduledTime) {
+ $this->runTask($task);
+ }
+ }
+ }
+ }
+
+ private function runTask(CronTask $task)
+ {
+ $logger = $this->taskLogger($task->name, app('tenant'));
+ app()->instance('taskLogger', $logger);
+
+
+ $taskClass = "\\App\\Tasks\\" . $task->name;
+ if (class_exists($taskClass)) {
+ $instance = new $taskClass();
+ $instance->handle();
+
+ // Update last_run
+ $task->last_run = now();
+ $task->save();
+ }
+ }
+
+ private function taskLogger(string $taskName, $tenant = null) : LoggerInterface
+ {
+ $tenantSlug = $tenant->slug;
+ $logDir = storage_path("logs/{$tenantSlug}");
+
+ if (!file_exists($logDir)) {
+ mkdir($logDir, 0755, true);
+ }
+
+ $logPath = "{$logDir}/{$taskName}.log";
+
+ return \Illuminate\Support\Facades\Log::build([
+ 'driver' => 'single',
+ 'path' => $logPath,
+ 'level' => 'debug',
+ ]);
+ }
+}
diff --git a/app/Providers/WebDavProvider.php b/app/Providers/WebDavProvider.php
index 7c76d2e..a8c798d 100644
--- a/app/Providers/WebDavProvider.php
+++ b/app/Providers/WebDavProvider.php
@@ -13,10 +13,10 @@ class WebDavProvider {
$this->workingDirectory = $workingDirectory;
}
- public function uploadFile(string $fileName) {
+ public function uploadFile(string $fileName) : bool {
$baseDir = storage_path('app/private/');
- $this->webDavClient->upload_file($baseDir . $fileName, $this->workingDirectory . '/'.
+ return $this->webDavClient->upload_file($baseDir . $fileName, $this->workingDirectory . '/'.
basename($fileName)
diff --git a/app/Repositories/InvoiceRepository.php b/app/Repositories/InvoiceRepository.php
index 9738cef..b481bdf 100644
--- a/app/Repositories/InvoiceRepository.php
+++ b/app/Repositories/InvoiceRepository.php
@@ -39,6 +39,10 @@ class InvoiceRepository {
return $invoices;
}
+ public function getUnexportedInvoices() : Collection {
+ return Invoice::where(['tenant' => app('tenant')->slug, 'status' => InvoiceStatus::INVOICE_STATUS_EXPORTED, 'upload_required' => true])->get();
+ }
+
public function getByStatus(CostUnit $costUnit, string $status, bool $forDisplay = true) : array {
$returnData = [];
foreach ($costUnit->invoices()->where('status', $status)->get() as $invoice) {
diff --git a/app/Tasks/CronTask.php b/app/Tasks/CronTask.php
new file mode 100644
index 0000000..4f0fac5
--- /dev/null
+++ b/app/Tasks/CronTask.php
@@ -0,0 +1,8 @@
+upload_exports) {
+ return;
+ }
+
+ $invoiceRepository = new InvoiceRepository();
+ foreach ($invoiceRepository->getUnexportedInvoices() as $invoice) {
+ app('taskLogger')->info("Uploading invoice {$invoice->invoice_number}");
+ $request = new UploadInvoiceRequest($invoice);
+ $command = new UploadInvoiceCommand($request);
+ if ($command->execute()->success) {
+ app('taskLogger')->info('Upload successful');
+ } else {
+ app('taskLogger')->error('Upload failed');
+ }
+
+ app('taskLogger')->info('------------------------------------');
+ };
+ }
+}
diff --git a/config/logging.php b/config/logging.php
index 9e998a4..86fbaac 100644
--- a/config/logging.php
+++ b/config/logging.php
@@ -126,7 +126,6 @@ return [
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
-
],
];
diff --git a/database/migrations/2026_02_01_140010_create_cron_tasks.php b/database/migrations/2026_02_01_140010_create_cron_tasks.php
new file mode 100644
index 0000000..ce76fc1
--- /dev/null
+++ b/database/migrations/2026_02_01_140010_create_cron_tasks.php
@@ -0,0 +1,35 @@
+string('slug')->primary();
+ });
+
+
+ Schema::create('cron_tasks', function (Blueprint $table) {
+ $table->id();
+ $table->string('name')->unique();
+ $table->string('execution_type');
+ $table->time('schedule_time')->nullable();
+ $table->timestamp('last_run')->nullable();
+ $table->timestamps();
+
+ $table->foreign('execution_type')->references('slug')->on('cron_task_types')->cascadeOnDelete()->cascadeOnUpdate();
+
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('cron_tasks');
+ Schema::dropIfExists('cron_task_types');
+ }
+};
diff --git a/routes/web.php b/routes/web.php
index 6d12720..ce678fd 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -3,6 +3,8 @@
use App\Domains\Dashboard\Controllers\DashboardController;
use App\Http\Controllers\TestRenderInertiaProvider;
use App\Middleware\IdentifyTenant;
+use App\Providers\CronController;
+use App\Providers\CronTaskHandleProvider;
use App\Providers\GlobalDataProvider;
use Illuminate\Support\Facades\Route;
@@ -17,7 +19,7 @@ require_once __DIR__ . '/../app/Domains/Invoice/Routes/api.php';
-
+Route::get('/execute-crons', [CronTaskHandleProvider::class, 'run']);
Route::middleware(IdentifyTenant::class)->group(function () {
Route::get('/', DashboardController::class);