add assignment completion api

This commit is contained in:
Yisroel Baum 2026-08-15 22:43:06 +03:00
parent 916f070455
commit 350d2ec0b7
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
11 changed files with 281 additions and 16 deletions

View file

@ -5,6 +5,7 @@ namespace App\Schedule;
use App\User\User;
use DateTimeImmutable;
use DateTimeZone;
use DomainException;
use Illuminate\Support\Facades\DB;
class EloquentScheduleRepository implements ScheduleRepository
@ -29,6 +30,7 @@ class EloquentScheduleRepository implements ScheduleRepository
'scheduled_date' => $assignmentDto->scheduledDate
->format('Y-m-d'),
'position' => $assignmentDto->position,
'completed_at' => null,
]);
}
@ -77,6 +79,7 @@ class EloquentScheduleRepository implements ScheduleRepository
->where('schedule_assignments.scheduled_date', $date->format(
'Y-m-d',
))
->whereNull('schedule_assignments.completed_at')
->with('schedule')
->orderByDesc('schedules.id')
->orderBy('schedule_assignments.position')
@ -95,6 +98,50 @@ class EloquentScheduleRepository implements ScheduleRepository
return $assignments;
}
public function findAssignmentForUser(
int $id,
User $user,
): ?ScheduleAssignment {
$model = ScheduleAssignmentModel::query()
->select('schedule_assignments.*')
->join(
'schedules',
'schedules.id',
'=',
'schedule_assignments.schedule_id',
)
->where('schedule_assignments.id', $id)
->where('schedules.user_id', $user->getId())
->first();
return $model === null
? null
: $this->assignmentToDomain($model);
}
public function updateAssignment(
ScheduleAssignment $assignment,
): ScheduleAssignment {
$query = ScheduleAssignmentModel::query()
->whereKey($assignment->getId());
if ($assignment->getCompletedAt() === null) {
$query->update(['completed_at' => null]);
} else {
$query->whereNull('completed_at')->update([
'completed_at' => $assignment->getCompletedAt(),
]);
}
$model = ScheduleAssignmentModel::find($assignment->getId());
if ($model === null) {
throw new DomainException(
"Assignment with id {$assignment->getId()} not found",
);
}
return $this->assignmentToDomain($model);
}
private function toDomain(ScheduleModel $model, User $user): Schedule
{
$assignmentModels = ScheduleAssignmentModel::query()
@ -129,6 +176,9 @@ class EloquentScheduleRepository implements ScheduleRepository
path: $model->element_path,
scheduledDate: $this->date($model->scheduled_date),
position: $model->position,
completedAt: $model->completed_at === null
? null
: $this->dateTime($model->completed_at),
);
}
@ -136,4 +186,10 @@ class EloquentScheduleRepository implements ScheduleRepository
{
return new DateTimeImmutable($value, new DateTimeZone('UTC'));
}
private function dateTime(DateTimeImmutable $value): DateTimeImmutable
{
return DateTimeImmutable::createFromInterface($value)
->setTimezone(new DateTimeZone('UTC'));
}
}

View file

@ -4,7 +4,7 @@ namespace App\Schedule;
use DateTimeImmutable;
final readonly class ScheduleAssignment
final class ScheduleAssignment
{
/**
* @param list<string> $path
@ -16,6 +16,7 @@ final readonly class ScheduleAssignment
private array $path,
private DateTimeImmutable $scheduledDate,
private int $position,
private ?DateTimeImmutable $completedAt,
) {}
public function getId(): int
@ -50,4 +51,21 @@ final readonly class ScheduleAssignment
{
return $this->position;
}
public function getCompletedAt(): ?DateTimeImmutable
{
return $this->completedAt;
}
public function complete(DateTimeImmutable $completedAt): void
{
if ($this->completedAt === null) {
$this->completedAt = $completedAt;
}
}
public function reopen(): void
{
$this->completedAt = null;
}
}

View file

@ -2,6 +2,7 @@
namespace App\Schedule;
use DateTimeImmutable;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
@ -15,6 +16,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property list<string> $element_path
* @property string $scheduled_date
* @property int $position
* @property DateTimeImmutable|null $completed_at
* @property-read ScheduleModel $schedule
*
* @method static Builder<static>|ScheduleAssignmentModel newModelQuery()
@ -30,6 +32,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
'element_path',
'scheduled_date',
'position',
'completed_at',
])]
class ScheduleAssignmentModel extends Model
{
@ -46,6 +49,7 @@ class ScheduleAssignmentModel extends Model
'schedule_id' => 'integer',
'element_path' => 'array',
'position' => 'integer',
'completed_at' => 'immutable_datetime',
];
}

View file

@ -23,4 +23,13 @@ interface ScheduleRepository
User $user,
DateTimeImmutable $date,
): array;
public function findAssignmentForUser(
int $id,
User $user,
): ?ScheduleAssignment;
public function updateAssignment(
ScheduleAssignment $assignment,
): ScheduleAssignment;
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Schedule\UseCases\SetAssignmentCompletion;
use App\Auth\Clock;
use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
use App\Schedule\ScheduleAssignment;
use App\Schedule\ScheduleRepository;
class SetAssignmentCompletion
{
public function __construct(
private ScheduleRepository $scheduleRepository,
private Clock $clock,
) {}
/**
* @throws BadRequestException
* @throws NotFoundException
*/
public function execute(
SetAssignmentCompletionRequest $request,
): ScheduleAssignment {
if ($request->completed === null) {
throw new BadRequestException(
'completed must be a boolean',
);
}
$assignment = $this->scheduleRepository->findAssignmentForUser(
$request->assignmentId,
$request->user,
);
if ($assignment === null) {
throw new NotFoundException('assignment not found');
}
if ($request->completed) {
$assignment->complete($this->clock->now());
} else {
$assignment->reopen();
}
return $this->scheduleRepository->updateAssignment($assignment);
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Schedule\UseCases\SetAssignmentCompletion;
use App\User\User;
final readonly class SetAssignmentCompletionRequest
{
public function __construct(
public int $assignmentId,
public User $user,
public ?bool $completed,
) {}
}