Ignore:
Timestamp:
08/22/26 19:00:38 (18 hours ago)
Author:
veronika-ils <ilioskaveronika@…>
Branches:
master
Parents:
ae83647
Message:

feat: The app is consistent with the changes made in phase 1 and phase 2

Location:
petify-backend/src/main/java/com/petify/petify
Files:
1 added
8 edited

Legend:

Unmodified
Added
Removed
  • petify-backend/src/main/java/com/petify/petify/api/VetClinicsController.java

    rae83647 rf6ed6e4  
    22
    33import com.petify.petify.domain.VetClinic;
     4import com.petify.petify.dto.UpdateClinicScheduleRequest;
    45import com.petify.petify.dto.VetClinicDTO;
    56import com.petify.petify.repo.VetClinicRepository;
    67import org.springframework.http.ResponseEntity;
    78import org.springframework.web.bind.annotation.GetMapping;
     9import org.springframework.web.bind.annotation.PutMapping;
    810import org.springframework.web.bind.annotation.RequestHeader;
    911import org.springframework.web.bind.annotation.RequestMapping;
     12import org.springframework.web.bind.annotation.RequestBody;
    1013import org.springframework.web.bind.annotation.RestController;
    1114
     15import java.time.LocalTime;
     16import java.time.format.DateTimeParseException;
     17import java.util.Arrays;
     18import java.util.Set;
    1219import java.util.List;
    1320import java.util.Map;
     
    1724@RequestMapping("/api/clinics")
    1825public class VetClinicsController {
     26
     27    private static final Set<String> ALLOWED_WORK_DAYS = Set.of(
     28        "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"
     29    );
    1930
    2031    private final VetClinicRepository vetClinicRepository;
     
    4051    }
    4152
     53    @PutMapping("/my/schedule")
     54    public ResponseEntity<?> updateMyClinicSchedule(
     55        @RequestHeader("X-User-Id") Long userId,
     56        @RequestBody UpdateClinicScheduleRequest request) {
     57        try {
     58            VetClinic clinic = vetClinicRepository.findByUserId(userId)
     59                .orElseThrow(() -> new RuntimeException("User is not linked to a clinic"));
     60
     61            String workDays = normalizeWorkDays(request.getWorkDays());
     62            LocalTime startTime = parseTime(request.getStartTime(), "Start time");
     63            LocalTime endTime = parseTime(request.getEndTime(), "End time");
     64
     65            if (!startTime.isBefore(endTime)) {
     66                throw new RuntimeException("Start time must be before end time");
     67            }
     68
     69            if (java.time.Duration.between(startTime, endTime).toMinutes() < 30) {
     70                throw new RuntimeException("Clinic schedule must contain at least one 30-minute slot");
     71            }
     72
     73            clinic.setWorkDays(workDays);
     74            clinic.setStartTime(startTime);
     75            clinic.setEndTime(endTime);
     76
     77            return ResponseEntity.ok(mapToDTO(vetClinicRepository.save(clinic)));
     78        } catch (RuntimeException e) {
     79            return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
     80        }
     81    }
     82
    4283    private VetClinicDTO mapToDTO(VetClinic clinic) {
    4384        return new VetClinicDTO(
     
    4586            clinic.getName(),
    4687            clinic.getCity(),
    47             clinic.getAddress()
     88            clinic.getAddress(),
     89            clinic.getWorkDays(),
     90            clinic.getStartTime() != null ? clinic.getStartTime().toString() : null,
     91            clinic.getEndTime() != null ? clinic.getEndTime().toString() : null
    4892        );
     93    }
     94
     95    private String normalizeWorkDays(String workDays) {
     96        if (workDays == null || workDays.isBlank()) {
     97            throw new RuntimeException("Choose at least one working day");
     98        }
     99
     100        List<String> normalized = Arrays.stream(workDays.split(","))
     101            .map(String::trim)
     102            .filter(day -> !day.isBlank())
     103            .map(String::toUpperCase)
     104            .distinct()
     105            .toList();
     106
     107        if (normalized.isEmpty()) {
     108            throw new RuntimeException("Choose at least one working day");
     109        }
     110
     111        for (String day : normalized) {
     112            if (!ALLOWED_WORK_DAYS.contains(day)) {
     113                throw new RuntimeException("Invalid working day: " + day);
     114            }
     115        }
     116
     117        return String.join(",", normalized);
     118    }
     119
     120    private LocalTime parseTime(String value, String label) {
     121        if (value == null || value.isBlank()) {
     122            throw new RuntimeException(label + " is required");
     123        }
     124
     125        try {
     126            return LocalTime.parse(value);
     127        } catch (DateTimeParseException e) {
     128            throw new RuntimeException(label + " must be in HH:mm format");
     129        }
    49130    }
    50131}
  • petify-backend/src/main/java/com/petify/petify/config/SecurityConfig.java

    rae83647 rf6ed6e4  
    9696                        .requestMatchers(HttpMethod.GET, "/api/clinics").permitAll()
    9797                        .requestMatchers(HttpMethod.GET, "/api/clinics/my").permitAll()
     98                        .requestMatchers(HttpMethod.PUT, "/api/clinics/my/schedule").permitAll()
    9899                        .requestMatchers(HttpMethod.GET, "/api/notifications/my").permitAll()
    99100
  • petify-backend/src/main/java/com/petify/petify/domain/UserReview.java

    rae83647 rf6ed6e4  
    1919    private Long targetUserId;
    2020
     21    @Column(name = "interaction_type", nullable = false, length = 40)
     22    private String interactionType;
     23
    2124    public UserReview() {}
    2225
    23     public UserReview(Review review, Long targetUserId) {
     26    public UserReview(Review review, Long targetUserId, String interactionType) {
    2427        this.review = review;        // DO NOT set reviewId
    2528        this.targetUserId = targetUserId;
     29        this.interactionType = interactionType;
    2630    }
    2731
     
    5155        this.targetUserId = targetUserId;
    5256    }
     57
     58    public String getInteractionType() {
     59        return interactionType;
     60    }
     61
     62    public void setInteractionType(String interactionType) {
     63        this.interactionType = interactionType;
     64    }
    5365}
    5466
  • petify-backend/src/main/java/com/petify/petify/domain/VetClinic.java

    rae83647 rf6ed6e4  
    44import lombok.Getter;
    55import lombok.Setter;
     6
     7import java.time.LocalTime;
    68
    79@Setter
     
    3739    private String address;
    3840
     41    @Column(name = "work_days")
     42    private String workDays;
     43
     44    @Column(name = "start_time")
     45    private LocalTime startTime;
     46
     47    @Column(name = "end_time")
     48    private LocalTime endTime;
     49
    3950    public VetClinic() {}
    4051
  • petify-backend/src/main/java/com/petify/petify/dto/CreateReviewRequest.java

    rae83647 rf6ed6e4  
    99    private Integer rating;
    1010    private String comment;
     11    private String interactionType;
    1112
    1213    public CreateReviewRequest() {
     
    1819    }
    1920
     21    public CreateReviewRequest(Integer rating, String comment, String interactionType) {
     22        this.rating = rating;
     23        this.comment = comment;
     24        this.interactionType = interactionType;
     25    }
     26
    2027}
  • petify-backend/src/main/java/com/petify/petify/dto/VetClinicDTO.java

    rae83647 rf6ed6e4  
    1111    private String city;
    1212    private String address;
     13    private String workDays;
     14    private String startTime;
     15    private String endTime;
     16    private Boolean scheduleComplete;
    1317
    1418    public VetClinicDTO() {
     
    2226    }
    2327
     28    public VetClinicDTO(
     29        Long clinicId,
     30        String name,
     31        String city,
     32        String address,
     33        String workDays,
     34        String startTime,
     35        String endTime
     36    ) {
     37        this.clinicId = clinicId;
     38        this.name = name;
     39        this.city = city;
     40        this.address = address;
     41        this.workDays = workDays;
     42        this.startTime = startTime;
     43        this.endTime = endTime;
     44        this.scheduleComplete = workDays != null && !workDays.isBlank() && startTime != null && endTime != null;
     45    }
     46
    2447}
    2548
  • petify-backend/src/main/java/com/petify/petify/service/AppointmentService.java

    rae83647 rf6ed6e4  
    3131import java.time.LocalTime;
    3232import java.time.format.DateTimeFormatter;
     33import java.util.Arrays;
    3334import java.util.List;
    3435import java.util.Set;
     
    3839
    3940    private static final Logger logger = LoggerFactory.getLogger(AppointmentService.class);
    40     private static final LocalTime CLINIC_DAY_START = LocalTime.of(9, 0);
    41     private static final LocalTime CLINIC_DAY_END = LocalTime.of(17, 0);
    4241    private static final int SLOT_MINUTES = 30;
    4342    private static final List<String> NON_BLOCKING_STATUSES = List.of("CANCELLED", "CANCELED", "NO_SHOW");
     
    8483        }
    8584
    86         if (!vetClinicRepository.existsById(request.getClinicId())) {
    87             throw new RuntimeException("Vet clinic not found");
    88         }
     85        VetClinic clinic = vetClinicRepository.findById(request.getClinicId())
     86            .orElseThrow(() -> new RuntimeException("Vet clinic not found"));
    8987
    9088        LocalDateTime appointmentTime = LocalDateTime.parse(request.getDateTime());
    91         if (!isClinicSlotAvailable(request.getClinicId(), appointmentTime)) {
     89        if (!isClinicSlotAvailable(clinic, appointmentTime)) {
    9290            throw new RuntimeException("Selected appointment slot is no longer available");
    9391        }
     
    210208        }
    211209
    212         if (!vetClinicRepository.existsById(clinicId)) {
    213             throw new RuntimeException("Vet clinic not found");
    214         }
    215 
    216         LocalDateTime dayStart = date.atTime(CLINIC_DAY_START);
    217         LocalDateTime dayEnd = date.atTime(CLINIC_DAY_END);
     210        VetClinic clinic = vetClinicRepository.findById(clinicId)
     211            .orElseThrow(() -> new RuntimeException("Vet clinic not found"));
     212        requireCompleteSchedule(clinic);
     213
     214        if (!isWorkingDay(clinic, date)) {
     215            return List.of();
     216        }
     217
     218        LocalDateTime dayStart = date.atTime(clinic.getStartTime());
     219        LocalDateTime dayEnd = date.atTime(clinic.getEndTime());
    218220        Set<LocalDateTime> bookedSlots = appointmentRepository
    219221            .findByClinicIdAndDateTimeBetweenAndStatusNotInOrderByDateTimeAsc(
     
    257259        }
    258260
    259         if (!vetClinicRepository.existsById(clinicId)) {
    260             throw new RuntimeException("Vet clinic not found");
    261         }
    262 
    263         LocalDateTime dayStart = date.atTime(CLINIC_DAY_START);
    264         LocalDateTime dayEnd = date.atTime(CLINIC_DAY_END);
     261        VetClinic clinic = vetClinicRepository.findById(clinicId)
     262            .orElseThrow(() -> new RuntimeException("Vet clinic not found"));
     263        requireCompleteSchedule(clinic);
     264
     265        if (!isWorkingDay(clinic, date)) {
     266            return List.of();
     267        }
     268
     269        LocalDateTime dayStart = date.atTime(clinic.getStartTime());
     270        LocalDateTime dayEnd = date.atTime(clinic.getEndTime());
    265271
    266272        return unavailableSlotRepository
     
    282288        }
    283289
    284         if (!vetClinicRepository.existsById(clinicId)) {
    285             throw new RuntimeException("Vet clinic not found");
    286         }
     290        VetClinic clinic = vetClinicRepository.findById(clinicId)
     291            .orElseThrow(() -> new RuntimeException("Vet clinic not found"));
    287292
    288293        LocalDateTime slotTime = LocalDateTime.parse(request.getDateTime());
    289         if (!isValidWorkingSlot(slotTime)) {
    290             throw new RuntimeException("Unavailable slot must be a future 30-minute slot between 09:00 and 17:00");
     294        if (!isValidWorkingSlot(clinic, slotTime)) {
     295            throw new RuntimeException("Unavailable slot must be a future 30-minute slot inside your clinic working schedule");
    291296        }
    292297
     
    332337    }
    333338
    334     private boolean isClinicSlotAvailable(Long clinicId, LocalDateTime appointmentTime) {
    335         if (!isValidWorkingSlot(appointmentTime)) {
     339    private boolean isClinicSlotAvailable(VetClinic clinic, LocalDateTime appointmentTime) {
     340        if (!isValidWorkingSlot(clinic, appointmentTime)) {
    336341            return false;
    337342        }
    338343
    339344        return !appointmentRepository.existsByClinicIdAndDateTimeAndStatusNotIn(
    340             clinicId,
     345            clinic.getClinicId(),
    341346            appointmentTime,
    342347            NON_BLOCKING_STATUSES
    343         ) && !unavailableSlotRepository.existsByClinicIdAndDateTime(clinicId, appointmentTime);
    344     }
    345 
    346     private boolean isValidWorkingSlot(LocalDateTime appointmentTime) {
     348        ) && !unavailableSlotRepository.existsByClinicIdAndDateTime(clinic.getClinicId(), appointmentTime);
     349    }
     350
     351    private boolean isValidWorkingSlot(VetClinic clinic, LocalDateTime appointmentTime) {
     352        requireCompleteSchedule(clinic);
    347353        LocalTime time = appointmentTime.toLocalTime();
    348         return !appointmentTime.isBefore(LocalDateTime.now())
    349             && !time.isBefore(CLINIC_DAY_START)
    350             && time.isBefore(CLINIC_DAY_END)
    351             && appointmentTime.getMinute() % SLOT_MINUTES == 0
     354        long minutesFromStart = java.time.Duration.between(clinic.getStartTime(), time).toMinutes();
     355
     356        return isWorkingDay(clinic, appointmentTime.toLocalDate())
     357            && !appointmentTime.isBefore(LocalDateTime.now())
     358            && !time.isBefore(clinic.getStartTime())
     359            && time.isBefore(clinic.getEndTime())
     360            && minutesFromStart >= 0
     361            && minutesFromStart % SLOT_MINUTES == 0
    352362            && appointmentTime.getSecond() == 0
    353363            && appointmentTime.getNano() == 0;
     364    }
     365
     366    private void requireCompleteSchedule(VetClinic clinic) {
     367        if (clinic.getWorkDays() == null || clinic.getWorkDays().isBlank()
     368            || clinic.getStartTime() == null
     369            || clinic.getEndTime() == null) {
     370            throw new RuntimeException("This clinic has not added working days and hours yet");
     371        }
     372
     373        if (!clinic.getStartTime().isBefore(clinic.getEndTime())) {
     374            throw new RuntimeException("Clinic working hours are invalid");
     375        }
     376    }
     377
     378    private boolean isWorkingDay(VetClinic clinic, LocalDate date) {
     379        String day = date.getDayOfWeek().name();
     380        return Arrays.stream(clinic.getWorkDays().split(","))
     381            .map(String::trim)
     382            .map(String::toUpperCase)
     383            .anyMatch(day::equals);
    354384    }
    355385
  • petify-backend/src/main/java/com/petify/petify/service/ReviewService.java

    rae83647 rf6ed6e4  
    1919
    2020import java.time.LocalDateTime;
     21import java.util.Set;
    2122import java.util.List;
    2223import java.util.stream.Collectors;
     
    2627
    2728    private static final Logger logger = LoggerFactory.getLogger(ReviewService.class);
     29    private static final Set<String> USER_REVIEW_INTERACTION_TYPES = Set.of(
     30            "EVENT",
     31            "PERSONAL_INTERACTION",
     32            "ONLINE",
     33            "PHONE_CALL",
     34            "OTHER"
     35    );
    2836
    2937    private final ReviewRepository reviewRepository;
     
    5866    public ReviewDTO createReview(Long reviewerId, Long targetUserId, CreateReviewRequest request) {
    5967        logger.info("==== START createReview ====");
    60         // Validate rating
    61         if (request.getRating() == null || request.getRating() < 1 || request.getRating() > 5) {
    62             logger.error(" VALIDATION FAILED: Invalid rating: {}", request.getRating());
    63             throw new RuntimeException("Rating must be between 1 and 5");
    64         }
     68        validateReviewRequest(request);
     69        String interactionType = normalizeUserReviewInteractionType(request.getInteractionType());
    6570
    6671
     
    118123
    119124        userReview.setTargetUserId(targetUserId);
     125        userReview.setInteractionType(interactionType);
    120126
    121127        // Save UserReview to database with flush
     
    285291    }
    286292
     293    private String normalizeUserReviewInteractionType(String interactionType) {
     294        if (interactionType == null || interactionType.isBlank()) {
     295            throw new RuntimeException("Please choose how you interacted with this user");
     296        }
     297
     298        String normalized = interactionType.trim().toUpperCase();
     299        if (!USER_REVIEW_INTERACTION_TYPES.contains(normalized)) {
     300            throw new RuntimeException("Interaction type must be one of: EVENT, PERSONAL_INTERACTION, ONLINE, PHONE_CALL, OTHER");
     301        }
     302
     303        return normalized;
     304    }
     305
    287306    private User getReviewer(Long reviewerId) {
    288307        return userRepository.findById(reviewerId)
Note: See TracChangeset for help on using the changeset viewer.