Invoice upload by robot

This commit is contained in:
2026-02-14 00:04:00 +01:00
parent 4f4dff2edd
commit 2b458eccd7
15 changed files with 278 additions and 6 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Domains\Invoice\Actions\UploadInvoice;
use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptCommand;
use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptRequest;
use App\Providers\WebDavProvider;
use Illuminate\Support\Facades\Storage;
class UploadInvoiceCommand {
private UploadInvoiceRequest $request;
public function __construct(UploadInvoiceRequest $request) {
$this->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;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Domains\Invoice\Actions\UploadInvoice;
use App\Models\Invoice;
class UploadInvoiceRequest {
public Invoice $invoice;
public function __construct(Invoice $invoice) {
$this->invoice = $invoice;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Invoice\Actions\UploadInvoice;
class UploadInvoiceResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
class CronTaskType extends CommonModel{
public const CRON_TASK_TYPE_REALTIME = 'realtime';
public const CRON_TASK_TYPE_DAILY = 'daily';
}

View File

@@ -3,6 +3,7 @@
namespace App\Installer;
use App\Enumerations\CostUnitType;
use App\Enumerations\CronTaskType;
use App\Enumerations\EatingHabit;
use App\Enumerations\FirstAidPermission;
use App\Enumerations\InvoiceStatus;
@@ -13,6 +14,7 @@ use App\Models\Tenant;
class ProductionDataSeeder {
public function execute() {
$this->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]);
}
}

12
app/Models/CronTask.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use App\Scopes\CommonModel;
class CronTask extends CommonModel
{
protected $table = 'cron_tasks';
protected $fillable = ['name', 'execution_type', 'schedule_time', 'last_run'];
protected $dates = ['last_run'];
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Providers;
use App\Enumerations\CronTaskType;
use App\Models\Tenant;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Carbon\Carbon;
use App\Models\CronTask;
use Psr\Log\LoggerInterface;
class CronTaskHandleProvider extends CommonController
{
public function run(Request $request)
{
$now = Carbon::now();
$tenants = Tenant::where('has_active_instance', true)->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',
]);
}
}

View File

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

View File

@@ -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) {

8
app/Tasks/CronTask.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
namespace App\Tasks;
interface CronTask
{
public function handle(): void;
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Tasks;
use App\Domains\Invoice\Actions\UploadInvoice\UploadInvoiceCommand;
use App\Domains\Invoice\Actions\UploadInvoice\UploadInvoiceRequest;
use App\Repositories\InvoiceRepository;
use Psr\Log\LoggerInterface;
class UploadInvoices implements CronTask {
public function handle(): void
{
if (!app('tenant')->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('------------------------------------');
};
}
}

View File

@@ -126,7 +126,6 @@ return [
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cron_task_types', function (Blueprint $table) {
$table->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');
}
};

View File

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