1.add appointment feature;
2.add new api for creation,query and cancel.
3.add new table for appointment.
This commit is contained in:
Bastriver 2026-01-28 11:39:58 +08:00
parent ab1d5364a0
commit e47a2e2f56
12 changed files with 773 additions and 166 deletions

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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");
}
}
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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());
}
}

View file

@ -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();
}
}