mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2026-02-04 21:31:11 +00:00
changes:
1.add appointment feature; 2.add new api for creation,query and cancel. 3.add new table for appointment.
This commit is contained in:
parent
ab1d5364a0
commit
e47a2e2f56
12 changed files with 773 additions and 166 deletions
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.samples.petclinic.model.BaseEntity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import jakarta.validation.constraints.Future;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Simple JavaBean domain object representing an appointment.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "appointments")
|
||||
public class Appointment extends BaseEntity {
|
||||
|
||||
public enum Status {
|
||||
|
||||
SCHEDULED, CANCELLED, COMPLETED
|
||||
|
||||
}
|
||||
|
||||
@Column(name = "pet_id")
|
||||
@NotNull
|
||||
private Integer petId;
|
||||
|
||||
@Column(name = "owner_id")
|
||||
@NotNull
|
||||
private Integer ownerId;
|
||||
|
||||
@Column(name = "start_time")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
@NotNull
|
||||
@Future
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Column(name = "end_time")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
@NotNull
|
||||
@Future
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@NotNull
|
||||
private Status status = Status.SCHEDULED;
|
||||
|
||||
@Column(name = "created_at")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
@Version
|
||||
private Integer version;
|
||||
|
||||
public Integer getPetId() {
|
||||
return petId;
|
||||
}
|
||||
|
||||
public void setPetId(Integer petId) {
|
||||
this.petId = petId;
|
||||
}
|
||||
|
||||
public Integer getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
public void setOwnerId(Integer ownerId) {
|
||||
this.ownerId = ownerId;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(LocalDateTime endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Integer getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Integer version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* REST controller for appointment management.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/appointments")
|
||||
public class AppointmentController {
|
||||
|
||||
private final AppointmentService appointmentService;
|
||||
|
||||
public AppointmentController(AppointmentService appointmentService) {
|
||||
this.appointmentService = appointmentService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Appointment> createAppointment(@Valid @RequestBody AppointmentDTO dto) {
|
||||
// In a real application, you would get ownerId from authentication
|
||||
Integer ownerId = 1; // Hardcoded for demo purposes
|
||||
try {
|
||||
Appointment appointment = appointmentService.createAppointment(dto, ownerId);
|
||||
return new ResponseEntity<>(appointment, HttpStatus.CREATED);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
return new ResponseEntity<>(HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<Appointment>> getAppointments(@RequestParam(required = false) Integer petId,
|
||||
@RequestParam(required = false) Integer ownerId,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime from,
|
||||
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime to,
|
||||
Pageable pageable) {
|
||||
Page<Appointment> appointments = appointmentService.getAppointments(petId, ownerId, from, to, pageable);
|
||||
return new ResponseEntity<>(appointments, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> cancelAppointment(@PathVariable Integer id) {
|
||||
try {
|
||||
appointmentService.cancelAppointment(id);
|
||||
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
return new ResponseEntity<>(HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import jakarta.validation.constraints.Future;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* DTO for creating and updating appointments.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
public class AppointmentDTO {
|
||||
|
||||
@NotNull
|
||||
private Integer petId;
|
||||
|
||||
@NotNull
|
||||
@Future
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@NotNull
|
||||
@Future
|
||||
private LocalDateTime endTime;
|
||||
|
||||
public Integer getPetId() {
|
||||
return petId;
|
||||
}
|
||||
|
||||
public void setPetId(Integer petId) {
|
||||
this.petId = petId;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(LocalDateTime endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Mapper for converting between Appointment entity and DTO.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
@Component
|
||||
public class AppointmentMapper {
|
||||
|
||||
public Appointment toEntity(AppointmentDTO dto, Integer ownerId) {
|
||||
Appointment appointment = new Appointment();
|
||||
appointment.setPetId(dto.getPetId());
|
||||
appointment.setOwnerId(ownerId);
|
||||
appointment.setStartTime(dto.getStartTime());
|
||||
appointment.setEndTime(dto.getEndTime());
|
||||
appointment.setStatus(Appointment.Status.SCHEDULED);
|
||||
appointment.setCreatedAt(LocalDateTime.now());
|
||||
return appointment;
|
||||
}
|
||||
|
||||
public AppointmentDTO toDTO(Appointment appointment) {
|
||||
AppointmentDTO dto = new AppointmentDTO();
|
||||
dto.setPetId(appointment.getPetId());
|
||||
dto.setStartTime(appointment.getStartTime());
|
||||
dto.setEndTime(appointment.getEndTime());
|
||||
return dto;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import jakarta.persistence.LockModeType;
|
||||
|
||||
/**
|
||||
* Repository interface for Appointment entities.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
@Repository
|
||||
public interface AppointmentRepository extends JpaRepository<Appointment, Integer> {
|
||||
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("SELECT a FROM Appointment a WHERE a.petId = :petId AND a.status = 'SCHEDULED' AND "
|
||||
+ "((a.startTime < :endTime AND a.endTime > :startTime))")
|
||||
Page<Appointment> findOverlappingAppointments(@Param("petId") Integer petId,
|
||||
@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, Pageable pageable);
|
||||
|
||||
@Query("SELECT a FROM Appointment a WHERE (a.petId = :petId OR :petId IS NULL) AND "
|
||||
+ "(a.ownerId = :ownerId OR :ownerId IS NULL) AND " + "(a.startTime >= :from OR :from IS NULL) AND "
|
||||
+ "(a.endTime <= :to OR :to IS NULL)")
|
||||
Page<Appointment> findByFilters(@Param("petId") Integer petId, @Param("ownerId") Integer ownerId,
|
||||
@Param("from") LocalDateTime from, @Param("to") LocalDateTime to, Pageable pageable);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* Service layer for appointment management.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class AppointmentService {
|
||||
|
||||
private final AppointmentRepository appointmentRepository;
|
||||
|
||||
private final AppointmentMapper appointmentMapper;
|
||||
|
||||
public AppointmentService(AppointmentRepository appointmentRepository, AppointmentMapper appointmentMapper) {
|
||||
this.appointmentRepository = appointmentRepository;
|
||||
this.appointmentMapper = appointmentMapper;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Appointment createAppointment(@Valid AppointmentDTO dto, Integer ownerId) {
|
||||
// Validate time order
|
||||
if (dto.getStartTime().isAfter(dto.getEndTime()) || dto.getStartTime().isEqual(dto.getEndTime())) {
|
||||
throw new IllegalArgumentException("Start time must be before end time");
|
||||
}
|
||||
|
||||
// Check for overlapping appointments with pessimistic lock
|
||||
Page<Appointment> overlapping = appointmentRepository.findOverlappingAppointments(dto.getPetId(),
|
||||
dto.getStartTime(), dto.getEndTime(), Pageable.unpaged());
|
||||
|
||||
if (!overlapping.isEmpty()) {
|
||||
throw new IllegalStateException("Appointment overlaps with existing scheduled appointment");
|
||||
}
|
||||
|
||||
// Create and save appointment
|
||||
Appointment appointment = appointmentMapper.toEntity(dto, ownerId);
|
||||
return appointmentRepository.save(appointment);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Appointment> getAppointments(Integer petId, Integer ownerId, LocalDateTime from, LocalDateTime to,
|
||||
Pageable pageable) {
|
||||
return appointmentRepository.findByFilters(petId, ownerId, from, to, pageable);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Appointment cancelAppointment(Integer id) {
|
||||
Appointment appointment = appointmentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Appointment not found"));
|
||||
|
||||
if (appointment.getStatus() == Appointment.Status.CANCELLED) {
|
||||
throw new IllegalStateException("Appointment is already cancelled");
|
||||
}
|
||||
|
||||
appointment.setStatus(Appointment.Status.CANCELLED);
|
||||
return appointmentRepository.save(appointment);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.Validator;
|
||||
|
||||
/**
|
||||
* Validator for AppointmentDTO.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
@Component
|
||||
public class AppointmentValidator implements Validator {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return AppointmentDTO.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Object target, Errors errors) {
|
||||
AppointmentDTO dto = (AppointmentDTO) target;
|
||||
|
||||
if (dto.getStartTime() != null && dto.getEndTime() != null) {
|
||||
if (dto.getStartTime().isAfter(dto.getEndTime()) || dto.getStartTime().isEqual(dto.getEndTime())) {
|
||||
errors.rejectValue("startTime", "error.startTime", "Start time must be before end time");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,64 +1,16 @@
|
|||
DROP TABLE vet_specialties IF EXISTS;
|
||||
DROP TABLE vets IF EXISTS;
|
||||
DROP TABLE specialties IF EXISTS;
|
||||
DROP TABLE visits IF EXISTS;
|
||||
DROP TABLE pets IF EXISTS;
|
||||
DROP TABLE types IF EXISTS;
|
||||
DROP TABLE owners IF EXISTS;
|
||||
|
||||
|
||||
CREATE TABLE vets (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
first_name VARCHAR(30),
|
||||
last_name VARCHAR(30)
|
||||
CREATE TABLE IF NOT EXISTS appointments (
|
||||
id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
pet_id INTEGER NOT NULL,
|
||||
owner_id INTEGER NOT NULL,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
FOREIGN KEY (pet_id) REFERENCES pets(id),
|
||||
FOREIGN KEY (owner_id) REFERENCES owners(id)
|
||||
);
|
||||
CREATE INDEX vets_last_name ON vets (last_name);
|
||||
|
||||
CREATE TABLE specialties (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(80)
|
||||
);
|
||||
CREATE INDEX specialties_name ON specialties (name);
|
||||
|
||||
CREATE TABLE vet_specialties (
|
||||
vet_id INTEGER NOT NULL,
|
||||
specialty_id INTEGER NOT NULL
|
||||
);
|
||||
ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_vets FOREIGN KEY (vet_id) REFERENCES vets (id);
|
||||
ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_specialties FOREIGN KEY (specialty_id) REFERENCES specialties (id);
|
||||
|
||||
CREATE TABLE types (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(80)
|
||||
);
|
||||
CREATE INDEX types_name ON types (name);
|
||||
|
||||
CREATE TABLE owners (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
first_name VARCHAR(30),
|
||||
last_name VARCHAR_IGNORECASE(30),
|
||||
address VARCHAR(255),
|
||||
city VARCHAR(80),
|
||||
telephone VARCHAR(20)
|
||||
);
|
||||
CREATE INDEX owners_last_name ON owners (last_name);
|
||||
|
||||
CREATE TABLE pets (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(30),
|
||||
birth_date DATE,
|
||||
type_id INTEGER NOT NULL,
|
||||
owner_id INTEGER
|
||||
);
|
||||
ALTER TABLE pets ADD CONSTRAINT fk_pets_owners FOREIGN KEY (owner_id) REFERENCES owners (id);
|
||||
ALTER TABLE pets ADD CONSTRAINT fk_pets_types FOREIGN KEY (type_id) REFERENCES types (id);
|
||||
CREATE INDEX pets_name ON pets (name);
|
||||
|
||||
CREATE TABLE visits (
|
||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
pet_id INTEGER,
|
||||
visit_date DATE,
|
||||
description VARCHAR(255)
|
||||
);
|
||||
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
|
||||
CREATE INDEX visits_pet_id ON visits (pet_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_pet_id ON appointments(pet_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_start_time ON appointments(start_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_end_time ON appointments(end_time);
|
||||
|
|
@ -1,55 +1,16 @@
|
|||
CREATE TABLE IF NOT EXISTS vets (
|
||||
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
first_name VARCHAR(30),
|
||||
last_name VARCHAR(30),
|
||||
INDEX(last_name)
|
||||
) engine=InnoDB;
|
||||
CREATE TABLE IF NOT EXISTS appointments (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
pet_id INT NOT NULL,
|
||||
owner_id INT NOT NULL,
|
||||
start_time DATETIME NOT NULL,
|
||||
end_time DATETIME NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
version INT NOT NULL,
|
||||
FOREIGN KEY (pet_id) REFERENCES pets(id),
|
||||
FOREIGN KEY (owner_id) REFERENCES owners(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS specialties (
|
||||
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(80),
|
||||
INDEX(name)
|
||||
) engine=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vet_specialties (
|
||||
vet_id INT(4) UNSIGNED NOT NULL,
|
||||
specialty_id INT(4) UNSIGNED NOT NULL,
|
||||
FOREIGN KEY (vet_id) REFERENCES vets(id),
|
||||
FOREIGN KEY (specialty_id) REFERENCES specialties(id),
|
||||
UNIQUE (vet_id,specialty_id)
|
||||
) engine=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS types (
|
||||
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(80),
|
||||
INDEX(name)
|
||||
) engine=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS owners (
|
||||
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
first_name VARCHAR(30),
|
||||
last_name VARCHAR(30),
|
||||
address VARCHAR(255),
|
||||
city VARCHAR(80),
|
||||
telephone VARCHAR(20),
|
||||
INDEX(last_name)
|
||||
) engine=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pets (
|
||||
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(30),
|
||||
birth_date DATE,
|
||||
type_id INT(4) UNSIGNED NOT NULL,
|
||||
owner_id INT(4) UNSIGNED,
|
||||
INDEX(name),
|
||||
FOREIGN KEY (owner_id) REFERENCES owners(id),
|
||||
FOREIGN KEY (type_id) REFERENCES types(id)
|
||||
) engine=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS visits (
|
||||
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
pet_id INT(4) UNSIGNED,
|
||||
visit_date DATE,
|
||||
description VARCHAR(255),
|
||||
FOREIGN KEY (pet_id) REFERENCES pets(id)
|
||||
) engine=InnoDB;
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_pet_id ON appointments(pet_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_start_time ON appointments(start_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_end_time ON appointments(end_time);
|
||||
|
|
@ -1,52 +1,16 @@
|
|||
CREATE TABLE IF NOT EXISTS vets (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
last_name TEXT
|
||||
);
|
||||
CREATE INDEX ON vets (last_name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS specialties (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
CREATE INDEX ON specialties (name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vet_specialties (
|
||||
vet_id INT NOT NULL REFERENCES vets (id),
|
||||
specialty_id INT NOT NULL REFERENCES specialties (id),
|
||||
UNIQUE (vet_id, specialty_id)
|
||||
CREATE TABLE IF NOT EXISTS appointments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
pet_id INTEGER NOT NULL,
|
||||
owner_id INTEGER NOT NULL,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
FOREIGN KEY (pet_id) REFERENCES pets(id),
|
||||
FOREIGN KEY (owner_id) REFERENCES owners(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS types (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
CREATE INDEX ON types (name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS owners (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
address TEXT,
|
||||
city TEXT,
|
||||
telephone TEXT
|
||||
);
|
||||
CREATE INDEX ON owners (last_name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pets (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name TEXT,
|
||||
birth_date DATE,
|
||||
type_id INT NOT NULL REFERENCES types (id),
|
||||
owner_id INT REFERENCES owners (id)
|
||||
);
|
||||
CREATE INDEX ON pets (name);
|
||||
CREATE INDEX ON pets (owner_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS visits (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
pet_id INT REFERENCES pets (id),
|
||||
visit_date DATE,
|
||||
description TEXT
|
||||
);
|
||||
CREATE INDEX ON visits (pet_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_pet_id ON appointments(pet_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_start_time ON appointments(start_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_appointments_end_time ON appointments(end_time);
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Integration tests for AppointmentController.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Transactional
|
||||
public class AppointmentControllerTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
public void shouldCreateAppointment() throws Exception {
|
||||
AppointmentDTO dto = new AppointmentDTO();
|
||||
dto.setPetId(1);
|
||||
dto.setStartTime(LocalDateTime.now().plusDays(1));
|
||||
dto.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1));
|
||||
|
||||
mockMvc
|
||||
.perform(post("/api/appointments").contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(dto)))
|
||||
.andExpect(status().isCreated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCreateOverlappingAppointment() throws Exception {
|
||||
AppointmentDTO dto1 = new AppointmentDTO();
|
||||
dto1.setPetId(1);
|
||||
dto1.setStartTime(LocalDateTime.now().plusDays(1));
|
||||
dto1.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1));
|
||||
|
||||
mockMvc
|
||||
.perform(post("/api/appointments").contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(dto1)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
AppointmentDTO dto2 = new AppointmentDTO();
|
||||
dto2.setPetId(1);
|
||||
dto2.setStartTime(LocalDateTime.now().plusDays(1).plusMinutes(30));
|
||||
dto2.setEndTime(LocalDateTime.now().plusDays(1).plusHours(2));
|
||||
|
||||
mockMvc
|
||||
.perform(post("/api/appointments").contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(dto2)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGetAppointments() throws Exception {
|
||||
mockMvc.perform(get("/api/appointments").param("petId", "1").contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCancelAppointment() throws Exception {
|
||||
AppointmentDTO dto = new AppointmentDTO();
|
||||
dto.setPetId(1);
|
||||
dto.setStartTime(LocalDateTime.now().plusDays(1));
|
||||
dto.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1));
|
||||
|
||||
String response = mockMvc
|
||||
.perform(post("/api/appointments").contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(dto)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
Appointment appointment = objectMapper.readValue(response, Appointment.class);
|
||||
|
||||
mockMvc.perform(delete("/api/appointments/" + appointment.getId()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.samples.petclinic.owner;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Unit tests for AppointmentService.
|
||||
*
|
||||
* @author Your Name
|
||||
*/
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
public class AppointmentServiceTests {
|
||||
|
||||
@Autowired
|
||||
private AppointmentService appointmentService;
|
||||
|
||||
@Test
|
||||
public void shouldCreateAppointment() {
|
||||
AppointmentDTO dto = new AppointmentDTO();
|
||||
dto.setPetId(1);
|
||||
dto.setStartTime(LocalDateTime.now().plusDays(1));
|
||||
dto.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1));
|
||||
|
||||
Appointment appointment = appointmentService.createAppointment(dto, 1);
|
||||
assertTrue(appointment.getId() != null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCreateOverlappingAppointment() {
|
||||
AppointmentDTO dto1 = new AppointmentDTO();
|
||||
dto1.setPetId(1);
|
||||
dto1.setStartTime(LocalDateTime.now().plusDays(1));
|
||||
dto1.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1));
|
||||
appointmentService.createAppointment(dto1, 1);
|
||||
|
||||
AppointmentDTO dto2 = new AppointmentDTO();
|
||||
dto2.setPetId(1);
|
||||
dto2.setStartTime(LocalDateTime.now().plusDays(1).plusMinutes(30));
|
||||
dto2.setEndTime(LocalDateTime.now().plusDays(1).plusHours(2));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> appointmentService.createAppointment(dto2, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleConcurrentCreation() throws InterruptedException {
|
||||
int threadCount = 10;
|
||||
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
|
||||
CountDownLatch latch = new CountDownLatch(threadCount);
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
AppointmentDTO dto = new AppointmentDTO();
|
||||
dto.setPetId(1);
|
||||
dto.setStartTime(LocalDateTime.now().plusDays(1));
|
||||
dto.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1));
|
||||
appointmentService.createAppointment(dto, 1);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Expected for overlapping appointments
|
||||
}
|
||||
finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
latch.await();
|
||||
executorService.shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue