Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions ProcessMaker/Console/Commands/EvaluateCaseRetention.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace ProcessMaker\Console\Commands;

use Illuminate\Console\Command;
use ProcessMaker\Jobs\EvaluateProcessRetentionJob;
use ProcessMaker\Models\Process;

class EvaluateCaseRetention extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cases:retention:evaluate';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Evaluate and delete cases past their retention period';

/**
* Execute the console command.
*/
public function handle()
{
// Only run if case retention policy is enabled
$enabled = config('app.case_retention_policy_enabled', false);
if (!$enabled) {
$this->info('Case retention policy is disabled');
$this->error('Skipping case retention evaluation');

return;
}

$this->info('Case retention policy is enabled');
$this->info('Evaluating and deleting cases past their retention period');

// Process all processes when retention policy is enabled
// Processes without retention_period will default to 1_year
Process::chunkById(100, function ($processes) {
foreach ($processes as $process) {
dispatch(new EvaluateProcessRetentionJob($process->id));
}
});

$this->info('Cases retention evaluation complete');
}
}
7 changes: 7 additions & 0 deletions ProcessMaker/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ protected function schedule(Schedule $schedule)
break;
}

// evaluate cases retention policy
$schedule->command('cases:retention:evaluate')
->daily()
->onOneServer()
->withoutOverlapping()
->runInBackground();

// 5 minutes is recommended in https://laravel.com/docs/12.x/horizon#metrics
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}
Expand Down
105 changes: 105 additions & 0 deletions ProcessMaker/Jobs/EvaluateProcessRetentionJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace ProcessMaker\Jobs;

use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Models\CaseNumber;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\ProcessRequest;

class EvaluateProcessRetentionJob implements ShouldQueue
{
use Queueable;

/**
* Create a new job instance.
*/
public function __construct(public int $processId)
{
}

/**
* Execute the job.
*/
public function handle(): void
{
// Only run if case retention policy is enabled
$enabled = config('app.case_retention_policy_enabled', false);
if (!$enabled) {
return;
}

$process = Process::find($this->processId);
if (!$process) {
Log::error('CaseRetentionJob: Process not found', ['process_id' => $this->processId]);

return;
}

// Default to 1_year if retention_period is not set
$retentionPeriod = $process->properties['retention_period'] ?? '1_year';
$retentionMonths = match ($retentionPeriod) {
'6_months' => 6,
'1_year' => 12,
'3_years' => 36,
'5_years' => 60,
default => 12, // Default to 1_year
};

// Default retention_updated_at to now if not set
// This means the retention policy applies from now for processes without explicit retention settings
$retentionUpdatedAt = isset($process->properties['retention_updated_at'])
? Carbon::parse($process->properties['retention_updated_at'])
: Carbon::now();

// Check if there are any process requests for this process
// If not, nothing to delete
if (!ProcessRequest::where('process_id', $this->processId)->exists()) {
return;
}

// Handle two scenarios:
// 1. Cases created BEFORE retention_updated_at: Delete if older than retention period from retention_updated_at
// (These cases were subject to the old retention policy, but we apply current retention from update date)
// 2. Cases created AFTER retention_updated_at: Delete if older than retention period from their creation date
// (These cases are subject to the new retention policy)

$now = Carbon::now();

// For cases created before retention_updated_at: cutoff is retention_updated_at - retention_period
$oldCasesCutoff = $retentionUpdatedAt->copy()->subMonths($retentionMonths);

// For cases created after retention_updated_at: cutoff is now - retention_period
$newCasesCutoff = $now->copy()->subMonths($retentionMonths);

// Use subquery to get process request IDs
$processRequestSubquery = ProcessRequest::where('process_id', $this->processId)->select('id');

CaseNumber::whereIn('process_request_id', $processRequestSubquery)
->where(function ($query) use ($retentionUpdatedAt, $oldCasesCutoff, $newCasesCutoff) {
// Cases created before retention_updated_at: delete if created before (retention_updated_at - retention_period)
$query->where(function ($q) use ($retentionUpdatedAt, $oldCasesCutoff) {
$q->where('created_at', '<', $retentionUpdatedAt)
->where('created_at', '<', $oldCasesCutoff);
})
// Cases created after retention_updated_at: delete if created before (now - retention_period)
->orWhere(function ($q) use ($retentionUpdatedAt, $newCasesCutoff) {
$q->where('created_at', '>=', $retentionUpdatedAt)
->where('created_at', '<', $newCasesCutoff);
});
})
->chunkById(100, function ($cases) {
$caseIds = $cases->pluck('id');
// Delete the cases
CaseNumber::whereIn('id', $caseIds)->delete();

// TODO: Add logs to track the number of cases deleted
// Get deleted timestamp
// $deletedAt = Carbon::now();
// RetentionPolicyLog::record($process->id, $caseIds, $deletedAt);
});
}
}
3 changes: 3 additions & 0 deletions config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@
// Enable or disable TCE customization feature
'tce_customization_enable' => env('TCE_CUSTOMIZATION_ENABLED', false),

// Enable or disable case retention policy
'case_retention_policy_enabled' => env('CASE_RETENTION_POLICY_ENABLED', false),

'prometheus_namespace' => env('PROMETHEUS_NAMESPACE', strtolower(preg_replace('/[^a-zA-Z0-9_]+/', '_', env('APP_NAME', 'processmaker')))),

'server_timing' => [
Expand Down
21 changes: 21 additions & 0 deletions database/factories/ProcessMaker/Models/CaseNumberFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Database\Factories\ProcessMaker\Models;

use Illuminate\Database\Eloquent\Factories\Factory;
use ProcessMaker\Models\CaseNumber;
use ProcessMaker\Models\ProcessRequest;

class CaseNumberFactory extends Factory
{
protected $model = CaseNumber::class;

public function definition(): array
{
return [
'process_request_id' => function () {
return ProcessRequest::factory()->create()->getKey();
},
];
}
}
Loading
Loading