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: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = 'com.flexcodelabs'
version = '0.0.47'
version = '0.0.48'
description = 'Flextuma App'

java {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.flexcodelabs.flextuma.core.entities.base.Owner;
import com.flexcodelabs.flextuma.core.enums.CategoryEnum;
import com.flexcodelabs.flextuma.core.enums.SmsTemplateStatus;

import jakarta.persistence.*;
import lombok.*;
Expand Down Expand Up @@ -43,6 +44,10 @@ public class SmsTemplate extends Owner {
@Column(columnDefinition = "TEXT", nullable = false)
private String content;

@Column(nullable = false, updatable = true)
@Enumerated(EnumType.STRING)
private SmsTemplateStatus status = SmsTemplateStatus.DRAFT;

@Column(nullable = true, updatable = true)
@Enumerated(EnumType.STRING)
private CategoryEnum category = CategoryEnum.PROMOTIONAL;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.flexcodelabs.flextuma.core.enums;

public enum SmsTemplateStatus {
DRAFT,
ACTIVE,
INACTIVE
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.flexcodelabs.flextuma.core.entities.sms.SmsLog;
import com.flexcodelabs.flextuma.core.enums.SmsCampaignStatus;
import com.flexcodelabs.flextuma.core.enums.SmsLogStatus;
import com.flexcodelabs.flextuma.core.enums.SmsTemplateStatus;
import com.flexcodelabs.flextuma.core.repositories.SmsCampaignRepository;
import com.flexcodelabs.flextuma.core.repositories.SmsLogRepository;
import com.flexcodelabs.flextuma.core.helpers.SmsSegmentCalculator;
Expand Down Expand Up @@ -66,6 +67,14 @@ private void processSingleCampaign(SmsCampaign campaign) {
return;
}

if (campaign.getTemplate() == null || campaign.getTemplate().getStatus() != SmsTemplateStatus.ACTIVE) {
log.error("Campaign [{}] references a template that is no longer active; cancelling",
campaign.getName());
campaign.setStatus(SmsCampaignStatus.CANCELLED);
campaignRepository.save(campaign);
return;
}

String[] recipients = recipientsStr.split(",");
log.info("Processing campaign [{}] for {} recipients", campaign.getName(), recipients.length);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.flexcodelabs.flextuma.core.entities.sms.SmsLog;
import com.flexcodelabs.flextuma.core.entities.sms.SmsTemplate;
import com.flexcodelabs.flextuma.core.enums.SmsLogStatus;
import com.flexcodelabs.flextuma.core.enums.SmsTemplateStatus;
import com.flexcodelabs.flextuma.core.helpers.SmsSegmentResult;
import com.flexcodelabs.flextuma.core.helpers.SmsSegmentCalculator;
import com.flexcodelabs.flextuma.core.helpers.TemplateUtils;
Expand Down Expand Up @@ -65,6 +66,11 @@ public SmsLog queueTemplatedSms(Map<String, String> placeholders, String usernam
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Template not found or you don't have access to it"));

if (template.getStatus() != SmsTemplateStatus.ACTIVE) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Template is not active");
}

SmsConnector connector = getConnector(currentUser, providerValue, placeholders.get("connectorId"));

String finalMessage = TemplateUtils.fillTemplate(template.getContent(), placeholders);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.flexcodelabs.flextuma.modules.sms.services;

import com.flexcodelabs.flextuma.core.entities.sms.SmsCampaign;
import com.flexcodelabs.flextuma.core.entities.sms.SmsTemplate;
import com.flexcodelabs.flextuma.core.enums.SmsCampaignStatus;
import com.flexcodelabs.flextuma.core.enums.SmsTemplateStatus;
import com.flexcodelabs.flextuma.core.repositories.SmsCampaignRepository;
import com.flexcodelabs.flextuma.core.repositories.SmsTemplateRepository;
import com.flexcodelabs.flextuma.core.services.BaseService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.UUID;

Expand All @@ -16,6 +21,7 @@
public class SmsCampaignService extends BaseService<SmsCampaign> {

private final SmsCampaignRepository repository;
private final SmsTemplateRepository templateRepository;

@Override
protected JpaRepository<SmsCampaign, UUID> getRepository() {
Expand Down Expand Up @@ -72,6 +78,26 @@ protected void onPreSave(SmsCampaign entity) {
if (entity.getStatus() == null) {
entity.setStatus(SmsCampaignStatus.SCHEDULED);
}
validateTemplateIsActive(entity);
}

@Override
protected SmsCampaign onPreUpdate(SmsCampaign newEntity, SmsCampaign oldEntity) {
SmsCampaign merged = super.onPreUpdate(newEntity, oldEntity);
validateTemplateIsActive(newEntity);
return merged;
}

private void validateTemplateIsActive(SmsCampaign entity) {
if (entity.getTemplate() == null || entity.getTemplate().getId() == null) {
return;
}
SmsTemplate template = templateRepository.findById(entity.getTemplate().getId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Template not found"));
if (template.getStatus() != SmsTemplateStatus.ACTIVE) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Cannot use an inactive template for a campaign");
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.flexcodelabs.flextuma.core.entities.sms.SmsLog;
import com.flexcodelabs.flextuma.core.entities.sms.SmsTemplate;
import com.flexcodelabs.flextuma.core.enums.SmsCampaignStatus;
import com.flexcodelabs.flextuma.core.enums.SmsTemplateStatus;
import com.flexcodelabs.flextuma.core.helpers.SmsSegmentCalculator;
import com.flexcodelabs.flextuma.core.helpers.SmsSegmentResult;
import com.flexcodelabs.flextuma.core.repositories.SmsCampaignRepository;
Expand Down Expand Up @@ -83,6 +84,7 @@ void processCampaigns_withDueCampaigns_shouldProcessThem() {
campaign.setRecipients("255700112233, 255700445566");
SmsTemplate template = new SmsTemplate();
template.setContent("Hello world");
template.setStatus(SmsTemplateStatus.ACTIVE);
campaign.setTemplate(template);
SmsConnector connector = new SmsConnector();
campaign.setConnector(connector);
Expand All @@ -106,6 +108,34 @@ void processCampaigns_withDueCampaigns_shouldProcessThem() {
assert (campaign.getStatus() == SmsCampaignStatus.COMPLETED);
}

@Test
void processCampaigns_withInactiveTemplate_shouldCancelWithoutDispatching() {
SmsCampaign campaign = new SmsCampaign();
campaign.setName("Test Campaign");
campaign.setRecipients("255700112233, 255700445566");
SmsTemplate template = new SmsTemplate();
template.setContent("Hello world");
template.setStatus(SmsTemplateStatus.INACTIVE);
campaign.setTemplate(template);
SmsConnector connector = new SmsConnector();
campaign.setConnector(connector);
User adminUser = new User();
adminUser.setUsername("admin");
campaign.setCreatedBy(adminUser);

when(campaignRepository.findDueCampaigns(eq(SmsCampaignStatus.SCHEDULED), any(LocalDateTime.class),
any(Pageable.class)))
.thenReturn(List.of(campaign));

worker.processCampaigns();

verify(campaignRepository, atLeastOnce()).save(campaign);
verify(walletService, never()).debit(any(), any(BigDecimal.class), anyString(), any());
verify(logRepository, never()).save(any(SmsLog.class));
verify(personalNotificationService, never()).notifyCampaignCompleted(any(), anyString());
assert (campaign.getStatus() == SmsCampaignStatus.CANCELLED);
}

@Test
void processCampaigns_withEmptyRecipients_shouldCompleteImmediately() {
SmsCampaign campaign = new SmsCampaign();
Expand All @@ -128,6 +158,7 @@ void processCampaigns_whenDebitFails_shouldContinueProcessing() {
campaign.setRecipients("255700112233");
SmsTemplate template = new SmsTemplate();
template.setContent("Hello");
template.setStatus(SmsTemplateStatus.ACTIVE);
campaign.setTemplate(template);
SmsConnector connector = new SmsConnector();
campaign.setConnector(connector);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.flexcodelabs.flextuma.core.entities.sms.SmsLog;
import com.flexcodelabs.flextuma.core.entities.sms.SmsTemplate;
import com.flexcodelabs.flextuma.core.enums.SmsLogStatus;
import com.flexcodelabs.flextuma.core.enums.SmsTemplateStatus;
import com.flexcodelabs.flextuma.core.repositories.SmsConnectorRepository;
import com.flexcodelabs.flextuma.core.repositories.SmsLogRepository;
import com.flexcodelabs.flextuma.core.repositories.SmsTemplateRepository;
Expand Down Expand Up @@ -143,10 +144,27 @@ void queueTemplatedSms_shouldThrowWhenTemplateNotFound() {
assertTrue(ex.getReason().contains("Template not found or you don't have access to it"));
}

@Test
void queueTemplatedSms_shouldThrowWhenTemplateIsInactive() {
SmsTemplate template = new SmsTemplate();
template.setContent("Hello {{name}}");
template.setStatus(SmsTemplateStatus.INACTIVE);

when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser));
when(templateRepository.findByCreatedByAndCode(testUser, "WELCOME")).thenReturn(Optional.of(template));

ResponseStatusException ex = assertThrows(ResponseStatusException.class,
() -> notificationService.queueTemplatedSms(validPlaceholders, "testuser"));

assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
assertTrue(ex.getReason().contains("Template is not active"));
}

@Test
void queueTemplatedSms_shouldThrowWhenConnectorNotFound() {
SmsTemplate template = new SmsTemplate();
template.setContent("Hello {{name}}");
template.setStatus(SmsTemplateStatus.ACTIVE);

when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser));
when(templateRepository.findByCreatedByAndCode(testUser, "WELCOME")).thenReturn(Optional.of(template));
Expand All @@ -164,6 +182,7 @@ void queueTemplatedSms_shouldThrowWhenConnectorNotFound() {
void queueTemplatedSms_shouldQueueSmsSuccessfully() {
SmsTemplate template = new SmsTemplate();
template.setContent("Hello {{name}}");
template.setStatus(SmsTemplateStatus.ACTIVE);

SmsConnector connector = new SmsConnector();
connector.setProvider("Twilio");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.flexcodelabs.flextuma.modules.sms.services;

import com.flexcodelabs.flextuma.core.entities.sms.SmsCampaign;
import com.flexcodelabs.flextuma.core.entities.sms.SmsTemplate;
import com.flexcodelabs.flextuma.core.enums.SmsTemplateStatus;
import com.flexcodelabs.flextuma.core.repositories.SmsCampaignRepository;
import com.flexcodelabs.flextuma.core.repositories.SmsTemplateRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

import java.util.Optional;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SmsCampaignServiceTest {

@Mock
private SmsCampaignRepository repository;

@Mock
private SmsTemplateRepository templateRepository;

@InjectMocks
private SmsCampaignService smsCampaignService;

@Test
void onPreSave_shouldRejectInactiveTemplate() {
UUID templateId = UUID.randomUUID();
SmsTemplate templateStub = new SmsTemplate();
templateStub.setId(templateId);

SmsTemplate persistedTemplate = new SmsTemplate();
persistedTemplate.setId(templateId);
persistedTemplate.setStatus(SmsTemplateStatus.INACTIVE);

when(templateRepository.findById(templateId)).thenReturn(Optional.of(persistedTemplate));

SmsCampaign campaign = new SmsCampaign();
campaign.setTemplate(templateStub);

ResponseStatusException ex = assertThrows(ResponseStatusException.class,
() -> smsCampaignService.onPreSave(campaign));

assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
}

@Test
void onPreSave_shouldAllowActiveTemplate() {
UUID templateId = UUID.randomUUID();
SmsTemplate templateStub = new SmsTemplate();
templateStub.setId(templateId);

SmsTemplate persistedTemplate = new SmsTemplate();
persistedTemplate.setId(templateId);
persistedTemplate.setStatus(SmsTemplateStatus.ACTIVE);

when(templateRepository.findById(templateId)).thenReturn(Optional.of(persistedTemplate));

SmsCampaign campaign = new SmsCampaign();
campaign.setTemplate(templateStub);

smsCampaignService.onPreSave(campaign);
}

@Test
void onPreSave_shouldAllowMissingTemplate() {
SmsCampaign campaign = new SmsCampaign();

smsCampaignService.onPreSave(campaign);
}
}