Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.
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
20 changes: 20 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
DATASOURCE_URL=jdbc:mysql://localhost/commercifydb?createDatabaseIfNotExist=true
DATASOURCE_USERNAME=
DATASOURCE_PASSWORD=
STRIPE_SECRET_TEST_KEY=
STRIPE_WEBHOOK_SECRET=
JWT_SECRET_KEY=7581e8477a88733917bc3b48f683a827935a492a0bd976a59429a72f28c71fd3
ADMIN_EMAIL=<a valid email, order notification are sent here>
ADMIN_PASSWORD=commercifyadmin123!
ADMIN_ORDER_DASHBOARD=https://<admin dashboard>/admin/orders
MOBILEPAY_CLIENT_ID=
MOBILEPAY_CLIENT_SECRET=
MOBILEPAY_SUBSCRIPTION_KEY=
MOBILEPAY_MERCHANT_ID=
MOBILEPAY_API_URL=
MOBILEPAY_SYSTEM_NAME=
MOBILEPAY_WEBHOOK_CALLBACK=https://<publicdomain>/api/v2/payments/webhooks/mobilepay/callback
MAIL_HOST=smtp.gmail.com
MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
return new BCryptPasswordEncoder(15);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.zenfulcode.commercify.order.application.events;

import com.zenfulcode.commercify.order.application.service.OrderApplicationService;
import com.zenfulcode.commercify.order.domain.event.OrderStatusChangedEvent;
import com.zenfulcode.commercify.order.domain.model.Order;
import com.zenfulcode.commercify.order.infrastructure.notification.OrderEmailNotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Component
@RequiredArgsConstructor
public class OrderEmailHandler {
private final OrderApplicationService orderService;
private final OrderEmailNotificationService notificationService;

// @Async
// @EventListener
// @Transactional(readOnly = true)
// public void handleOrderCreated(OrderCreatedEvent event) {
// try {
// log.info("Sending order confirmation notification for order: {}", event.getOrderId());
// Order order = orderService.getOrderById(event.getOrderId());
// notificationService.sendOrderConfirmation(order);
// } catch (Exception e) {
// log.error("Failed to send order confirmation notification", e);
// }
// }

@Async
@EventListener
@Transactional(readOnly = true)
public void handleOrderStatusChanged(OrderStatusChangedEvent event) {
log.info("Sending email confirmation notification for order: {}", event.getOrderId());
try {
Order order = orderService.getOrderById(event.getOrderId());

if (event.isPaidTransition()) {
log.info("Sending order confirmation email for order: {}", order.getId());
notificationService.sendOrderConfirmation(order);
notificationService.notifyAdminNewOrder(order);

} else if (event.isShippingTransition()) {
log.info("Sending shipping confirmation email for order: {}", order.getId());
notificationService.sendShippingConfirmation(order);
} else if (event.isCompletedTransition()) {
log.info("Sending order status update email for order: {}", order.getId());
notificationService.sendOrderStatusUpdate(order);
}
} catch (Exception e) {
log.error("Failed to send order status update notification", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public String getEventType() {
return "ORDER_STATUS_CHANGED";
}

public boolean isPaidTransition() {
return newStatus == OrderStatus.PAID;
}

public boolean isCompletedTransition() {
return newStatus == OrderStatus.COMPLETED;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ public static OrderShippingInfo create(
return info;
}

public String getCustomerName() {
return customerFirstName + " " + customerLastName;
}

public CustomerDetails toCustomerDetails() {
return new CustomerDetails(
customerFirstName,
Expand All @@ -113,7 +117,7 @@ public Address toShippingAddress() {

public Address toBillingAddress() {
if (!hasBillingAddress()) {
return null;
return toShippingAddress();
}
return new Address(
billingStreet,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.zenfulcode.commercify.order.domain.service;

import com.zenfulcode.commercify.order.domain.model.Order;

public interface OrderNotificationService {
void sendOrderConfirmation(Order order);

void sendOrderStatusUpdate(Order order);

void sendShippingConfirmation(Order order);

void notifyAdminNewOrder(Order order);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.zenfulcode.commercify.order.infrastructure.notification;

import com.zenfulcode.commercify.order.domain.model.Order;
import com.zenfulcode.commercify.order.domain.model.OrderLine;
import com.zenfulcode.commercify.order.domain.service.OrderNotificationService;
import com.zenfulcode.commercify.shared.domain.exception.EmailSendingException;
import com.zenfulcode.commercify.shared.infrastructure.service.EmailService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

@Service
@Slf4j
@RequiredArgsConstructor
public class OrderEmailNotificationService implements OrderNotificationService {
private final EmailService emailService;
private final TemplateEngine templateEngine;

@Value("${admin.email}")
private String adminEmail;

@Value("${admin.order-dashboard")
private String orderDashboard;

private static final String ORDER_CONFIRMATION_TEMPLATE = "order/confirmation-email";
private static final String ORDER_STATUS_UPDATE_TEMPLATE = "order/status-update-email";
private static final String ORDER_SHIPPING_TEMPLATE = "order/shipping-email";
private static final String ADMIN_ORDER_TEMPLATE = "order/admin-order-notification";

@Override
public void sendOrderConfirmation(Order order) {
try {
Context context = createOrderContext(order);
String emailContent = templateEngine.process(ORDER_CONFIRMATION_TEMPLATE, context);
emailService.sendEmail(
order.getOrderShippingInfo().toCustomerDetails().email(),
"Order Confirmation - #" + order.getId(),
emailContent, true
);
log.info("Order confirmation email sent for order: {}", order.getId());
} catch (Exception e) {
log.error("Failed to send order confirmation email", e);
throw new EmailSendingException("Failed to send order confirmation email: " + e.getMessage());
}
}

@Override
public void sendOrderStatusUpdate(Order order) {
try {
Context context = createOrderContext(order);
String emailContent = templateEngine.process(ORDER_STATUS_UPDATE_TEMPLATE, context);
emailService.sendEmail(
order.getOrderShippingInfo().toCustomerDetails().email(),
"Order Status Update - #" + order.getId(),
emailContent, true
);
log.info("Order status update email sent for order: {}", order.getId());
} catch (Exception e) {
log.error("Failed to send order status update email", e);
throw new EmailSendingException("Failed to send order status update email: " + e.getMessage());
}
}

@Override
public void sendShippingConfirmation(Order order) {
try {
Context context = createOrderContext(order);
String emailContent = templateEngine.process(ORDER_SHIPPING_TEMPLATE, context);
emailService.sendEmail(
order.getOrderShippingInfo().toCustomerDetails().email(),
"Order Shipped - #" + order.getId(),
emailContent, true
);
log.info("Order shipping confirmation email sent for order: {}", order.getId());
} catch (Exception e) {
log.error("Failed to send shipping confirmation email", e);
throw new EmailSendingException("Failed to send shipping confirmation email: " + e.getMessage());
}
}

@Override
public void notifyAdminNewOrder(Order order) {
try {
Context context = createAdminOrderContext(order);
String emailContent = templateEngine.process(ADMIN_ORDER_TEMPLATE, context);
emailService.sendEmail(
adminEmail,
"New Order Received - #" + order.getId(),
emailContent, true
);
log.info("Admin notification sent for order: {}", order.getId());
} catch (Exception e) {
log.error("Failed to send admin notification", e);
throw new EmailSendingException("Failed to send admin notification: " + e.getMessage());
}
}

private Context createOrderContext(Order order) {
Context context = new Context(Locale.getDefault());

List<Map<String, Object>> orderItems = order.getOrderLines().stream()
.map(this::createOrderItemMap)
.toList();

Map<String, Object> shippingAddress = new HashMap<>();
shippingAddress.put("city", order.getOrderShippingInfo().toShippingAddress().city());
shippingAddress.put("zipCode", order.getOrderShippingInfo().toShippingAddress().zipCode());
shippingAddress.put("country", order.getOrderShippingInfo().toShippingAddress().country());
shippingAddress.put("street", order.getOrderShippingInfo().toShippingAddress().street());
shippingAddress.put("state", order.getOrderShippingInfo().toShippingAddress().state());
context.setVariable("shippingAddress", shippingAddress);

Map<String, Object> billingAddress = new HashMap<>();
billingAddress.put("city", order.getOrderShippingInfo().toBillingAddress().city());
billingAddress.put("zipCode", order.getOrderShippingInfo().toBillingAddress().zipCode());
billingAddress.put("country", order.getOrderShippingInfo().toBillingAddress().country());
billingAddress.put("street", order.getOrderShippingInfo().toBillingAddress().street());
billingAddress.put("state", order.getOrderShippingInfo().toBillingAddress().state());
context.setVariable("billingAddress", billingAddress);

Map<String, Object> details = new HashMap<>();
details.put("id", order.getId().toString());
details.put("customerName", order.getOrderShippingInfo().getCustomerName());
details.put("customerPhone", order.getOrderShippingInfo().getCustomerPhone());
details.put("customerEmail", order.getOrderShippingInfo().getCustomerEmail());
details.put("orderNumber", order.getId().toString());
details.put("status", order.getStatus().toString());
details.put("createdAt", order.getCreatedAt());
details.put("currency", order.getCurrency());
details.put("totalAmount", order.getTotalAmount().getAmount().doubleValue());
details.put("items", orderItems);
context.setVariable("order", details);
return context;
}

private Context createAdminOrderContext(Order order) {
Context context = createOrderContext(order);
context.setVariable("adminOrderUrl", String.format("%s/%s", orderDashboard, order.getId().toString()));
return context;
}

private Map<String, Object> createOrderItemMap(OrderLine orderLine) {
Map<String, Object> items = new HashMap<>();

items.put("name", orderLine.getProduct().getName());
items.put("variant", orderLine.getProductVariant() != null ?
orderLine.getProductVariant().getSku() : "");
items.put("quantity", orderLine.getQuantity());
items.put("sku", orderLine.getProductVariant() != null ?
orderLine.getProductVariant().getSku() : orderLine.getProduct().getId().toString());
items.put("unitPrice", orderLine.getUnitPrice().getAmount().doubleValue());
items.put("total", orderLine.getTotal().getAmount().doubleValue());
return items;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.zenfulcode.commercify.shared.domain.exception;

public class EmailSendingException extends DomainException {
public EmailSendingException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.zenfulcode.commercify.shared.infrastructure.config;

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

import java.util.Properties;

@Configuration
public class MailConfig {

@Value("${spring.mail.host}")
private String host;

@Value("${spring.mail.port}")
private int port;

@Value("${spring.mail.username}")
private String username;

@Value("${spring.mail.password}")
private String password;

@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;

@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private boolean starttls;

@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);

Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", auth);
props.put("mail.smtp.starttls.enable", starttls);
props.put("mail.debug", "false"); // Set to true for debugging

return mailSender;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.zenfulcode.commercify.shared.infrastructure.service;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender mailSender;

public void sendEmail(String to, String subject, String content, boolean isHtml) throws MessagingException {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");

helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, isHtml);

mailSender.send(mimeMessage);
}
}
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ security.jwt.refresh-token-expiration=86400000
# Admin Configuration
admin.email=${ADMIN_EMAIL}
admin.password=${ADMIN_PASSWORD}
admin.order-dashboard=${ADMIN_ORDER_DASHBOARD}
# MobilePay Configuration
integration.payments.mobilepay.client-id=${MOBILEPAY_CLIENT_ID}
integration.payments.mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID}
Expand Down
Loading