Files
Investidor-V2/app/Services/OfferAvailability.php
T

81 lines
3.6 KiB
PHP

<?php
namespace App\Services;
use App\Models\Opportunity;
use Carbon\CarbonImmutable;
/** Read-time exclusion never depends on the scheduler or the NDA. */
final class OfferAvailability
{
/** @return array{state:string,expires_at:?string,policy_pending:bool} */
public function view(mixed $operation, mixed $publication): array
{
// Legacy public snapshots without an editorial hash retain their first frozen policy.
$key = is_array($publication) ? ($publication['editorial_hash'] ?? '') : null;
$key ??= is_array($operation) ? ($operation['last_approval_hash'] ?? '') : '';
if (! is_string($key)) {
return ['state' => 'unavailable', 'expires_at' => null, 'policy_pending' => true];
}
$policy = is_array($operation) ? ($operation['validity_policies'][$key] ?? $operation['validity'] ?? null) : null;
$pending = ! is_array($policy) || ($policy['days'] ?? null) === null;
$result = ['state' => 'available', 'expires_at' => null, 'policy_pending' => $pending];
if ($operation !== null && ! is_array($operation)) {
return [...$result, 'state' => 'unavailable'];
}
$state = $operation['status'] ?? 'available';
if (! is_int($operation['cycle'] ?? 0) || ($operation['cycle'] ?? 0) < 0) {
return [...$result, 'state' => 'unavailable'];
}
if (! in_array($state, ['available', 'withdrawn', 'expired', 'revalidation'], true)) {
return [...$result, 'state' => 'unavailable'];
}
if ($state !== 'available') {
return [...$result, 'state' => $state];
}
$days = $policy['days'] ?? null;
if (($operation['cycle'] ?? 0) > 0 && ($publication['offer_cycle'] ?? null) !== $operation['cycle']) {
return $result;
}
if ($days === null || ! is_array($publication) || ($publication['publication']['state'] ?? null) !== 'published') {
return $result;
}
if (! is_int($days) || $days < 1 || $days > 36500) {
return [...$result, 'state' => 'unavailable'];
}
try {
$at = $publication['publication']['at'] ?? null;
if (! is_string($at) || ! preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D', $at)) {
return [...$result, 'state' => 'unavailable'];
}
$start = CarbonImmutable::parse($at);
$end = $start->addDays($days)->utc();
} catch (\Throwable) {
return [...$result, 'state' => 'unavailable'];
}
return [...$result, 'expires_at' => $end->toIso8601String(), 'state' => $end->lte(CarbonImmutable::now()) ? 'expired' : 'available'];
}
/** @return array{state:string,expires_at:?string,policy_pending:bool} */
public function record(Opportunity $record): array
{
return $this->view($record->workflow_context['offer_operation'] ?? null, $record->workflow_context['offer_publication'] ?? null);
}
public function publicAllowed(mixed $operation, mixed $publication): bool
{
if ($this->view($operation, $publication)['state'] !== 'available') {
return false;
}
$cycle = $operation['cycle'] ?? 0;
if (! is_int($cycle) || $cycle < 0) {
return false;
}
// A new validation must never resurrect the previous public snapshot.
return $cycle === 0 || (is_array($publication) && ($publication['offer_cycle'] ?? null) === $cycle
&& is_string($operation['activated_hash'] ?? null) && ($publication['editorial_hash'] ?? null) === $operation['activated_hash']);
}
}