diff --git a/pom.xml b/pom.xml
index a50ad5a..1f4fb9f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -180,6 +180,19 @@
true
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ com.github.vladimir-bukhtoyarov
+ bucket4j-core
+ 7.6.0
+
+
@@ -208,4 +221,4 @@
BBPS
-
+
\ No newline at end of file
diff --git a/src/main/java/com/billpaymentservice/bbps/config/RateLimiterConfig.java b/src/main/java/com/billpaymentservice/bbps/config/RateLimiterConfig.java
new file mode 100644
index 0000000..7ced321
--- /dev/null
+++ b/src/main/java/com/billpaymentservice/bbps/config/RateLimiterConfig.java
@@ -0,0 +1,68 @@
+package com.billpaymentservice.bbps.config;
+
+import java.time.Duration;
+import java.util.Arrays;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.core.Ordered;
+
+import com.billpaymentservice.bbps.filter.RateLimitingFilter;
+import com.billpaymentservice.bbps.ratelimit.LocalBucketManager;
+
+@Configuration
+public class RateLimiterConfig {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterConfig.class);
+
+ @Value("${spring.redis.host:localhost}")
+ private String redisHost;
+
+ @Value("${spring.redis.port:6379}")
+ private int redisPort;
+
+ // allow comma-separated patterns, default to /api/* and /masterData/* for backward compatibility
+ @Value("${rate.limiter.url-pattern:/api/*,/masterData/*}")
+ private String urlPattern;
+
+ @Bean
+ public LettuceConnectionFactory redisConnectionFactory() {
+ RedisStandaloneConfiguration cfg = new RedisStandaloneConfiguration(redisHost, redisPort);
+ LettuceConnectionFactory factory = new LettuceConnectionFactory(cfg);
+ // optional tuning
+ factory.setValidateConnection(true);
+ return factory;
+ }
+
+ @Bean
+ public StringRedisTemplate stringRedisTemplate(LettuceConnectionFactory connectionFactory) {
+ StringRedisTemplate tpl = new StringRedisTemplate(connectionFactory);
+ tpl.setEnableTransactionSupport(false);
+ return tpl;
+ }
+
+ @Bean
+ public FilterRegistrationBean rateLimiterFilterRegistration(StringRedisTemplate stringRedisTemplate, LocalBucketManager localBucketManager) {
+ RateLimitingFilter filter = new RateLimitingFilter(stringRedisTemplate, localBucketManager);
+ FilterRegistrationBean reg = new FilterRegistrationBean<>();
+ reg.setFilter(filter);
+
+ // support comma-separated patterns in property (e.g. "/api/*,/masterData/*")
+ String[] patterns = urlPattern.split("\\s*,\\s*");
+ reg.addUrlPatterns(patterns);
+
+ LOGGER.info("Registering RateLimitingFilter for patterns={}", Arrays.toString(patterns));
+
+ reg.setName("rateLimitingFilter");
+ // set the filter to highest precedence so it runs before other servlet filters (e.g., security chain)
+ reg.setOrder(Ordered.HIGHEST_PRECEDENCE);
+ return reg;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/billpaymentservice/bbps/filter/RateLimitingFilter.java b/src/main/java/com/billpaymentservice/bbps/filter/RateLimitingFilter.java
new file mode 100644
index 0000000..868f674
--- /dev/null
+++ b/src/main/java/com/billpaymentservice/bbps/filter/RateLimitingFilter.java
@@ -0,0 +1,174 @@
+package com.billpaymentservice.bbps.filter;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.RedisConnectionFailureException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.billpaymentservice.bbps.ratelimit.LocalBucketManager;
+import com.billpaymentservice.bbps.util.Helper;
+
+import io.github.bucket4j.Bucket;
+
+
+public class RateLimitingFilter extends OncePerRequestFilter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitingFilter.class);
+
+ private static final long RATE_PER_WINDOW = 1L;
+ private static final long BURST = 0L;
+ private static final long WINDOW_MILLIS = 60_000L;
+
+ private final StringRedisTemplate redisTemplate;
+ private final LocalBucketManager localBucketManager;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ private final DefaultRedisScript incrWithExpireScript;
+
+ public RateLimitingFilter(StringRedisTemplate redisTemplate, LocalBucketManager localBucketManager) {
+ this.redisTemplate = redisTemplate;
+ this.localBucketManager = localBucketManager;
+ String script =
+ "local v = redis.call('INCR', KEYS[1]);" +
+ "if v == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end;" +
+ "return v";
+
+ this.incrWithExpireScript = new DefaultRedisScript<>(script, Long.class);
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ String mid = validateAndExtractMid(request, response);
+ if (mid == null) return;
+
+ LOGGER.info("RateLimiter activated → path={} mid={}", request.getRequestURI(), mid);
+ response.setHeader("X-RateLimit-Filter", "invoked");
+
+ long limit = RATE_PER_WINDOW + BURST;
+ String redisKey = buildRedisKey(mid);
+
+ // 1️⃣ Local Bucket Check
+ if (!consumeLocalToken(mid, response)) {
+ long resetAt = calculateResetTime();
+ setRateHeaders(response, limit, 0, resetAt);
+ writeErrorResponse(request, response, 429, "Too Many Requests", "Rate limit exceeded (local)", mid);
+ return;
+ }
+
+ // 2️⃣ Redis Counter
+ Long count = incrementRedisCounter(redisKey);
+ long resetAt = calculateResetTime();
+ long remaining = Math.max(0, limit - count);
+
+ setRateHeaders(response, limit, remaining, resetAt);
+ response.setHeader("X-RateLimit-Count", String.valueOf(count));
+
+ if (count > limit) {
+ handleGlobalLimitExceeded(mid, response, request, resetAt);
+ return;
+ }
+
+ filterChain.doFilter(request, response);
+
+ } // -------------------------HELPERS-------------------------
+
+
+ private String validateAndExtractMid(HttpServletRequest request, HttpServletResponse response) throws IOException
+ { String mid = request.getHeader("mid");
+ if (mid == null || mid.trim().isEmpty())
+ { writeErrorResponse(request, response, 400, "Bad Request", "Required header 'mid' is missing or empty", null);
+ return null; }
+ return mid.trim(); }
+
+ private boolean consumeLocalToken(String mid, HttpServletResponse response) {
+ Bucket bucket = localBucketManager.getBucket("local:mid:" + mid);
+ boolean consumed = bucket.tryConsume(1);
+ response.setHeader("X-RateLimit-Local-Remaining", String.valueOf(bucket.getAvailableTokens()));
+ return consumed;
+ }
+
+ private Long incrementRedisCounter(String key) {
+ long ttlSeconds = (WINDOW_MILLIS / 1000L) + 2;
+
+ try {
+ Long result = redisTemplate.execute(
+ incrWithExpireScript,
+ Collections.singletonList(key),
+ String.valueOf(ttlSeconds)
+ );
+ return (result == null ? 1 : result);
+ } catch (Exception e) {
+ LOGGER.warn("Redis unavailable → fallback mode");
+ return 1L;
+ }
+ }
+
+ private long calculateResetTime() {
+ long nowSec = System.currentTimeMillis() / 1000L;
+ long win = WINDOW_MILLIS / 1000L;
+ return ((nowSec / win) * win) + win;
+ }
+
+ private String buildRedisKey(String mid) {
+ long index = System.currentTimeMillis() / WINDOW_MILLIS;
+ return "ratelimit:mid:" + mid + ":ts:" + index;
+ }
+
+ private void setRateHeaders(HttpServletResponse response, long limit, long remaining, long resetAt) {
+ response.setHeader("X-RateLimit-Limit", String.valueOf(limit));
+ response.setHeader("X-RateLimit-Remaining", String.valueOf(remaining));
+ response.setHeader("X-RateLimit-Reset", String.valueOf(resetAt));
+ }
+
+ private void handleGlobalLimitExceeded(String mid,
+ HttpServletResponse res,
+ HttpServletRequest req,
+ long resetAt) throws IOException {
+
+ long now = System.currentTimeMillis() / 1000L;
+ res.setHeader("Retry-After", String.valueOf(Math.max(1, resetAt - now)));
+
+ writeErrorResponse(req, res, 429,
+ "Too Many Requests", "Rate limit exceeded (global)", mid);
+ }
+
+ private void writeErrorResponse(HttpServletRequest request,
+ HttpServletResponse response,
+ int status, String error,
+ String message, String mid) throws IOException {
+
+ if (response.isCommitted()) return;
+
+ Map body = new HashMap<>();
+ body.put("timestamp", Instant.now().toString());
+ body.put("status", status);
+ body.put("error", error);
+ body.put("message", message);
+ body.put("path", request.getRequestURI());
+
+ if (mid != null) body.put("mid", mid);
+
+ response.setStatus(status);
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ mapper.writeValue(response.getWriter(), body);
+ }
+}
+
+
+
diff --git a/src/main/java/com/billpaymentservice/bbps/ratelimit/LocalBucketManager.java b/src/main/java/com/billpaymentservice/bbps/ratelimit/LocalBucketManager.java
new file mode 100644
index 0000000..50c1d8b
--- /dev/null
+++ b/src/main/java/com/billpaymentservice/bbps/ratelimit/LocalBucketManager.java
@@ -0,0 +1,34 @@
+package com.billpaymentservice.bbps.ratelimit;
+
+import java.time.Duration;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.springframework.stereotype.Component;
+
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket;
+import io.github.bucket4j.Bucket4j;
+import io.github.bucket4j.Refill;
+
+@Component
+public class LocalBucketManager {
+
+ // match the filter: 1 request per minute, no burst
+ private static final long RATE_PER_WINDOW = 1L;
+ private static final long BURST = 0L;
+ private static final Duration WINDOW_DURATION = Duration.ofMinutes(1);
+
+ private final ConcurrentMap cache = new ConcurrentHashMap<>();
+
+ public Bucket getBucket(String key) {
+ return cache.computeIfAbsent(key, k -> createBucket());
+ }
+
+ private Bucket createBucket() {
+ Bandwidth limit = Bandwidth.classic(RATE_PER_WINDOW + BURST, Refill.greedy(RATE_PER_WINDOW, WINDOW_DURATION));
+ return Bucket4j.builder()
+ .addLimit(limit)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/billpaymentservice/bbps/serviceImpl/AuthServiceImpl.java b/src/main/java/com/billpaymentservice/bbps/serviceImpl/AuthServiceImpl.java
index 65be8d6..cf85184 100644
--- a/src/main/java/com/billpaymentservice/bbps/serviceImpl/AuthServiceImpl.java
+++ b/src/main/java/com/billpaymentservice/bbps/serviceImpl/AuthServiceImpl.java
@@ -75,9 +75,19 @@ public class AuthServiceImpl implements AuthService {
String mobileNo = request.getMobile();
try {
Optional byMobileNumber = userRepository.findByMobile(mobileNo);
+
+
if (byMobileNumber.isPresent()) {
UserEntity user = byMobileNumber.get();
+ LOGGER.info("User found for mobileNo: {}", user);
+
+ if(user.getIsDeleted() != true && user.isEnabled()== false) {
+ LOGGER.warn("User with mobileNo: {} is deleted", mobileNo);
+ return ResponseModel.builder().message("user account is disabled.please contact your service provider").status("Failure")
+ .statusCode(BBPSConstant.USER_IS_DELTED_IN_DB).data(null).build();
+ }
+
String token = jwtUtil.generateToken(user.getMobile());
LOGGER.info("Generated JWT token for mobileNo: " + mobileNo);
diff --git a/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java b/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java
index 553d2ba..e4b06ed 100644
--- a/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java
+++ b/src/main/java/com/billpaymentservice/bbps/util/BBPSConstant.java
@@ -18,6 +18,7 @@ public class BBPSConstant extends BBPS_RESPONSE_COD_CONSTANT {
public static final Long PENDING_CODE = 211L;
public static final Long FRAUD_CODE = 1001L;
public static final int BADREQUEST = 400;
+ public static final int TO_MANY_REQUEST=429;
public static final String SUCCESS_STATUS = "success";
public static final String FAILED_STATUS = "failed";
diff --git a/src/main/java/com/billpaymentservice/bbps/util/BBPS_RESPONSE_COD_CONSTANT.java b/src/main/java/com/billpaymentservice/bbps/util/BBPS_RESPONSE_COD_CONSTANT.java
index 8bd51a9..73e2165 100644
--- a/src/main/java/com/billpaymentservice/bbps/util/BBPS_RESPONSE_COD_CONSTANT.java
+++ b/src/main/java/com/billpaymentservice/bbps/util/BBPS_RESPONSE_COD_CONSTANT.java
@@ -1,5 +1,6 @@
package com.billpaymentservice.bbps.util;
public class BBPS_RESPONSE_COD_CONSTANT {
+ public static final int USER_IS_DELTED_IN_DB =602;
}
diff --git a/src/main/java/com/billpaymentservice/bbps/util/ConfigHelper.java b/src/main/java/com/billpaymentservice/bbps/util/ConfigHelper.java
new file mode 100644
index 0000000..1f6c41a
--- /dev/null
+++ b/src/main/java/com/billpaymentservice/bbps/util/ConfigHelper.java
@@ -0,0 +1,20 @@
+package com.billpaymentservice.bbps.util;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class ConfigHelper {
+
+
+
+
+}
diff --git a/src/main/java/com/billpaymentservice/bbps/util/Helper.java b/src/main/java/com/billpaymentservice/bbps/util/Helper.java
index 6c62baf..bf4a955 100644
--- a/src/main/java/com/billpaymentservice/bbps/util/Helper.java
+++ b/src/main/java/com/billpaymentservice/bbps/util/Helper.java
@@ -30,7 +30,7 @@ import com.billpaymentservice.bbps.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
-public class Helper {
+public class Helper extends ConfigHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(Helper.class);
private final InboundTraceLogRepository inboundTraceLogRepository;
diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties
index 6722a87..16caf94 100644
--- a/src/main/resources/application-local.properties
+++ b/src/main/resources/application-local.properties
@@ -18,9 +18,9 @@ swagger.server.description=Local Development Server
# DataSource
-spring.datasource.url=jdbc:postgresql://localhost:5432/bbps_new_updated
+#spring.datasource.url=jdbc:postgresql://localhost:5432/bbps_new_updated
-#spring.datasource.url=jdbc:postgresql://localhost:5432/bbps_enhanced
+spring.datasource.url=jdbc:postgresql://localhost:5432/bbps_enhanced
>>>>>>> Stashed changes
spring.datasource.username=postgres
#spring.datasource.password=UAT@DB_2025
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b64b5a8..888fe3a 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -21,7 +21,7 @@ spring.profiles.active=local
# DataSource
-spring.datasource.url=jdbc:postgresql://localhost:5432/devdbuat
+spring.datasource.url=jdbc:postgresql://localhost:5432/bbps_new_updated
spring.datasource.username=postgres
#spring.datasource.password=UAT@DB_2025
diff --git a/src/test/java/com/billpaymentservice/bbps/filter/RateLimitingFilterTest.java b/src/test/java/com/billpaymentservice/bbps/filter/RateLimitingFilterTest.java
new file mode 100644
index 0000000..4cd7371
--- /dev/null
+++ b/src/test/java/com/billpaymentservice/bbps/filter/RateLimitingFilterTest.java
@@ -0,0 +1,109 @@
+package com.billpaymentservice.bbps.filter;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import com.billpaymentservice.bbps.ratelimit.LocalBucketManager;
+
+import io.github.bucket4j.Bucket;
+
+class RateLimitingFilterTest {
+
+ private StringRedisTemplate redisTemplate;
+ private LocalBucketManager localBucketManager;
+ private RateLimitingFilter filter;
+
+ @BeforeEach
+ void setup() {
+ redisTemplate = mock(StringRedisTemplate.class);
+ localBucketManager = mock(LocalBucketManager.class);
+ filter = new RateLimitingFilter(redisTemplate, localBucketManager);
+ }
+
+ @Test
+ void whenLocalBucketExhausted_thenReturns429() throws ServletException, IOException {
+ MockHttpServletRequest req = new MockHttpServletRequest();
+ req.addHeader("mid", "MID_LOCAL_EXHAUST");
+ MockHttpServletResponse resp = new MockHttpServletResponse();
+
+ Bucket mockBucket = mock(Bucket.class);
+ when(localBucketManager.getBucket("local:mid:MID_LOCAL_EXHAUST")).thenReturn(mockBucket);
+ when(mockBucket.tryConsume(1)).thenReturn(false);
+
+ FilterChain chain = mock(FilterChain.class);
+
+ filter.doFilter(req, resp, chain);
+
+ assertEquals(429, resp.getStatus());
+ String content = resp.getContentAsString();
+ assertTrue(content.contains("Rate limit exceeded (local)"));
+ verify(chain, never()).doFilter(any(), any());
+ }
+
+ @Test
+ void whenLocalAllowsAndRedisUnderLimit_thenPassesThrough() throws Exception {
+ MockHttpServletRequest req = new MockHttpServletRequest();
+ req.addHeader("mid", "MID_OK");
+ MockHttpServletResponse resp = new MockHttpServletResponse();
+
+ Bucket mockBucket = mock(Bucket.class);
+ when(localBucketManager.getBucket("local:mid:MID_OK")).thenReturn(mockBucket);
+ when(mockBucket.tryConsume(1)).thenReturn(true);
+ when(mockBucket.getAvailableTokens()).thenReturn(1000L);
+
+ // stub redis execute to return 1 (below limit)
+ when(redisTemplate.execute(any(DefaultRedisScript.class), anyList(), eq("2"))).thenReturn(1L);
+
+ // use a FilterChain that sets a flag by setting response status to 200
+ FilterChain chain = (request, response) -> {
+ ((MockHttpServletResponse) response).setStatus(200);
+ };
+
+ filter.doFilter(req, resp, chain);
+
+ assertEquals(200, resp.getStatus());
+ // headers should be present
+ assertEquals("1200", resp.getHeader("X-RateLimit-Limit"));
+ assertNotNull(resp.getHeader("X-RateLimit-Remaining"));
+ }
+
+ @Test
+ void whenLocalAllowsButRedisOverLimit_thenReturns429AndReturnsToken() throws Exception {
+ MockHttpServletRequest req = new MockHttpServletRequest();
+ req.addHeader("mid", "MID_GLOBAL_EXHAUST");
+ MockHttpServletResponse resp = new MockHttpServletResponse();
+
+ Bucket mockBucket = mock(Bucket.class);
+ when(localBucketManager.getBucket("local:mid:MID_GLOBAL_EXHAUST")).thenReturn(mockBucket);
+ when(mockBucket.tryConsume(1)).thenReturn(true);
+ when(mockBucket.getAvailableTokens()).thenReturn(0L);
+
+ // stub redis execute to indicate global over-limit
+ when(redisTemplate.execute(any(DefaultRedisScript.class), anyList(), eq("2"))).thenReturn(1201L);
+
+ FilterChain chain = mock(FilterChain.class);
+
+ filter.doFilter(req, resp, chain);
+
+ assertEquals(429, resp.getStatus());
+ String content = resp.getContentAsString();
+ assertTrue(content.contains("Rate limit exceeded (global)"));
+
+ // verify token returned to local bucket
+ verify(mockBucket, times(1)).addTokens(1);
+ verify(chain, never()).doFilter(any(), any());
+ }
+}