Skip to content

Commit dfebca4

Browse files
Merge pull request #8773 from ProcessMaker/observation/FOUR-30459
Add Tier Downgrade Support for Case Retention Policy with Auto-Adjustment and Admin Warning
2 parents 8c1f877 + 91da6b1 commit dfebca4

8 files changed

Lines changed: 178 additions & 7 deletions

File tree

ProcessMaker/Console/Commands/EvaluateCaseRetention.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use ProcessMaker\Jobs\EvaluateProcessRetentionJob;
77
use ProcessMaker\Models\Process;
88
use ProcessMaker\Models\ProcessCategory;
9+
use ProcessMaker\Services\CaseRetentionTierService;
910

1011
class EvaluateCaseRetention extends Command
1112
{
@@ -39,6 +40,8 @@ public function handle()
3940

4041
$this->info('Case retention policy is enabled');
4142
$this->info('Dispatching retention evaluation jobs for all processes');
43+
// Get the allowed periods for the current tier (support for downgrading to a lower tier)
44+
$tierAllowedPeriods = CaseRetentionTierService::allowedPeriodsForCurrentTier();
4245

4346
// Get system category IDs to exclude
4447
$systemCategoryIds = ProcessCategory::where('is_system', true)->pluck('id');
@@ -61,9 +64,9 @@ public function handle()
6164
});
6265
}
6366

64-
$query->chunkById(100, function ($processes) use (&$jobCount) {
67+
$query->chunkById(100, function ($processes) use (&$jobCount, $tierAllowedPeriods) {
6568
foreach ($processes as $process) {
66-
dispatch(new EvaluateProcessRetentionJob($process->id));
69+
dispatch(new EvaluateProcessRetentionJob($process->id, $tierAllowedPeriods));
6770
$jobCount++;
6871
}
6972
});

ProcessMaker/Http/Controllers/Api/ProcessController.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use ProcessMaker\Package\WebEntry\Models\WebentryRoute;
4545
use ProcessMaker\Providers\WorkflowServiceProvider;
4646
use ProcessMaker\Rules\BPMNValidation;
47+
use ProcessMaker\Services\CaseRetentionTierService;
4748
use ProcessMaker\Traits\ProjectAssetTrait;
4849
use Throwable;
4950

@@ -224,6 +225,11 @@ public function index(Request $request)
224225
// Get the launchpad configuration
225226
$process->launchpad = ProcessLaunchpad::getLaunchpad($launchpad, $process->id);
226227

228+
$process->case_retention_tier_adjustment_notice = false;
229+
if ($user->is_administrator && config('app.case_retention_policy_enabled')) {
230+
$process->case_retention_tier_adjustment_notice = CaseRetentionTierService::adjustmentNoticeIsActive($process);
231+
}
232+
227233
// Filter all processes that have event definitions (start events like message event, conditional event, signal event, timer event)
228234
if ($request->has('without_event_definitions') && $request->input('without_event_definitions') == 'true') {
229235
$startEvents = $process->events->filter(function ($event) {
@@ -606,6 +612,12 @@ public function update(Request $request, Process $process)
606612
$this->restoreProcessRetentionPropertiesFromOriginal($process, $original);
607613
}
608614

615+
if (auth()->user()->is_administrator && $request->has('properties') && is_array($request->input('properties')) && array_key_exists('retention_period', $request->input('properties'))) {
616+
$properties = $process->properties ?? [];
617+
unset($properties[CaseRetentionTierService::NOTICE_PROPERTY_KEY], $properties[CaseRetentionTierService::NOTICE_AT_PROPERTY_KEY]);
618+
$process->properties = $properties;
619+
}
620+
609621
// Catch errors to send more specific status
610622
try {
611623
$process->saveOrFail();
@@ -697,7 +709,13 @@ private function restoreProcessRetentionPropertiesFromOriginal(Process $process,
697709
$properties = [];
698710
}
699711

700-
$keys = ['retention_updated_by', 'retention_updated_at', 'retention_period'];
712+
$keys = [
713+
'retention_updated_by',
714+
'retention_updated_at',
715+
'retention_period',
716+
CaseRetentionTierService::NOTICE_PROPERTY_KEY,
717+
CaseRetentionTierService::NOTICE_AT_PROPERTY_KEY,
718+
];
701719
foreach ($keys as $key) {
702720
if (array_key_exists($key, $originalProperties)) {
703721
$properties[$key] = $originalProperties[$key];

ProcessMaker/Http/Resources/Process.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function toArray($request)
2828
$array['svg'] = $this->svg;
2929
}
3030
$array['manager_id'] = $this->manager_id;
31+
$array['case_retention_tier_adjustment_notice'] = (bool) ($this->resource->case_retention_tier_adjustment_notice ?? false);
3132

3233
return $array;
3334
}

ProcessMaker/Jobs/EvaluateProcessRetentionJob.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@
1515
use ProcessMaker\Models\ProcessRequest;
1616
use ProcessMaker\Models\ProcessRequestToken;
1717
use ProcessMaker\Models\TaskDraft;
18+
use ProcessMaker\Services\CaseRetentionTierService;
1819

1920
class EvaluateProcessRetentionJob implements ShouldQueue
2021
{
2122
use Queueable, DeletesCaseRecords;
2223

2324
/**
24-
* Create a new job instance.
25+
* @param list<string>|null $tierAllowedPeriods From {@see EvaluateCaseRetention} so tier options are not
26+
* re-resolved for every queued process; null = resolve in job.
2527
*/
26-
public function __construct(public int $processId)
27-
{
28+
public function __construct(
29+
public int $processId,
30+
public ?array $tierAllowedPeriods = null,
31+
) {
2832
}
2933

3034
/**
@@ -75,6 +79,14 @@ public function handle(): void
7579
return;
7680
}
7781

82+
if (CaseRetentionTierService::clampProcessRetentionToCurrentTier($process, $this->tierAllowedPeriods)) {
83+
Log::info('EvaluateProcessRetentionJob: Retention period clamped to current tier maximum', [
84+
'process_id' => $this->processId,
85+
'retention_period' => $process->properties['retention_period'] ?? null,
86+
]);
87+
$process->refresh();
88+
}
89+
7890
// Default to one_year if retention_period is not set
7991
$retentionPeriod = $process->properties['retention_period'] ?? 'one_year';
8092
$retentionMonths = match ($retentionPeriod) {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ProcessMaker\Services;
6+
7+
use Carbon\Carbon;
8+
use ProcessMaker\Models\Process;
9+
10+
class CaseRetentionTierService
11+
{
12+
/**
13+
* @deprecated Stored only for backward compatibility; use {@see self::NOTICE_AT_PROPERTY_KEY}.
14+
*/
15+
public const NOTICE_PROPERTY_KEY = 'case_retention_tier_adjustment_notice';
16+
17+
public const NOTICE_AT_PROPERTY_KEY = 'case_retention_tier_adjustment_notice_at';
18+
19+
public const NOTICE_DURATION_HOURS = 24;
20+
21+
private const VALID_PERIODS = ['six_months', 'one_year', 'three_years', 'five_years'];
22+
23+
/** @var array<string, int> */
24+
private const PERIOD_MONTHS = [
25+
'six_months' => 6,
26+
'one_year' => 12,
27+
'three_years' => 36,
28+
'five_years' => 60,
29+
];
30+
31+
/**
32+
* Whether the process-listing warning for a tier-driven retention clamp should show (admins only).
33+
*/
34+
public static function adjustmentNoticeIsActive(Process $process): bool
35+
{
36+
$props = $process->properties ?? [];
37+
$at = $props[self::NOTICE_AT_PROPERTY_KEY] ?? null;
38+
if (is_string($at) && $at !== '') {
39+
return Carbon::parse($at)->greaterThan(now()->subHours(self::NOTICE_DURATION_HOURS));
40+
}
41+
42+
if (filter_var($props[self::NOTICE_PROPERTY_KEY] ?? false, FILTER_VALIDATE_BOOLEAN)) {
43+
$updatedAt = $process->updated_at;
44+
45+
return $updatedAt && Carbon::parse($updatedAt)->greaterThan(now()->subHours(self::NOTICE_DURATION_HOURS));
46+
}
47+
48+
return false;
49+
}
50+
51+
/**
52+
* Retention period options allowed for the configured CASE_RETENTION_TIER.
53+
*
54+
* @return list<string>
55+
*/
56+
public static function allowedPeriodsForCurrentTier(): array
57+
{
58+
$tier = (string) config('app.case_retention_tier', '1');
59+
$options = config('app.case_retention_tier_options', []);
60+
61+
return $options[$tier] ?? $options['1'] ?? ['six_months', 'one_year'];
62+
}
63+
64+
/**
65+
* Longest retention period in the allowed list (by duration), not by array order.
66+
*
67+
* @param list<string> $allowed
68+
*/
69+
public static function longestAllowedPeriod(array $allowed): string
70+
{
71+
$best = 'one_year';
72+
$bestMonths = 0;
73+
foreach ($allowed as $period) {
74+
if (!is_string($period)) {
75+
continue;
76+
}
77+
$months = self::PERIOD_MONTHS[$period] ?? null;
78+
if ($months === null) {
79+
continue;
80+
}
81+
if ($months > $bestMonths) {
82+
$bestMonths = $months;
83+
$best = $period;
84+
}
85+
}
86+
87+
return $bestMonths > 0 ? $best : 'one_year';
88+
}
89+
90+
public static function normalizePeriod(mixed $period): string
91+
{
92+
if (is_string($period) && in_array($period, self::VALID_PERIODS, true)) {
93+
return $period;
94+
}
95+
96+
return 'one_year';
97+
}
98+
99+
/**
100+
* If the process retention period is not allowed for the current tier, set it to the
101+
* longest period allowed for that tier, refresh retention_updated_at, clear retention_updated_by
102+
* (so the UI shows the default retention message), and record when to show the admin notice (24h).
103+
*
104+
* @param list<string>|null $tierAllowedPeriods When null, resolved from config; when set (e.g. from a
105+
* batch command), avoids re-reading tier options per process.
106+
* @return bool True when the process was updated.
107+
*/
108+
public static function clampProcessRetentionToCurrentTier(Process $process, ?array $tierAllowedPeriods = null): bool
109+
{
110+
$allowed = $tierAllowedPeriods ?? self::allowedPeriodsForCurrentTier();
111+
$current = self::normalizePeriod($process->properties['retention_period'] ?? null);
112+
113+
if (in_array($current, $allowed, true)) {
114+
return false;
115+
}
116+
117+
$maxPeriod = self::longestAllowedPeriod($allowed);
118+
$properties = $process->properties ?? [];
119+
$properties['retention_period'] = $maxPeriod;
120+
$properties['retention_updated_at'] = now()->toIso8601String();
121+
unset($properties['retention_updated_by']);
122+
unset($properties[self::NOTICE_PROPERTY_KEY]);
123+
$properties[self::NOTICE_AT_PROPERTY_KEY] = now()->toIso8601String();
124+
$process->properties = $properties;
125+
$process->saveQuietly();
126+
127+
return true;
128+
}
129+
}

config/app.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@
310310
'case_retention_policy_enabled' => filter_var(env('CASE_RETENTION_POLICY_ENABLED', false), FILTER_VALIDATE_BOOLEAN),
311311

312312
// Controls which retention periods are available in the UI for the current tier.
313-
'case_retention_tier' => env('CASE_RETENTION_TIER', '1'),
313+
'case_retention_tier' => trim((string) env('CASE_RETENTION_TIER', '1'), " \t\n\r\0\x0B\"'"),
314314
'case_retention_tier_options' => [
315315
'1' => ['six_months', 'one_year'],
316316
'2' => ['six_months', 'one_year', 'three_years'],

resources/js/processes/components/ProcessMixin.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ export default {
123123
if (process.warnings) {
124124
process.warningMessages.push(this.$t("BPMN validation issues. Request cannot be started."));
125125
}
126+
if (process.case_retention_tier_adjustment_notice) {
127+
process.warningMessages.push(
128+
this.$t(
129+
"Case retention was automatically shortened to match your subscription tier. The new retention period applies immediately.",
130+
),
131+
);
132+
}
126133
return process;
127134
});
128135
return data;

resources/lang/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,6 +1607,7 @@
16071607
"Process is missing end event": "Process is missing end event",
16081608
"Process is missing start event": "Process is missing start event",
16091609
"Process Launchpad": "Process Launchpad",
1610+
"Case retention was automatically shortened to match your subscription tier. The new retention period applies immediately.": "Case retention was automatically shortened to match your subscription tier. The new retention period applies immediately.",
16101611
"Process Manager not configured.": "Process Manager not configured.",
16111612
"Process Manager": "Process Manager",
16121613
"Process Owner": "Process Owner",

0 commit comments

Comments
 (0)