update api and add more features
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Promotions;
|
||||
|
||||
use App\Models\StudentPromotionRecord;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* State machine for promotion records (plan sections 3, 4, 7).
|
||||
*
|
||||
* All status transitions go through this service so the audit trail
|
||||
* stays in sync (plan section 18).
|
||||
*/
|
||||
class PromotionStatusService
|
||||
{
|
||||
public function __construct(private PromotionAuditService $audit)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of allowed transitions from current → set of next statuses.
|
||||
* Edge cases (manual overrides) are handled by `forceStatus`.
|
||||
*/
|
||||
private const ALLOWED_TRANSITIONS = [
|
||||
StudentPromotionRecord::STATUS_NOT_REVIEWED => [
|
||||
StudentPromotionRecord::STATUS_ELIGIBLE,
|
||||
StudentPromotionRecord::STATUS_AWAITING_PARENT,
|
||||
StudentPromotionRecord::STATUS_CONDITIONAL,
|
||||
StudentPromotionRecord::STATUS_REPEATED,
|
||||
StudentPromotionRecord::STATUS_ON_HOLD,
|
||||
StudentPromotionRecord::STATUS_GRADUATED,
|
||||
StudentPromotionRecord::STATUS_WITHDRAWN,
|
||||
],
|
||||
StudentPromotionRecord::STATUS_ELIGIBLE => [
|
||||
StudentPromotionRecord::STATUS_AWAITING_PARENT,
|
||||
StudentPromotionRecord::STATUS_ENROLLMENT_STARTED,
|
||||
StudentPromotionRecord::STATUS_CONDITIONAL,
|
||||
StudentPromotionRecord::STATUS_ON_HOLD,
|
||||
StudentPromotionRecord::STATUS_NOT_ENROLLED,
|
||||
StudentPromotionRecord::STATUS_WITHDRAWN,
|
||||
],
|
||||
StudentPromotionRecord::STATUS_AWAITING_PARENT => [
|
||||
StudentPromotionRecord::STATUS_ENROLLMENT_STARTED,
|
||||
StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED,
|
||||
StudentPromotionRecord::STATUS_NOT_ENROLLED,
|
||||
StudentPromotionRecord::STATUS_ON_HOLD,
|
||||
StudentPromotionRecord::STATUS_WITHDRAWN,
|
||||
],
|
||||
StudentPromotionRecord::STATUS_ENROLLMENT_STARTED => [
|
||||
StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED,
|
||||
StudentPromotionRecord::STATUS_AWAITING_PARENT,
|
||||
StudentPromotionRecord::STATUS_NOT_ENROLLED,
|
||||
StudentPromotionRecord::STATUS_ON_HOLD,
|
||||
StudentPromotionRecord::STATUS_WITHDRAWN,
|
||||
],
|
||||
StudentPromotionRecord::STATUS_CONDITIONAL => [
|
||||
StudentPromotionRecord::STATUS_ELIGIBLE,
|
||||
StudentPromotionRecord::STATUS_AWAITING_PARENT,
|
||||
StudentPromotionRecord::STATUS_REPEATED,
|
||||
StudentPromotionRecord::STATUS_ON_HOLD,
|
||||
StudentPromotionRecord::STATUS_NOT_REVIEWED,
|
||||
],
|
||||
StudentPromotionRecord::STATUS_ON_HOLD => [
|
||||
StudentPromotionRecord::STATUS_NOT_REVIEWED,
|
||||
StudentPromotionRecord::STATUS_ELIGIBLE,
|
||||
StudentPromotionRecord::STATUS_AWAITING_PARENT,
|
||||
StudentPromotionRecord::STATUS_REPEATED,
|
||||
StudentPromotionRecord::STATUS_CONDITIONAL,
|
||||
StudentPromotionRecord::STATUS_WITHDRAWN,
|
||||
StudentPromotionRecord::STATUS_NOT_ENROLLED,
|
||||
],
|
||||
// Terminal statuses are intentionally not in the map (no auto
|
||||
// transitions); admins can still override via `forceStatus`.
|
||||
];
|
||||
|
||||
public function canTransition(string $from, string $to): bool
|
||||
{
|
||||
if ($from === $to) {
|
||||
return true;
|
||||
}
|
||||
if (!in_array($to, StudentPromotionRecord::ALL_STATUSES, true)) {
|
||||
return false;
|
||||
}
|
||||
$allowed = self::ALLOWED_TRANSITIONS[$from] ?? [];
|
||||
return in_array($to, $allowed, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a status transition with audit logging. Returns the saved
|
||||
* record. Throws if the new status is invalid or the transition is
|
||||
* not allowed.
|
||||
*/
|
||||
public function transition(
|
||||
StudentPromotionRecord $record,
|
||||
string $newStatus,
|
||||
?int $userId = null,
|
||||
?string $notes = null
|
||||
): StudentPromotionRecord {
|
||||
if (!in_array($newStatus, StudentPromotionRecord::ALL_STATUSES, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown promotion status "%s".', $newStatus));
|
||||
}
|
||||
|
||||
$oldStatus = (string) $record->promotion_status;
|
||||
if ($oldStatus === $newStatus) {
|
||||
return $record;
|
||||
}
|
||||
if (!$this->canTransition($oldStatus, $newStatus)) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Transition from "%s" to "%s" is not allowed.',
|
||||
$oldStatus,
|
||||
$newStatus
|
||||
));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($record, $oldStatus, $newStatus, $userId, $notes) {
|
||||
$record->promotion_status = $newStatus;
|
||||
$record->updated_by = $userId;
|
||||
$record->save();
|
||||
|
||||
$this->audit->logStatusChange($record, $oldStatus, $newStatus, $userId, $notes);
|
||||
|
||||
return $record;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a status change ignoring the transition map (e.g. admin
|
||||
* override). The audit trail records this as a manual override.
|
||||
*/
|
||||
public function forceStatus(
|
||||
StudentPromotionRecord $record,
|
||||
string $newStatus,
|
||||
?int $userId = null,
|
||||
?string $notes = null
|
||||
): StudentPromotionRecord {
|
||||
if (!in_array($newStatus, StudentPromotionRecord::ALL_STATUSES, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown promotion status "%s".', $newStatus));
|
||||
}
|
||||
|
||||
$oldStatus = (string) $record->promotion_status;
|
||||
if ($oldStatus === $newStatus) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($record, $oldStatus, $newStatus, $userId, $notes) {
|
||||
$record->promotion_status = $newStatus;
|
||||
$record->updated_by = $userId;
|
||||
$record->save();
|
||||
|
||||
$this->audit->logManualOverride(
|
||||
$record,
|
||||
'promotion_status',
|
||||
$oldStatus,
|
||||
$newStatus,
|
||||
$userId,
|
||||
$notes ?? 'Manual status override'
|
||||
);
|
||||
|
||||
return $record;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user