Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6e2df1d
Add checkmark icon
mcraeteisha Feb 3, 2026
74a91e7
Add Cases Retention to Process Configurations
mcraeteisha Feb 3, 2026
5c9a8c3
Add case_retention_policy_enabled config
mcraeteisha Feb 3, 2026
bd0b9ca
Add warning icon
mcraeteisha Feb 4, 2026
816505b
Configure new kernal command for evaluating retention
sanjacornelius Feb 5, 2026
4a87002
Implement new job to run and delete cases
sanjacornelius Feb 5, 2026
5875f22
Implement EvaludateCasesRetention command
sanjacornelius Feb 5, 2026
1ee9291
Add modal on tier/period change
mcraeteisha Feb 5, 2026
2755edb
Merge pull request #8717 from ProcessMaker/task/FOUR-29105
sanjacornelius Feb 5, 2026
98ab8d8
Implement unit tests
sanjacornelius Feb 6, 2026
9db1e9a
Create caseNumber factory
sanjacornelius Feb 6, 2026
b1e3c39
Handle retention policy update deletions
sanjacornelius Feb 6, 2026
082163c
Remove todo
sanjacornelius Feb 6, 2026
eca7b57
Disable job if feature flag is not enabled
sanjacornelius Feb 6, 2026
f3d2573
Default to 6_month retention period for processes that do not have re…
sanjacornelius Feb 6, 2026
31d1720
Add warning and success modal
mcraeteisha Feb 6, 2026
dbc2536
set default retention period to 1 year
sanjacornelius Feb 10, 2026
958cbe5
remove unused import
sanjacornelius Feb 10, 2026
fb58fc3
Disable multiselect options by tier
mcraeteisha Feb 10, 2026
fdad4af
Add retention tiers to config
mcraeteisha Feb 10, 2026
dca5de8
Update test default retention period
sanjacornelius Feb 10, 2026
23c5f1a
Update EvaluateProcessRetentionJob.php
sanjacornelius Feb 10, 2026
09df9d8
Check if case retention policy is enabled before running job
sanjacornelius Feb 10, 2026
da5863a
fix truthy statement
sanjacornelius Feb 10, 2026
a24399f
fix issue with cached config
sanjacornelius Feb 10, 2026
dfa1502
Resolve failing tests: Cases not being deleted due to improper retent…
sanjacornelius Feb 11, 2026
888d7a9
CusorBot Fix: use subquery instead of loading all IDs into memory
sanjacornelius Feb 11, 2026
ebe3c70
Set default retention period
mcraeteisha Feb 12, 2026
72daa9a
Merge pull request #8721 from ProcessMaker/task/FOUR-29110
sanjacornelius Feb 12, 2026
3c269cc
Resolve modal showing 'confirm' screen on close
mcraeteisha Feb 12, 2026
a95f47f
Update README Documentation; add Case Retention Info
mcraeteisha Feb 12, 2026
9435866
Merge branch 'develop' into epic/FOUR-29101
sanjacornelius Feb 13, 2026
204d1a3
Merge branch 'epic/FOUR-29101' into task/FOUR-29107
mcraeteisha Feb 13, 2026
2e61155
Merge pull request #8727 from ProcessMaker/task/FOUR-29107
sanjacornelius Feb 17, 2026
2573e60
Merge pull request #8735 from ProcessMaker/task/FOUR-29106
sanjacornelius Feb 17, 2026
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 @@ -90,6 +90,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);
});
}
}
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,26 @@ How to use icon: <i class="fp-my-jonas-custom-icon" />
npm run dev-font
```

# Case Retention Tier (CASE_RETENTION_TIER)

The case retention policy controls how long cases are stored before they are automatically and permanently deleted. The **CASE_RETENTION_TIER** environment variable determines which retention periods customers can select when configuring a process. Each tier exposes a different set of options in the UI; options for higher tiers are visible but disabled so users see what is available at higher tiers.

### Supported tiers

| Tier | Retention options available |
|------|----------------------------|
| **1** | Six months, One year |
| **2** | Six months, One year, Three years |
| **3** | Six months, One year, Three years, Five years |

Set the variable in your `.env` file:
```env
CASE_RETENTION_POLICY_ENABLED=true
CASE_RETENTION_TIER=1
```
Use `1`, `2`, or `3`. The default is `1` if not set. The default retention period shown in the UI for Tier 1 is one year.



# Prometheus and Grafana

Expand Down
15 changes: 15 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 All @@ -302,6 +305,18 @@
'multitenancy' => env('MULTITENANCY', false),

'reassign_restrict_to_assignable_users' => env('REASSIGN_RESTRICT_TO_ASSIGNABLE_USERS', true),

// When true, shows the Cases Retention section on process configuration
'case_retention_policy_enabled' => filter_var(env('CASE_RETENTION_POLICY_ENABLED', false), FILTER_VALIDATE_BOOLEAN),

// Controls which retention periods are available in the UI for the current tier.
'case_retention_tier' => env('CASE_RETENTION_TIER', '1'),
'case_retention_tier_options' => [
'1' => ['six_months', 'one_year'],
'2' => ['six_months', 'one_year', 'three_years'],
'3' => ['six_months', 'one_year', 'three_years', 'five_years'],
],

'resources_core_path' => base_path('resources-core'),
'scheduler' => [
'claim_timeout_minutes' => env('SCHEDULER_CLAIM_TIMEOUT_MINUTES', 5),
Expand Down
3 changes: 3 additions & 0 deletions devhub/pm-font/svg/check-circle-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions devhub/pm-font/svg/exclamation-triangle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading