Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ems-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>
<groupId>com.example</groupId>
<artifactId>ems-common</artifactId>
<version>1.1.7</version>
<version>1.1.8</version>
<name>ems-common</name>
<description>Common module for Event Management System</description>
<url/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.ems_common.exceptions;

public class EventAlreadyEndedException extends RuntimeException {
public EventAlreadyEndedException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public ResponseEntity<ErrorResponseDto> handleForbiddenException(RuntimeExceptio

@ExceptionHandler({CannotJoinOwnEventException.class})
public ResponseEntity<ErrorResponseDto> handleCannotJoinOwnEventException(CannotJoinOwnEventException ex,
HttpServletRequest request) {
HttpServletRequest request) {
ErrorResponseDto error = new ErrorResponseDto(
"CANNOT_JOIN_OWN_EVENT",
ex.getMessage(),
Expand All @@ -102,6 +102,21 @@ public ResponseEntity<ErrorResponseDto> handleCannotJoinOwnEventException(Cannot
.body(error);
}

@ExceptionHandler({EventAlreadyEndedException.class})
public ResponseEntity<ErrorResponseDto> handleEventAlreadyEndedException(EventAlreadyEndedException ex,
HttpServletRequest request) {
ErrorResponseDto error = new ErrorResponseDto(
"EVENT_ALREADY_ENDED",
ex.getMessage(),
HttpStatus.BAD_REQUEST.value(),
LocalDateTime.now(),
request.getRequestURI()
);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(error);
}

@ExceptionHandler({TooManyRequestsException.class})
public ResponseEntity<ErrorResponseDto> handleTooManyRequestsException(TooManyRequestsException ex,
HttpServletRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
package com.example.event_service_app.configs;

import com.example.event_service_app.scheduler.EventReminderJob;
import com.example.event_service_app.scheduler.EventStatusUpdateJob;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfig {

private static final String JOB_NAME = "eventReminderJob";
private static final String JOB_GROUP = "reminderGroup";
private static final String TRIGGER_NAME = "eventReminderTrigger";
private static final String REMINDER_JOB_NAME = "eventReminderJob";
private static final String REMINDER_JOB_GROUP = "reminderGroup";
private static final String REMINDER_TRIGGER_NAME = "eventReminderTrigger";

private static final String STATUS_JOB_NAME = "eventStatusUpdateJob";
private static final String STATUS_JOB_GROUP = "statusGroup";
private static final String STATUS_TRIGGER_NAME = "eventStatusUpdateTrigger";

@Bean
public JobDetail eventReminderJobDetail() {
return JobBuilder.newJob(EventReminderJob.class)
.withIdentity(JOB_NAME, JOB_GROUP)
.withIdentity(REMINDER_JOB_NAME, REMINDER_JOB_GROUP)
.withDescription("24 saat içinde başlayacak etkinlikler için Kafka'ya hatırlatıcı event gönderir")
.storeDurably() // trigger olmasa da silinmesin
.requestRecovery() // node crash sonrası yeniden çalıştır
Expand All @@ -27,13 +32,37 @@ public Trigger eventReminderTrigger(JobDetail eventReminderJobDetail) {
// Her saat başı çalışır
return TriggerBuilder.newTrigger()
.forJob(eventReminderJobDetail)
.withIdentity(TRIGGER_NAME, JOB_GROUP)
.withIdentity(REMINDER_TRIGGER_NAME, REMINDER_JOB_GROUP)
.withDescription("Saatlik reminder trigger")
.withSchedule(
CronScheduleBuilder.cronSchedule("0 0 * * * ?") // her saat başı
.withMisfireHandlingInstructionFireAndProceed()
)
.build();
}

@Bean
public JobDetail eventStatusUpdateJobDetail() {
return JobBuilder.newJob(EventStatusUpdateJob.class)
.withIdentity(STATUS_JOB_NAME, STATUS_JOB_GROUP)
.withDescription("Event status'lerini otomatik olarak günceller (UPCOMING -> ONGOING -> COMPLETED)")
.storeDurably()
.requestRecovery()
.build();
}

@Bean
public Trigger eventStatusUpdateTrigger(JobDetail eventStatusUpdateJobDetail) {
// Her 5 dakikada bir çalışır
return TriggerBuilder.newTrigger()
.forJob(eventStatusUpdateJobDetail)
.withIdentity(STATUS_TRIGGER_NAME, STATUS_JOB_GROUP)
.withDescription("5 dakikalık event status update trigger")
.withSchedule(
CronScheduleBuilder.cronSchedule("0 0/5 * * * ?") // her 5 dakika
.withMisfireHandlingInstructionFireAndProceed()
)
.build();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@
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.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.List;

public interface EventRepository extends JpaRepository<Event, Long> {
Page<Event> findByCategoryId(Long categoryId, Pageable pageable);
Page<Event> findByCityIgnoreCase(String city, Pageable pageable);
Page<Event> findByStartDateBetween(LocalDateTime start, LocalDateTime end, Pageable pageable);
Page<Event> findByStatus(EventStatus status, Pageable pageable);
Page<Event> findByOwnerId(Long ownerId, Pageable pageable);

@Query("SELECT e FROM Event e WHERE e.status = :status AND e.endDate < :now")
List<Event> findByStatusAndEndDateBefore(@Param("status") EventStatus status, @Param("now") LocalDateTime now);

@Query("SELECT e FROM Event e WHERE e.status = :status AND e.startDate <= :now AND e.endDate >= :now")
List<Event> findByStatusAndStartDateBeforeAndEndDateAfter(@Param("status") EventStatus status, @Param("now") LocalDateTime now);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.example.event_service_app.scheduler;

import com.example.event_service_app.entity.Event;
import com.example.event_service_app.repository.EventRepository;
import com.example.event_service_client.enums.EventStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
@DisallowConcurrentExecution
public class EventStatusUpdateJob extends QuartzJobBean {

private final EventRepository eventRepository;

@Override
@Transactional
protected void executeInternal(JobExecutionContext context) {
LocalDateTime now = LocalDateTime.now();

// UPCOMING -> ONGOING (startDate <= now < endDate)
List<Event> upcomingToOngoing = eventRepository.findByStatusAndStartDateBeforeAndEndDateAfter(
EventStatus.UPCOMING, now);

for (Event event : upcomingToOngoing) {
event.setStatus(EventStatus.ONGOING);
eventRepository.save(event);
log.info("[EventStatusUpdateJob] Event {} status updated from UPCOMING to ONGOING", event.getId());
}

// ONGOING -> COMPLETED (endDate < now)
List<Event> ongoingToCompleted = eventRepository.findByStatusAndEndDateBefore(
EventStatus.ONGOING, now);

for (Event event : ongoingToCompleted) {
event.setStatus(EventStatus.COMPLETED);
eventRepository.save(event);
log.info("[EventStatusUpdateJob] Event {} status updated from ONGOING to COMPLETED", event.getId());
}

// UPCOMING -> COMPLETED (endDate < now, missed ONGOING window)
List<Event> upcomingToCompleted = eventRepository.findByStatusAndEndDateBefore(
EventStatus.UPCOMING, now);

for (Event event : upcomingToCompleted) {
event.setStatus(EventStatus.COMPLETED);
eventRepository.save(event);
log.info("[EventStatusUpdateJob] Event {} status updated from UPCOMING to COMPLETED", event.getId());
}

int total = upcomingToOngoing.size() + ongoingToCompleted.size() + upcomingToCompleted.size();
log.info("[EventStatusUpdateJob] Total {} events updated", total);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.example.ems_common.dto.NotificationEventType;
import com.example.ems_common.exceptions.AlreadyExistsException;
import com.example.ems_common.exceptions.CannotJoinOwnEventException;
import com.example.ems_common.exceptions.EventAlreadyEndedException;
import com.example.ems_common.exceptions.NotFoundException;
import com.example.event_service_client.enums.EventStatus;
import com.example.ems_common.security.SecurityUtils;
import com.example.event_service_app.entity.Event;
import com.example.event_service_app.entity.OutboxEvent;
Expand Down Expand Up @@ -48,6 +50,12 @@ public ParticipationResponseDto register(ParticipationCreateDto dto) {
throw new CannotJoinOwnEventException("You cannot join an event that you have created");
}

if (event.getEndDate().isBefore(LocalDateTime.now()) ||
event.getStatus() == EventStatus.COMPLETED ||
event.getStatus() == EventStatus.CANCELLED) {
throw new EventAlreadyEndedException("Event has already ended or been cancelled");
}

if (participationRepository.existsByEventIdAndParticipantId(dto.getEventId(), currentUserId)) {
throw new AlreadyExistsException("Already registered for this event");
}
Expand Down