added sms integration
This commit is contained in:
@@ -10,7 +10,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import zw.qantra.tm.domain.enums.RequestType;
|
import zw.qantra.tm.domain.enums.RequestType;
|
||||||
import zw.qantra.tm.domain.models.Transaction;
|
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;
|
import zw.qantra.tm.domain.services.TransactionService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -18,7 +18,7 @@ import zw.qantra.tm.domain.services.TransactionService;
|
|||||||
@CrossOrigin(origins = "*")
|
@CrossOrigin(origins = "*")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TestController {
|
public class TestController {
|
||||||
private final EmailService emailService;
|
private final NotificationService notificationService;
|
||||||
private final TransactionService transactionService;
|
private final TransactionService transactionService;
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
|
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
|
||||||
@@ -44,7 +44,7 @@ public class TestController {
|
|||||||
|
|
||||||
@GetMapping("/hello/{email}")
|
@GetMapping("/hello/{email}")
|
||||||
public ResponseEntity<String> helloEndpoint(@PathVariable String email) {
|
public ResponseEntity<String> helloEndpoint(@PathVariable String email) {
|
||||||
emailService.sendSimpleMessage(email, "Hello", "Hello World!");
|
notificationService.sendEmail(email, "Hello", "Hello World!");
|
||||||
return ResponseEntity.ok("Hello World!");
|
return ResponseEntity.ok("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<SmsMessage> smsList;
|
||||||
|
}
|
||||||
|
|
||||||
17
src/main/java/zw/qantra/tm/domain/dtos/sms/SmsMessage.java
Normal file
17
src/main/java/zw/qantra/tm/domain/dtos/sms/SmsMessage.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
package zw.qantra.tm.domain.services;
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.mail.SimpleMailMessage;
|
import org.springframework.mail.SimpleMailMessage;
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Component;
|
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
|
@Component
|
||||||
public class EmailService {
|
public class EmailService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(EmailService.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private JavaMailSender emailSender;
|
private JavaMailSender emailSender;
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
|
@Deprecated
|
||||||
public void sendSimpleMessage(
|
public void sendSimpleMessage(
|
||||||
String to, String subject, String text) {
|
String to, String subject, String text) {
|
||||||
|
log.warn("EmailService.sendSimpleMessage() is deprecated. Use NotificationService instead.");
|
||||||
SimpleMailMessage message = new SimpleMailMessage();
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
message.setFrom("noreply@qantra.co.zw");
|
message.setFrom("noreply@qantra.co.zw");
|
||||||
message.setTo(to);
|
message.setTo(to);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ import zw.qantra.tm.domain.dtos.*;
|
|||||||
import zw.qantra.tm.domain.dtos.erp.VelocityCustomerResponseDto;
|
import zw.qantra.tm.domain.dtos.erp.VelocityCustomerResponseDto;
|
||||||
import zw.qantra.tm.domain.models.Otp;
|
import zw.qantra.tm.domain.models.Otp;
|
||||||
import zw.qantra.tm.domain.models.User;
|
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.OtpService;
|
||||||
import zw.qantra.tm.domain.services.UserService;
|
import zw.qantra.tm.domain.services.UserService;
|
||||||
import io.nflow.engine.workflow.curated.State;
|
import io.nflow.engine.workflow.curated.State;
|
||||||
@@ -48,7 +48,7 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RestService restService;
|
private RestService restService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private EmailService emailService;
|
private NotificationService notificationService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VelocityPaymentService velocityPaymentService;
|
private VelocityPaymentService velocityPaymentService;
|
||||||
|
|
||||||
@@ -117,9 +117,9 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
// Generate OTP
|
// Generate OTP
|
||||||
Otp otp = otpService.generateOtp(username, "REGISTRATION");
|
Otp otp = otpService.generateOtp(username, "REGISTRATION");
|
||||||
|
|
||||||
String string = String.format("Your Velocity Pay verification code is: %s", otp.getOtp());
|
notificationService.sendOtpEmail(email, username, otp.getOtp());
|
||||||
emailService.sendSimpleMessage(email, "Velocity Pay verification code", string);
|
notificationService.sendSms(registerRequest.getPhone(),
|
||||||
// LinkedHashMap response = restService.postAs(infobipUrl, string, headers, LinkedHashMap.class);
|
"Your Velocity Pay verification code is: " + otp.getOtp());
|
||||||
|
|
||||||
log.info("otp generated successfully: {}", otp.getOtp());
|
log.info("otp generated successfully: {}", otp.getOtp());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -142,10 +142,12 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
public void resendOtp(StateExecution execution) {
|
public void resendOtp(StateExecution execution) {
|
||||||
OtpRequest otpRequest = execution.getVariable("resend", OtpRequest.class);
|
OtpRequest otpRequest = execution.getVariable("resend", OtpRequest.class);
|
||||||
Otp otp = otpService.generateOtp(otpRequest.getUsername(), "REGISTRATION");
|
Otp otp = otpService.generateOtp(otpRequest.getUsername(), "REGISTRATION");
|
||||||
String string = String.format("Your Velocity Pay verification code is: %s", otp.getOtp());
|
// The username field in OtpRequest may actually contain the email; use it directly
|
||||||
emailService.sendSimpleMessage(otpRequest.getUsername(), "Velocity Pay verification code", string);
|
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) {
|
public NextAction otpSuccess(StateExecution execution) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ server.port=6950
|
|||||||
|
|
||||||
server.servlet.context-path=/api
|
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.driver-class-name=org.postgresql.Driver
|
||||||
|
|
||||||
spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false
|
spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false
|
||||||
@@ -55,3 +55,5 @@ jobrunr.datasource.username=sa
|
|||||||
jobrunr.datasource.password=
|
jobrunr.datasource.password=
|
||||||
|
|
||||||
app.base-url=http://localhost:6950
|
app.base-url=http://localhost:6950
|
||||||
|
|
||||||
|
sms.debug=true
|
||||||
@@ -84,3 +84,6 @@ jobrunr.datasource.password=
|
|||||||
app.reports.storage-path=./reports
|
app.reports.storage-path=./reports
|
||||||
app.base-url=https://pay.velocityafrica.net
|
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
|
||||||
|
|||||||
62
src/main/resources/templates/email-otp.html
Normal file
62
src/main/resources/templates/email-otp.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Verification Code</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; background-color: #E9D2CA; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #E9D2CA; padding: 40px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 24px rgba(24, 23, 28, 0.12);">
|
||||||
|
<!-- Header -->
|
||||||
|
<tr>
|
||||||
|
<td style="background: linear-gradient(135deg, #18171C 0%, #3a3228 60%, #9B6314 100%); padding: 40px 30px; text-align: center;">
|
||||||
|
<h1 style="color: #E9D2CA; font-size: 28px; margin: 0; font-weight: 600; letter-spacing: -0.5px;">Verify Your Account</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 36px;">
|
||||||
|
<p style="color: #18171C; font-size: 16px; line-height: 1.6; margin: 0 0 24px 0;">Hi <strong style="color: #9B6314;">{{username}}</strong>,</p>
|
||||||
|
<p style="color: #18171C; font-size: 16px; line-height: 1.6; margin: 0 0 24px 0;">Thank you for choosing <strong style="color: #9B6314;">Velocity Pay</strong>. Please use the verification code below to complete your registration:</p>
|
||||||
|
|
||||||
|
<!-- OTP Code -->
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 32px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<div style="display: inline-block; background: #9B6314; border-radius: 12px; padding: 4px;">
|
||||||
|
<div style="background: #ffffff; border-radius: 10px; padding: 16px 48px; letter-spacing: 10px; font-size: 36px; font-weight: 700; color: #18171C;">
|
||||||
|
{{otpCode}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="color: #988A7D; font-size: 14px; line-height: 1.6; margin: 0 0 8px 0;">This code will expire in <strong style="color: #9B6314;">10 minutes</strong>. If you didn't request this code, please ignore this email.</p>
|
||||||
|
|
||||||
|
<hr style="border: none; border-top: 1px solid #E9D2CA; margin: 32px 0;">
|
||||||
|
|
||||||
|
<p style="color: #989195; font-size: 13px; line-height: 1.5; margin: 0;">If you have any questions, feel free to contact our support team at <a href="mailto:support@velocityafrica.net" style="color: #9B6314; text-decoration: none;">support@velocityafrica.net</a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #18171C; padding: 24px 36px; text-align: center; border-top: 1px solid #AD8847;">
|
||||||
|
<p style="color: #E9D2CA; font-size: 12px; line-height: 1.5; margin: 0 0 8px 0;">© 2026 Velocity Pay. All rights reserved.</p>
|
||||||
|
<p style="color: #988A7D; font-size: 12px; line-height: 1.5; margin: 0;">Qantra Technologies Pvt Ltd</p>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<a href="https://velocityafrica.net" style="color: #AD8847; font-size: 12px; text-decoration: none; margin: 0 8px;">Website</a>
|
||||||
|
<span style="color: #989195;">|</span>
|
||||||
|
<a href="mailto:support@velocityafrica.net" style="color: #AD8847; font-size: 12px; text-decoration: none; margin: 0 8px;">Contact Support</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user