diff --git a/src/main/java/zw/qantra/tm/domain/controllers/TestController.java b/src/main/java/zw/qantra/tm/domain/controllers/TestController.java index 1e1a8d5..8ae9160 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/TestController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/TestController.java @@ -10,7 +10,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.services.EmailService; +import zw.qantra.tm.domain.services.NotificationService; import zw.qantra.tm.domain.services.TransactionService; @RestController @@ -18,7 +18,7 @@ import zw.qantra.tm.domain.services.TransactionService; @CrossOrigin(origins = "*") @RequiredArgsConstructor public class TestController { - private final EmailService emailService; + private final NotificationService notificationService; private final TransactionService transactionService; private static final Logger logger = LoggerFactory.getLogger(TestController.class); @@ -44,7 +44,7 @@ public class TestController { @GetMapping("/hello/{email}") public ResponseEntity helloEndpoint(@PathVariable String email) { - emailService.sendSimpleMessage(email, "Hello", "Hello World!"); + notificationService.sendEmail(email, "Hello", "Hello World!"); return ResponseEntity.ok("Hello World!"); } diff --git a/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsApiResponse.java b/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsApiResponse.java new file mode 100644 index 0000000..52273b0 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsApiResponse.java @@ -0,0 +1,14 @@ +package zw.qantra.tm.domain.dtos.sms; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SmsApiResponse { + private int status; + private String result; +} + diff --git a/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsBatchRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsBatchRequest.java new file mode 100644 index 0000000..f978245 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsBatchRequest.java @@ -0,0 +1,18 @@ +package zw.qantra.tm.domain.dtos.sms; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SmsBatchRequest { + private String batchReference; + private List smsList; +} + diff --git a/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsMessage.java b/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsMessage.java new file mode 100644 index 0000000..19b5fa2 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/sms/SmsMessage.java @@ -0,0 +1,17 @@ +package zw.qantra.tm.domain.dtos.sms; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SmsMessage { + private String reference; + private String message; + private String msisdn; +} + diff --git a/src/main/java/zw/qantra/tm/domain/services/EmailService.java b/src/main/java/zw/qantra/tm/domain/services/EmailService.java index e0784f3..43e7500 100644 --- a/src/main/java/zw/qantra/tm/domain/services/EmailService.java +++ b/src/main/java/zw/qantra/tm/domain/services/EmailService.java @@ -1,19 +1,31 @@ package zw.qantra.tm.domain.services; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +/** + * @deprecated Use {@link NotificationService} instead for all email sending. + * Email functionality has been moved to NotificationService which supports + * pretty HTML templates. + */ +@Deprecated @Component public class EmailService { + private static final Logger log = LoggerFactory.getLogger(EmailService.class); + @Autowired private JavaMailSender emailSender; @Async + @Deprecated public void sendSimpleMessage( String to, String subject, String text) { + log.warn("EmailService.sendSimpleMessage() is deprecated. Use NotificationService instead."); SimpleMailMessage message = new SimpleMailMessage(); message.setFrom("noreply@qantra.co.zw"); message.setTo(to); diff --git a/src/main/java/zw/qantra/tm/domain/services/NotificationService.java b/src/main/java/zw/qantra/tm/domain/services/NotificationService.java new file mode 100644 index 0000000..0112637 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/NotificationService.java @@ -0,0 +1,137 @@ +package zw.qantra.tm.domain.services; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpHeaders; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import jakarta.mail.internet.MimeMessage; +import zw.qantra.tm.domain.dtos.sms.SmsApiResponse; +import zw.qantra.tm.domain.dtos.sms.SmsBatchRequest; +import zw.qantra.tm.domain.dtos.sms.SmsMessage; +import zw.qantra.tm.rest.RestService; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class NotificationService { + private static final Logger logger = LoggerFactory.getLogger(NotificationService.class); + + private final RestService restService; + private final JavaMailSender emailSender; + + @Value("${zss.sms.api.url}") + private String smsApiUrl; + @Value("${zss.sms.api.auth}") + private String smsApiAuth; + @Value("${sms.debug}") + private Boolean smsDebug; + + @Async + public void sendEmail(String to, String subject, String text) { + sendEmailWithHtml(to, subject, text, null); + } + + @Async + public void sendEmailWithHtml(String to, String subject, String htmlContent) { + sendEmailWithHtml(to, subject, null, htmlContent); + } + + @Async + public void sendEmailWithHtml(String to, String subject, String text, String htmlContent) { + try { + MimeMessage message = emailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + helper.setFrom("noreply@qantra.co.zw"); + helper.setTo(to); + helper.setSubject(subject); + + if (htmlContent != null) { + helper.setText(text != null ? text : "", htmlContent); + } else if (text != null) { + helper.setText(text); + } + + emailSender.send(message); + logger.info("Email sent to {} - Subject: {}", to, subject); + } catch (Exception e) { + logger.error("Failed to send email to {}: {}", to, e.getMessage(), e); + } + } + + @Async + public void sendOtpEmail(String to, String username, String otpCode) { + try { + // Load and populate the HTML template + ClassPathResource resource = new ClassPathResource("templates/email-otp.html"); + String htmlTemplate; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) { + htmlTemplate = reader.lines().collect(Collectors.joining("\n")); + } + + // Simple placeholder replacement + String htmlContent = htmlTemplate + .replace("{{username}}", username != null ? username : "") + .replace("{{otpCode}}", otpCode != null ? otpCode : ""); + + MimeMessage message = emailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + helper.setFrom("noreply@qantra.co.zw"); + helper.setTo(to); + helper.setSubject("Velocity Pay verification code"); + helper.setText("Your Velocity Pay verification code is: " + otpCode, htmlContent); + + emailSender.send(message); + logger.info("OTP email sent to {} for user: {}", to, username); + } catch (Exception e) { + logger.error("Failed to send OTP email to {}: {}", to, e.getMessage(), e); + } + } + + @Async + public void sendSms(String to, String message) { + if(smsDebug) { + logger.info("Sending SMS to {}: {}", to, message); + return; + } + try { + SmsMessage smsMessage = SmsMessage.builder() + .reference(UUID.randomUUID().toString()) + .message(message) + .msisdn(to) + .build(); + + SmsBatchRequest batchRequest = SmsBatchRequest.builder() + .batchReference(UUID.randomUUID().toString()) + .smsList(Collections.singletonList(smsMessage)) + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", smsApiAuth); + + SmsApiResponse response = restService.postWithExternalAuth( + smsApiUrl, + batchRequest, + headers, + SmsApiResponse.class + ); + + logger.info("SMS sent to {} - Status: {}, Result: {}", to, response.getStatus(), response.getResult()); + } catch (Exception e) { + logger.error("Failed to send SMS to {}: {}", to, e.getMessage(), e); + } + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java index 72999bd..d38c065 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java @@ -18,7 +18,7 @@ import zw.qantra.tm.domain.dtos.*; import zw.qantra.tm.domain.dtos.erp.VelocityCustomerResponseDto; import zw.qantra.tm.domain.models.Otp; import zw.qantra.tm.domain.models.User; -import zw.qantra.tm.domain.services.EmailService; +import zw.qantra.tm.domain.services.NotificationService; import zw.qantra.tm.domain.services.OtpService; import zw.qantra.tm.domain.services.UserService; import io.nflow.engine.workflow.curated.State; @@ -48,7 +48,7 @@ public class RegistrationWorkflow extends WorkflowDefinition { @Autowired private RestService restService; @Autowired - private EmailService emailService; + private NotificationService notificationService; @Autowired private VelocityPaymentService velocityPaymentService; @@ -117,9 +117,9 @@ public class RegistrationWorkflow extends WorkflowDefinition { // Generate OTP Otp otp = otpService.generateOtp(username, "REGISTRATION"); - String string = String.format("Your Velocity Pay verification code is: %s", otp.getOtp()); - emailService.sendSimpleMessage(email, "Velocity Pay verification code", string); - // LinkedHashMap response = restService.postAs(infobipUrl, string, headers, LinkedHashMap.class); + notificationService.sendOtpEmail(email, username, otp.getOtp()); + notificationService.sendSms(registerRequest.getPhone(), + "Your Velocity Pay verification code is: " + otp.getOtp()); log.info("otp generated successfully: {}", otp.getOtp()); } catch (Exception e) { @@ -142,10 +142,12 @@ public class RegistrationWorkflow extends WorkflowDefinition { public void resendOtp(StateExecution execution) { OtpRequest otpRequest = execution.getVariable("resend", OtpRequest.class); Otp otp = otpService.generateOtp(otpRequest.getUsername(), "REGISTRATION"); - String string = String.format("Your Velocity Pay verification code is: %s", otp.getOtp()); - emailService.sendSimpleMessage(otpRequest.getUsername(), "Velocity Pay verification code", string); + // The username field in OtpRequest may actually contain the email; use it directly + notificationService.sendOtpEmail(otpRequest.getUsername(), otpRequest.getUsername(), otp.getOtp()); + notificationService.sendSms(otpRequest.getPhone(), + "Your Velocity Pay verification code is: " + otp.getOtp()); - log.info("otp generated successfully: {}", otp.getOtp()); + log.info("otp resent successfully: {}", otp.getOtp()); } public NextAction otpSuccess(StateExecution execution) { diff --git a/src/main/resources/application-lab.properties b/src/main/resources/application-lab.properties index 975ef4c..af57397 100644 --- a/src/main/resources/application-lab.properties +++ b/src/main/resources/application-lab.properties @@ -7,7 +7,7 @@ server.port=6950 server.servlet.context-path=/api -spring.jpa.hibernate.ddl-auto=validate +spring.jpa.hibernate.ddl-auto=create spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false @@ -55,3 +55,5 @@ jobrunr.datasource.username=sa jobrunr.datasource.password= app.base-url=http://localhost:6950 + +sms.debug=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8c6bd2b..c245e23 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -84,3 +84,6 @@ jobrunr.datasource.password= app.reports.storage-path=./reports app.base-url=https://pay.velocityafrica.net +zss.sms.api.url=https://secure.zss.co.zw/vportal/cnm/vsms/json/batch +zss.sms.api.auth=Basic UWFudHJhdGVjaDpVbUk3OUpwUDh3QTY= +sms.debug=false diff --git a/src/main/resources/templates/email-otp.html b/src/main/resources/templates/email-otp.html new file mode 100644 index 0000000..60308fb --- /dev/null +++ b/src/main/resources/templates/email-otp.html @@ -0,0 +1,62 @@ + + + + + + Verification Code + + + + + + +
+ + + + + + + + + + + + + +
+

Verify Your Account

+
+

Hi {{username}},

+

Thank you for choosing Velocity Pay. Please use the verification code below to complete your registration:

+ + + + + + +
+
+
+ {{otpCode}} +
+
+
+ +

This code will expire in 10 minutes. If you didn't request this code, please ignore this email.

+ +
+ +

If you have any questions, feel free to contact our support team at support@velocityafrica.net

+
+

© 2026 Velocity Pay. All rights reserved.

+

Qantra Technologies Pvt Ltd

+ +
+
+ + \ No newline at end of file