feat: add notification channels for email and webhooks

This commit is contained in:
dualfroz committed 2026-08-27 10:04:29 +02:00
1 parent 959b400f5a
commit aec31a4555
9 files changed
+162

No files matched your search

@@ -0,0 +1,23 @@
package com.slotline.notification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class EmailNotificationChannel implements NotificationChannel {
private static final Logger log = LoggerFactory.getLogger(EmailNotificationChannel.class);
@Override
public String name() {
return "email";
}
@Override
public void deliver(NotificationMessage message) {
log.info("[email] user={} type={} resource='{}' [{} - {}] :: {}",
message.recipientUserId(), message.type(), message.resourceName(),
message.startTime(), message.endTime(), message.summary());
}
}
@@ -0,0 +1,38 @@
package com.slotline.notification;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
@Component
public class HttpWebhookGateway implements WebhookGateway {
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
private final ObjectMapper objectMapper;
public HttpWebhookGateway(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void post(String url, NotificationMessage message) {
try {
String body = objectMapper.writeValueAsString(message);
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(5))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
httpClient.send(request, HttpResponse.BodyHandlers.discarding());
} catch (Exception failure) {
throw new WebhookDeliveryException(url, failure);
}
}
}
@@ -0,0 +1,8 @@
package com.slotline.notification;
public interface NotificationChannel {
String name();
void deliver(NotificationMessage message);
}
@@ -0,0 +1,14 @@
package com.slotline.notification;
import java.time.Instant;
public record NotificationMessage(
NotificationType type,
Long organizationId,
Long recipientUserId,
Long resourceId,
String resourceName,
Instant startTime,
Instant endTime,
String summary) {
}
@@ -0,0 +1,30 @@
package com.slotline.notification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
private final List<NotificationChannel> channels;
public NotificationService(List<NotificationChannel> channels) {
this.channels = channels;
}
public void dispatch(NotificationMessage message) {
for (NotificationChannel channel : channels) {
try {
channel.deliver(message);
} catch (RuntimeException failure) {
log.warn("Notification channel '{}' failed to deliver {} for user {}",
channel.name(), message.type(), message.recipientUserId(), failure);
}
}
}
}
@@ -0,0 +1,7 @@
package com.slotline.notification;
public enum NotificationType {
RESERVATION_CREATED,
RESERVATION_CANCELLED,
WAITLIST_PROMOTED
}
@@ -0,0 +1,8 @@
package com.slotline.notification;
public class WebhookDeliveryException extends RuntimeException {
public WebhookDeliveryException(String url, Throwable cause) {
super("Failed to deliver webhook to " + url, cause);
}
}
@@ -0,0 +1,6 @@
package com.slotline.notification;
public interface WebhookGateway {
void post(String url, NotificationMessage message);
}
@@ -0,0 +1,28 @@
package com.slotline.notification;
import com.slotline.config.SlotlineProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty(prefix = "slotline.notifications.webhook", name = "url")
public class WebhookNotificationChannel implements NotificationChannel {
private final WebhookGateway gateway;
private final String url;
public WebhookNotificationChannel(WebhookGateway gateway, SlotlineProperties properties) {
this.gateway = gateway;
this.url = properties.getNotifications().getWebhook().getUrl();
}
@Override
public String name() {
return "webhook";
}
@Override
public void deliver(NotificationMessage message) {
gateway.post(url, message);
}
}