EscolaLMS/Consultations

View on GitHub
src/Services/ConsultationService.php

Summary

Maintainability
D
1 day
Test Coverage
B
83%
<?php

namespace EscolaLms\Consultations\Services;

use Auth;
use Carbon\Carbon;
use EscolaLms\Consultations\Dto\ConsultationDto;
use EscolaLms\Consultations\Dto\FilterConsultationTermsListDto;
use EscolaLms\Consultations\Dto\FilterListDto;
use EscolaLms\Consultations\Enum\ConstantEnum;
use EscolaLms\Consultations\Enum\ConsultationStatusEnum;
use EscolaLms\Consultations\Enum\ConsultationTermStatusEnum;
use EscolaLms\Consultations\Events\ApprovedTerm;
use EscolaLms\Consultations\Events\ApprovedTermWithTrainer;
use EscolaLms\Consultations\Events\ChangeTerm;
use EscolaLms\Consultations\Events\RejectTerm;
use EscolaLms\Consultations\Events\RejectTermWithTrainer;
use EscolaLms\Consultations\Events\ReminderAboutTerm;
use EscolaLms\Consultations\Events\ReminderTrainerAboutTerm;
use EscolaLms\Consultations\Events\ReportTerm;
use EscolaLms\Consultations\Exceptions\ChangeTermException;
use EscolaLms\Consultations\Exceptions\ConsultationNotFound;
use EscolaLms\Consultations\Helpers\StrategyHelper;
use EscolaLms\Consultations\Http\Requests\ListConsultationsRequest;
use EscolaLms\Consultations\Http\Resources\ConsultationSimpleResource;
use EscolaLms\Consultations\Models\Consultation;
use EscolaLms\Consultations\Models\ConsultationProposedTerm;
use EscolaLms\Consultations\Models\ConsultationUserPivot;
use EscolaLms\Consultations\Models\User;
use EscolaLms\Consultations\Repositories\Contracts\ConsultationRepositoryContract;
use EscolaLms\Consultations\Repositories\Contracts\ConsultationUserRepositoryContract;
use EscolaLms\Consultations\Services\Contracts\ConsultationServiceContract;
use EscolaLms\Core\Dtos\OrderDto;
use EscolaLms\Files\Helpers\FileHelper;
use EscolaLms\Jitsi\Helpers\StringHelper;
use EscolaLms\Jitsi\Services\Contracts\JitsiServiceContract;
use EscolaLms\ModelFields\Facades\ModelFields;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class ConsultationService implements ConsultationServiceContract
{
    private ConsultationRepositoryContract $consultationRepositoryContract;
    private ConsultationUserRepositoryContract $consultationUserRepositoryContract;
    private JitsiServiceContract $jitsiServiceContract;

    public function __construct(
        ConsultationRepositoryContract $consultationRepositoryContract,
        ConsultationUserRepositoryContract $consultationUserRepositoryContract,
        JitsiServiceContract $jitsiServiceContract
    ) {
        $this->consultationRepositoryContract = $consultationRepositoryContract;
        $this->consultationUserRepositoryContract = $consultationUserRepositoryContract;
        $this->jitsiServiceContract = $jitsiServiceContract;
    }

    public function getConsultationsList(array $search = [], bool $onlyActive = false, OrderDto $orderDto = null): Builder
    {
        if ($onlyActive) {
            $now = now()->format('Y-m-d');
            $search['active_to'] = isset($search['active_to']) ? Carbon::make($search['active_to'])->format('Y-m-d') : $now;
            $search['active_from'] = isset($search['active_from']) ? Carbon::make($search['active_from'])->format('Y-m-d') : $now;
            $search['status'] = [ConsultationStatusEnum::PUBLISHED];
        }
        $criteria = FilterListDto::prepareFilters($search);
        return $this->consultationRepositoryContract->allQueryBuilder(
            $search,
            $criteria
        )->orderBy($orderDto->getOrderBy() ?? 'created_at', $orderDto->getOrder() ?? 'desc');
    }

    public function getConsultationsListForCurrentUser(array $search = []): Builder
    {
        $now = now();
        $search['active_to'] = isset($search['active_to']) ? Carbon::make($search['active_to']) : $now;
        $search['active_from'] = isset($search['active_from']) ? Carbon::make($search['active_from']) : $now;
        $criteria = FilterListDto::prepareFilters($search);
        return $this->consultationRepositoryContract->forCurrentUser(
            $search,
            $criteria
        );
    }

    public function store(ConsultationDto $consultationDto): Consultation
    {
        return DB::transaction(function () use($consultationDto) {
            $consultation = $this->consultationRepositoryContract->create($consultationDto->toArray());
            $this->setRelations($consultation, $consultationDto->getRelations());
            $this->setFiles($consultation, $consultationDto->getFiles());
            $consultation->save();
            return $consultation;
        });
    }

    public function update(int $id, ConsultationDto $consultationDto): Consultation
    {
        $consultation = $this->show($id);
        return DB::transaction(function () use($consultation, $consultationDto) {
            $this->setFiles($consultation, $consultationDto->getFiles());
            $consultation = $this->consultationRepositoryContract->updateModel($consultation, $consultationDto->toArray());
            $this->setRelations($consultation, $consultationDto->getRelations());
            return $consultation;
        });
    }

    public function show(int $id): Consultation
    {
        $consultation = $this->consultationRepositoryContract->find($id);
        if (!$consultation) {
            throw new ConsultationNotFound();
        }
        return $consultation;
    }

    public function delete(int $id): ?bool
    {
        return DB::transaction(function () use($id) {
            return $this->consultationRepositoryContract->delete($id);
        });
    }

    public function reportTerm(int $consultationTermId, string $executedAt): bool
    {
        return DB::transaction(function () use ($consultationTermId, $executedAt) {
            $consultationTerm = $this->consultationUserRepositoryContract->find($consultationTermId);
            if ($this->termIsBusy($consultationTerm->consultation_id, $executedAt)) {
                abort(400, __('Term is busy, change your term.'));
            }
            $data = [
                'executed_status' => ConsultationTermStatusEnum::REPORTED,
                'executed_at' => Carbon::make($executedAt)
            ];
            $this->consultationUserRepositoryContract->updateModel($consultationTerm, $data);
            $author = $consultationTerm->consultation->author;
            event(new ReportTerm($author, $consultationTerm));
            return true;
        });
    }

    public function approveTerm(int $consultationTermId): bool
    {
        $consultationTerm = $this->consultationUserRepositoryContract->find($consultationTermId);
        $this->setStatus($consultationTerm, ConsultationTermStatusEnum::APPROVED);
        event(new ApprovedTerm($consultationTerm->user, $consultationTerm));
        event(new ApprovedTermWithTrainer(auth()->user(), $consultationTerm));
        return true;
    }

    public function rejectTerm(int $consultationTermId): bool
    {
        $consultationTerm = $this->consultationUserRepositoryContract->find($consultationTermId);
        $this->setStatus($consultationTerm, ConsultationTermStatusEnum::REJECT);
        event(new RejectTerm($consultationTerm->user, $consultationTerm));
        event(new RejectTermWithTrainer(auth()->user(), $consultationTerm));
        return true;
    }

    public function setStatus(ConsultationUserPivot $consultationTerm, string $status): ConsultationUserPivot
    {
        return DB::transaction(function () use ($status, $consultationTerm) {
            return $this->consultationUserRepositoryContract->updateModel($consultationTerm, ['executed_status' => $status]);
        });
    }

    public function generateJitsi(int $consultationTermId): array
    {
        $consultationTerm = $this->consultationUserRepositoryContract->find($consultationTermId);
        if (!$this->canGenerateJitsi(
            $consultationTerm->executed_at,
            $consultationTerm->executed_status,
            $consultationTerm->consultation->getDuration()
        )) {
            throw new NotFoundHttpException(__('Consultation term is not available'));
        }
        $isModerator = false;
        $configOverwrite = [];
        $configInterface = [];
        if ($consultationTerm->consultation->author === auth()->user()->getKey()) {
            $configOverwrite = [
                "disableModeratorIndicator" => true,
                "startScreenSharing" => false,
                "enableEmailInStats" => false,
            ];
            $isModerator = true;
        }
        if ($consultationTerm->consultation->logotype_path) {
            $configInterface = [
                'BRAND_WATERMARK_LINK' => '',
                'DEFAULT_LOGO_URL' => $consultationTerm->consultation->logotype_url,
                'DEFAULT_WELCOME_PAGE_LOGO_URL' => $consultationTerm->consultation->logotype_url,
                'HIDE_INVITE_MORE_HEADER' => true
            ];
        }
        return $this->jitsiServiceContract->getChannelData(
            auth()->user(),
            StringHelper::convertToJitsiSlug($consultationTerm->consultation->name),
            $isModerator,
            $configOverwrite,
            $configInterface
        );
    }

    public function canGenerateJitsi(?string $executedAt, ?string $status, ?string $duration): bool
    {
        $now = now();
        if (isset($executedAt)) {
            $dateTo = Carbon::make($executedAt);
            return in_array($status, [ConsultationTermStatusEnum::APPROVED, ConsultationTermStatusEnum::REJECT]) &&
                $now->getTimestamp() >= $dateTo->getTimestamp() &&
                !$this->isEnded($executedAt, $duration);
        }
        return false;
    }

    public function generateJitsiUrlForEmail(int $consultationTermId, int $userId): ?string
    {
        $consultationTerm = $this->consultationUserRepositoryContract->find($consultationTermId);
        $isModerator = false;
        $configOverwrite = [];
        $configInterface = [];
        if ($consultationTerm->consultation->author === $userId) {
            $configOverwrite = [
                "disableModeratorIndicator" => true,
                "startScreenSharing" => false,
                "enableEmailInStats" => false,
            ];
            $isModerator = true;
        }
        if ($consultationTerm->consultation->logotype_path) {
            $configInterface = [
                'BRAND_WATERMARK_LINK' => '',
                'DEFAULT_LOGO_URL' => $consultationTerm->consultation->logotype_url,
                'DEFAULT_WELCOME_PAGE_LOGO_URL' => $consultationTerm->consultation->logotype_url,
                'HIDE_INVITE_MORE_HEADER' => true
            ];
        }
        $user = User::find($userId);
        $result = $this->jitsiServiceContract->getChannelData(
            $user,
            StringHelper::convertToJitsiSlug($consultationTerm->consultation->name),
            $isModerator,
            $configOverwrite,
            $configInterface
        );
        return key_exists('url', $result) ? $result['url'] : null;
    }

    public function generateDateTo(string $dateTo, string $duration): ?Carbon
    {
        $modifyTimeStrings = [
            'seconds', 'second', 'minutes', 'minute', 'hours', 'hour', 'weeks', 'week', 'years', 'year'
        ];
        $explode = array_filter(explode(' ', $duration));
        $count = $explode[0] ?? 0;
        $string = $explode[1] ?? 'hours';
        $string = in_array($string, $modifyTimeStrings) ? $string : 'hours';
        return Carbon::make($dateTo)->modify('+' . ((int)$count) . ' ' . $string);
    }

    public function setRelations(Consultation $consultation, array $relations = []): void
    {
        foreach ($relations as $key => $value) {
            $className = 'ConsultationWith' . ucfirst($key) . 'Strategy';
            StrategyHelper::useStrategyPattern(
                $className,
                'RelationsStrategy',
                'setRelation',
                $consultation,
                $relations
            );
        }
    }

    public function proposedTerms(int $consultationTermId): ?array
    {
        $consultationUserPivot = $this->consultationUserRepositoryContract->find($consultationTermId);
        return $this->filterProposedTerms($consultationUserPivot->consultation_id, $consultationUserPivot->consultation->proposedTerms) ?? null;
    }

    public function setFiles(Consultation $consultation, array $files = []): void
    {
        foreach ($files as $key => $file) {
            $consultation->$key = FileHelper::getFilePath($file, ConstantEnum::DIRECTORY . "/{$consultation->getKey()}/images");
        }
    }

    public function getConsultationTermsByConsultationId(int $consultationId, array $search = []): Collection
    {
        $filterConsultationTermsDto = FilterConsultationTermsListDto::prepareFilters(
            array_merge($search, ['consultation_id' => $consultationId])
        );
        return $this->consultationUserRepositoryContract->allQueryBuilder(
            $search,
            $filterConsultationTermsDto
        )->get();
    }

    public function forCurrentUserResponse(
        ListConsultationsRequest $listConsultationsRequest
    ): AnonymousResourceCollection {
        $search = $listConsultationsRequest->except(['limit', 'skip', 'order', 'order_by', 'paginate']);
        $consultations = $this->getConsultationsListForCurrentUser($search);
        if ($listConsultationsRequest->input('paginate', false)) {
            $consultationsCollection = ConsultationSimpleResource::collection($consultations->paginate(
                $listConsultationsRequest->get('per_page') ??
                config('escolalms_consultations.perPage', ConstantEnum::PER_PAGE)
            ));
        } else {
            $consultationsCollection = ConsultationSimpleResource::collection($consultations->get());
        }
        ConsultationSimpleResource::extend(function (ConsultationSimpleResource $consultation) {
            return [
                'consultation_term_id' => $consultation->consultation_user_id,
                'name' => $consultation->name,
                'image_path' => $consultation->image_path,
                'image_url' => $consultation->image_url,
                'executed_status' => $consultation->executed_status,
                'executed_at' => Carbon::make($consultation->executed_at),
                'is_started' => $this->isStarted(
                    $consultation->executed_at,
                    $consultation->executed_status,
                    $consultation->getDuration()
                ),
                'is_ended' => $this->isEnded($consultation->executed_at, $consultation->getDuration()),
                'in_coming' => $this->inComing(
                    $consultation->executed_at,
                    $consultation->executed_status,
                    $consultation->getDuration()
                ),
            ];
        });
        return $consultationsCollection;
    }

    public function attachToUser(array $data): void
    {
        $this->consultationUserRepositoryContract->create($data);
    }

    public function isEnded(?string $executedAt, ?string $duration): bool
    {
        if ($executedAt && $duration !== '') {
            $dateTo = $this->generateDateTo($executedAt, $duration);
            return $dateTo->getTimestamp() <= now()->getTimestamp();
        }
        return false;
    }

    public function isStarted(?string $executedAt, ?string $status, ?string $duration): bool
    {
        return $this->canGenerateJitsi($executedAt, $status, $duration);
    }

    public function inComing(?string $executedAt, ?string $status, ?string $duration): bool
    {
        return !$this->isStarted($executedAt, $status, $duration) && !$this->isEnded($executedAt, $duration);
    }

    public function reminderAboutConsultation(string $reminderStatus): void
    {
        foreach ($this->getReminderData($reminderStatus) as $consultationTerm) {
            event(new ReminderAboutTerm(
                $consultationTerm->user,
                $consultationTerm,
                $reminderStatus
            ));
            event(new ReminderTrainerAboutTerm(
                $consultationTerm->consultation->author,
                $consultationTerm,
                $reminderStatus
            ));
        }
    }

    public function setReminderStatus(ConsultationUserPivot $consultationTerm, string $status): void
    {
        $this->consultationUserRepositoryContract->updateModel($consultationTerm, ['reminder_status' => $status]);
    }

    public function changeTerm(int $consultationTermId, string $executedAt): bool
    {
        DB::transaction(function () use ($consultationTermId, $executedAt) {
            if ($consultationUser = $this->consultationUserRepositoryContract->update([
                'executed_at' => Carbon::make($executedAt),
                'executed_status' => ConsultationTermStatusEnum::APPROVED
            ], $consultationTermId)) {
                if (!$consultationUser->user || !$consultationUser) {
                    throw new ChangeTermException(__('Term is changed but not executed event because user or term is no exists'));
                }
                event(new ChangeTerm($consultationUser->user, $consultationUser));
                return true;
            }
            throw new ChangeTermException(__('Term is not changed'));
        });
        return false;
    }

    public function getConsultationTermsForTutor(): Collection
    {
        return $this->consultationUserRepositoryContract->getByCurrentUserTutor();
    }

    public function termIsBusy(int $consultationId, string $date): bool
    {
        $consultation = $this->consultationRepositoryContract->find($consultationId);
        $terms = $this->consultationUserRepositoryContract->getBusyTerms($consultationId, $date);
        $userId = Auth::user()->getKey();
        if ($terms->first(fn (ConsultationUserPivot $userPivot) => $userPivot->user_id === $userId) !== null) {
            abort(400, __('You already reported this term.'));
        }

        return $terms->count() >= $consultation->max_session_students;
    }

    public function termIsBusyForUser(int $consultationId, string $date, int $userId): bool
    {
        $consultation = $this->consultationRepositoryContract->find($consultationId);
        $terms = $this->consultationUserRepositoryContract->getBusyTerms($consultationId, $date);
        if ($terms->first(fn (ConsultationUserPivot $userPivot) => $userPivot->user_id === $userId) !== null) {
            abort(400, __('Term is busy for this user.'));
        }

        return $terms->count() >= $consultation->max_session_students;
    }

    public function getBusyTermsFormatDate(int $consultationId): array
    {
        return $this->consultationUserRepositoryContract->getBusyTerms($consultationId)->map(
            fn ($term) => Carbon::make($term->executed_at)
        )->unique()->toArray();
    }

    public function filterProposedTerms(int $consultationId, Collection $proposedTerms): array
    {
        $busyTerms = $this->getBusyTermsFormatDate($consultationId);
        return $proposedTerms->map(fn(ConsultationProposedTerm $proposedTerm) => Carbon::make($proposedTerm->proposed_at))->filter(fn (Carbon $proposedTerm) => !in_array($proposedTerm, $busyTerms))->toArray();
    }

    public function updateModelFieldsFromRequest(Consultation $consultation, FormRequest $request): void
    {
        $keys = ModelFields::getFieldsMetadata(Consultation::class)->pluck('name');
        $fields = $request->collect()->only($keys)->toArray();
        $this->consultationRepositoryContract->update($fields, $consultation->getKey());
    }

    private function getReminderData(string $reminderStatus): Collection
    {
        $dateTimeFrom = now()
            ->modify(config('escolalms_consultations.modifier_date.' . $reminderStatus, '+1 hour'))
            ->subMinutes(30);
        $dateTimeTo = now()
            ->modify(config('escolalms_consultations.modifier_date.' . $reminderStatus, '+1 hour'))
            ->addMinutes(30);
        $exclusionStatuses = config('escolalms_consultations.exclusion_reminder_status.' . $reminderStatus, []);
        $data = [
            'date_time_to' => $dateTimeTo,
            'date_time_from' => $dateTimeFrom,
            'reminder_status' => $exclusionStatuses,
            'status' => [ConsultationTermStatusEnum::APPROVED]
        ];
        return $this->consultationUserRepositoryContract->getIncomingTerm(
            FilterConsultationTermsListDto::prepareFilters($data)->getCriteria()
        );
    }
}