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);