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: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
implementation("software.amazon.awssdk:s3")

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
Expand All @@ -46,6 +47,7 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
implementation 'com.auth0:java-jwt:4.3.0'
implementation 'com.github.jknack:handlebars:4.4.0'


compileOnly 'com.google.code.findbugs:jsr305:3.0.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import app.teamwize.api.event.model.EventPayload;
import app.teamwize.api.event.model.EventType;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.Map;

@Schema(name = "ORGANIZATION_CREATED", description = "Organization created event")
public record OrganizationCreatedEvent(UserEventPayload user,
OrganizationEventPayload organization) implements EventPayload {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@ public record UserEventPayload(
Long id,
UserRole role,
String email,
String password,
String phone,
String firstName,
String lastName,
String country,
String timezone) {
String timezone,
Long teamId,
Long organizationId
) {

public UserEventPayload(User user) {
this(user.getId(),
user.getRole(),
user.getEmail(),
user.getPassword(),
user.getPhone(),
user.getFirstName(),
user.getLastName(),
user.getCountry(),
user.getTimezone()
user.getTimezone(),
user.getTeam().getId(),
user.getOrganization().getId()
);
}
}
14 changes: 0 additions & 14 deletions src/main/java/app/teamwize/api/base/error/ApiErrorItem.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ public class EventEntity extends BaseAuditEntity {
@JoinColumn(name = "organization_id")
private Organization organization;

@OneToMany(mappedBy = "event")
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<EventExecutionEntity> executions;
}
3 changes: 3 additions & 0 deletions src/main/java/app/teamwize/api/event/model/Event.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package app.teamwize.api.event.model;

import app.teamwize.api.organization.domain.entity.Organization;

import java.time.Instant;
import java.util.Map;

public record Event(
Long id,
EventType type,
Organization organization,
Map<String, Object> params,
EventStatus status,
Byte maxAttempts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public enum EventExitCode {

HANDLER_NOT_FOUND,// No appropriate handler was found for the event type; event could not be processed due to lack of support

PROCESSING_ERROR, // An error occurred during event processing that prevented successful completion
ERROR, // An error occurred during event processing that prevented successful completion

CONNECTION_ERROR, // Failed to connect to an external service required for event processing (e.g., email/SMS server)

Expand Down
30 changes: 25 additions & 5 deletions src/main/java/app/teamwize/api/event/model/EventType.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
package app.teamwize.api.event.model;

import app.teamwize.api.auth.domain.event.OrganizationCreatedEvent;
import app.teamwize.api.leave.model.event.LeaveCreatedEvent;
import app.teamwize.api.leave.model.event.LeaveStatusUpdatedEvent;
import app.teamwize.api.notification.model.NotificationTriggerReceptors;
import app.teamwize.api.notification.model.event.NotificationCreatedEvent;
import app.teamwize.api.team.domain.event.TeamCreatedEvent;
import app.teamwize.api.user.domain.event.UserInvitedEvent;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.util.List;

import static app.teamwize.api.notification.model.NotificationTriggerReceptors.*;

@RequiredArgsConstructor
@Getter
public enum EventType {
ORGANIZATION_CREATED,
USER_CREATED,
LEAVE_CREATED,
LEAVE_STATUS_UPDATED,
TEAM_CREATED,
ORGANIZATION_CREATED(OrganizationCreatedEvent.class, List.of(USER, ORGANIZATION_ADMIN)),
USER_CREATED(UserInvitedEvent.class, List.of(USER, TEAM_ADMIN, ORGANIZATION_ADMIN)),
LEAVE_CREATED(LeaveCreatedEvent.class, List.of(USER, TEAM_ADMIN, ORGANIZATION_ADMIN)),
LEAVE_STATUS_UPDATED(LeaveStatusUpdatedEvent.class, List.of(USER, TEAM_ADMIN, ORGANIZATION_ADMIN)),
TEAM_CREATED(TeamCreatedEvent.class, List.of(USER, TEAM_ADMIN, ORGANIZATION_ADMIN, ALL_TEAM_MEMBERS)),
NOTIFICATION_CREATED(NotificationCreatedEvent.class, List.of(USER, TEAM_ADMIN, ORGANIZATION_ADMIN, ALL_TEAM_MEMBERS));

private final Class<? extends EventPayload> payloadType;
private final List<NotificationTriggerReceptors> supportedReceptors;
}
25 changes: 17 additions & 8 deletions src/main/java/app/teamwize/api/event/service/EventService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import app.teamwize.api.event.service.handler.EventHandler;
import app.teamwize.api.organization.domain.entity.Organization;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Scheduled;
Expand All @@ -28,6 +29,7 @@ public class EventService {

private final EventRepository eventRepository;
private final EventExecutionRepository executionRepository;
@Lazy
private final List<EventHandler> eventHandlers;
private final EventMapper eventMapper;

Expand All @@ -36,15 +38,19 @@ public class EventService {
public Event emmit(Long organizationId, EventType eventType, Map<String, Object> params, byte maxAttempts, Instant scheduledAt) {
var executions = eventHandlers.stream().filter(eventHandler -> eventHandler.accepts(eventType)).map(eventHandler -> new EventExecutionEntity()
.setStatus(EventExecutionStatus.PENDING)
.setAttempts(0)
.setHandler(eventHandler.name())).toList();
var event = new EventEntity()
.setOrganization(new Organization(organizationId))
.setType(eventType)
.setParams(Map.of())
.setParams(params)
.setStatus(EventStatus.PENDING)
.setMaxAttempts(maxAttempts)
.setScheduledAt(scheduledAt)
.setExecutions(executions);

executions.forEach(eventExecutionEntity -> eventExecutionEntity.setEvent(event));

return eventMapper.toEvent(eventRepository.persist(event));
}

Expand All @@ -53,20 +59,17 @@ public Event emmit(Long organizationId, EventPayload eventPayload) {
return emmit(organizationId, eventPayload.name(), eventPayload.payload(), (byte) 3, Instant.now());
}


// Maybe it's better to try forever to execute an event
// There is no need to have exitCode for events they are not jobs

@Transactional
@Scheduled(fixedDelay = 60_000)
@Scheduled(fixedDelay = 1_000)
public void processEvents() {
var pendingEvents = eventRepository.findByStatus(EventStatus.PENDING);
for (var pendingEvent : pendingEvents) {
for (var execution : pendingEvent.getExecutions()) {
var pendingExecutions = pendingEvent.getExecutions().stream().filter(execution -> execution.getStatus() == EventExecutionStatus.PENDING || execution.getStatus() == EventExecutionStatus.RETRYING).toList();
for (var execution : pendingExecutions) {
var handlerOptional = eventHandlers.stream().filter(eventHandler -> eventHandler.name().equals(execution.getHandler())).findFirst();
if (handlerOptional.isEmpty()) continue;
var handler = handlerOptional.get();
var executionResult = handler.process(pendingEvent);
var executionResult = handler.process(eventMapper.toEvent(pendingEvent));
if (executionResult.exitCode() == EventExitCode.SUCCESS) {
execution.setStatus(EventExecutionStatus.FINISHED);
} else {
Expand All @@ -82,7 +85,13 @@ public void processEvents() {
}
executionRepository.update(execution);
}
if (pendingExecutions.stream().allMatch(execution -> execution.getStatus() == EventExecutionStatus.FINISHED)) {
pendingEvent.setStatus(EventStatus.FINISHED);
} else {
pendingEvent.setStatus(EventStatus.PENDING);
}
}
eventRepository.updateAll(pendingEvents);
}

public Paged<Event> getEvents(Pagination pagination) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package app.teamwize.api.event.service.handler;

import app.teamwize.api.event.entity.EventEntity;
import app.teamwize.api.event.model.Event;
import app.teamwize.api.event.model.EventExitCode;
import app.teamwize.api.event.model.EventType;

Expand All @@ -11,8 +11,7 @@ public interface EventHandler {

boolean accepts(EventType type);

EventExecutionResult process(EventEntity eventEntity);

EventExecutionResult process(Event eventEntity);

record EventExecutionResult(EventExitCode exitCode, Map<String, Object> metadata) {
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import app.teamwize.api.auth.domain.event.UserEventPayload;
import app.teamwize.api.event.model.EventPayload;
import app.teamwize.api.event.model.EventType;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.Map;

@Schema(name = "LEAVE_CREATED", description = "Leave created event")
public record LeaveCreatedEvent(LeaveEventPayload leave, UserEventPayload user) implements EventPayload {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import app.teamwize.api.leave.model.LeaveStatus;
import app.teamwize.api.leave.model.entity.Leave;
import app.teamwize.api.leave.model.entity.LeavePolicyActivatedType;

import java.time.Instant;

Expand All @@ -14,7 +13,7 @@ public record LeaveEventPayload(

LeaveStatus status,

LeavePolicyActivatedType type,
LeavePolicyActivatedTypePayload type,

String reason,

Expand All @@ -26,7 +25,7 @@ public LeaveEventPayload(Leave leave) {
leave.getStartAt(),
leave.getEndAt(),
leave.getStatus(),
leave.getActivatedType(),
new LeavePolicyActivatedTypePayload(leave.getActivatedType()),
leave.getReason(),
leave.getDuration()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package app.teamwize.api.leave.model.event;

import app.teamwize.api.leave.model.entity.LeavePolicyActivatedType;

public record LeavePolicyActivatedTypePayload(
String name,
Boolean requiresApproval,
Integer amount,
Long typeId,
Long policyId,
String policyName) {

public LeavePolicyActivatedTypePayload(LeavePolicyActivatedType leavePolicyActivatedType) {
this(leavePolicyActivatedType.getType().getName(),
leavePolicyActivatedType.getRequiresApproval(),
leavePolicyActivatedType.getAmount(),
leavePolicyActivatedType.getId().getTypeId(),
leavePolicyActivatedType.getId().getPolicyId(),
leavePolicyActivatedType.getPolicy().getName()
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import app.teamwize.api.auth.domain.event.UserEventPayload;
import app.teamwize.api.event.model.EventPayload;
import app.teamwize.api.event.model.EventType;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.Map;

@Schema(name = "LEAVE_STATUS_UPDATED", description = "Leave status updated event")
public record LeaveStatusUpdatedEvent(LeaveEventPayload leave, UserEventPayload user) implements EventPayload {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import app.teamwize.api.leave.model.command.LeaveUpdateCommand;
import app.teamwize.api.leave.model.entity.Leave;
import app.teamwize.api.leave.model.entity.LeavePolicyActivatedTypeId;
import app.teamwize.api.leave.model.event.LeaveCreatedEvent;
import app.teamwize.api.leave.model.event.LeaveEventPayload;
import app.teamwize.api.leave.model.event.LeaveStatusUpdatedEvent;
import app.teamwize.api.leave.repository.LeaveRepository;
Expand Down Expand Up @@ -83,7 +84,7 @@ public Leave createLeave(Long organizationId, Long userId, LeaveCreateCommand co
dayOff = leaveRepository.persist(dayOff);


// eventService.emmit(organizationId, new LeaveCreatedEvent(new LeaveEventPayload(dayOff), new UserEventPayload(user)));
eventService.emmit(organizationId, new LeaveCreatedEvent(new LeaveEventPayload(dayOff), new UserEventPayload(user)));

return dayOff;
}
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/app/teamwize/api/notification/config/MailConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package app.teamwize.api.notification.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;


@Configuration
public class MailConfig {

@Bean
public JavaMailSender getJavaMailSender(org.springframework.core.env.Environment env) {
var mailSender = new JavaMailSenderImpl();
mailSender.setHost(env.getProperty("spring.mail.host"));
mailSender.setPort(Integer.parseInt(env.getProperty("spring.mail.port", "587")));
mailSender.setUsername(env.getProperty("spring.mail.username"));
mailSender.setPassword(env.getProperty("spring.mail.password"));

var props = mailSender.getJavaMailProperties();
props.put("mail.smtp.auth", env.getProperty("spring.mail.properties.mail.smtp.auth"));
props.put("mail.smtp.starttls.enable", env.getProperty("spring.mail.properties.mail.smtp.starttls.enable"));
props.put("mail.debug", "false"); // Set to true for debugging
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.from", env.getProperty("spring.mail.username"));
props.put("mail.smtp.connectiontimeout", "5000");
props.put("mail.smtp.timeout", "5000");
props.put("mail.smtp.writetimeout", "5000");
return mailSender;
}
}
Loading
Loading