diff --git a/authentication-service/pom.xml b/authentication-service/pom.xml index c055845..681dc69 100644 --- a/authentication-service/pom.xml +++ b/authentication-service/pom.xml @@ -32,6 +32,11 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.kafka + spring-kafka + + org.springframework.boot @@ -79,6 +84,12 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.cloud + spring-cloud-commons + 4.2.0-RC1 + compile + diff --git a/authentication-service/src/main/java/com/finpay/authentication/clients/UserServiceClient.java b/authentication-service/src/main/java/com/finpay/authentication/clients/UserServiceClient.java new file mode 100644 index 0000000..c031f8c --- /dev/null +++ b/authentication-service/src/main/java/com/finpay/authentication/clients/UserServiceClient.java @@ -0,0 +1,38 @@ +package com.finpay.authentication.clients; +import com.finpay.authentication.dtos.UserDto; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Optional; + +@Component +public class UserServiceClient { + + private final RestTemplate restTemplate; + private final String apiGatewayBaseUrl = "http://api-gateway"; // API Gateway base URL + private final String userServicePath = "/user-service/api/users"; // Path to User Service in the gateway + + public UserServiceClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + /** + * Fetch a user by email from the User Service via the API Gateway. + * + * @param email the email of the user + * @return an Optional containing UserDto if the user exists, or empty if not found + */ + public Optional getUserByEmail(String email) { + String url = UriComponentsBuilder.fromHttpUrl(apiGatewayBaseUrl + userServicePath) + .queryParam("email", email) + .toUriString(); + try { + UserDto userDto = restTemplate.getForObject(url, UserDto.class); + return Optional.ofNullable(userDto); + } catch (Exception e) { + // Log error or handle exception as needed + return Optional.empty(); + } + } +} diff --git a/authentication-service/src/main/java/com/finpay/authentication/config/AppConfig.java b/authentication-service/src/main/java/com/finpay/authentication/config/AppConfig.java new file mode 100644 index 0000000..0d41040 --- /dev/null +++ b/authentication-service/src/main/java/com/finpay/authentication/config/AppConfig.java @@ -0,0 +1,16 @@ +package com.finpay.authentication.config; + +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + + @Bean + @LoadBalanced + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/authentication-service/src/main/java/com/finpay/authentication/controllers/AuthController.java b/authentication-service/src/main/java/com/finpay/authentication/controllers/AuthController.java index e93d57f..a0ec406 100644 --- a/authentication-service/src/main/java/com/finpay/authentication/controllers/AuthController.java +++ b/authentication-service/src/main/java/com/finpay/authentication/controllers/AuthController.java @@ -1,11 +1,7 @@ package com.finpay.authentication.controllers; -import com.finpay.authentication.dtos.JwtResponse; -import com.finpay.authentication.dtos.LoginRequest; -import com.finpay.authentication.dtos.MessageResponse; -import com.finpay.authentication.dtos.SignupRequest; -import com.finpay.authentication.models.User; -import com.finpay.authentication.repository.UserRepository; +import com.finpay.authentication.clients.UserServiceClient; +import com.finpay.authentication.dtos.*; import com.finpay.authentication.utils.JwtUtil; import org.springframework.http.*; import org.springframework.security.authentication.*; @@ -14,52 +10,30 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; -import java.util.UUID; - @RestController @RequestMapping("/api/auth") public class AuthController { private final AuthenticationManager authenticationManager; - private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; + private final UserServiceClient userServiceClient; // Add UserServiceClient // Constructor-based injection public AuthController(AuthenticationManager authenticationManager, - UserRepository userRepository, PasswordEncoder passwordEncoder, - JwtUtil jwtUtil) { + JwtUtil jwtUtil, + UserServiceClient userServiceClient) { this.authenticationManager = authenticationManager; - this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.jwtUtil = jwtUtil; - } - - @PostMapping("/register") - public ResponseEntity registerUser(@RequestBody SignupRequest signupRequest) { - if (userRepository.existsByEmail(signupRequest.getEmail())) { - return ResponseEntity - .badRequest() - .body(new MessageResponse("Error: Email is already in use!")); - } - - // Create new user - User user = User.builder() - .id(UUID.randomUUID()) - .name(signupRequest.getName()) - .email(signupRequest.getEmail()) - .password(passwordEncoder.encode(signupRequest.getPassword())) - .build(); - - userRepository.save(user); - - return ResponseEntity.ok(new MessageResponse("User registered successfully!")); + this.userServiceClient = userServiceClient; // Initialize UserServiceClient } @PostMapping("/login") - public ResponseEntity authenticateUser(@RequestBody LoginRequest loginRequest) { + public ResponseEntity authenticateUser(@RequestBody LoginRequest loginRequest) { try { + // Authenticate user Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getEmail(), @@ -67,14 +41,15 @@ public ResponseEntity authenticateUser(@RequestBody LoginRequest loginRequest ) ); - // If authentication is successful - String jwt = jwtUtil.generateJwtToken(loginRequest.getEmail()); - - User user = userRepository.findByEmail(loginRequest.getEmail()) + // Fetch user details from User Service + UserDto userDto = userServiceClient.getUserByEmail(loginRequest.getEmail()) .orElseThrow(() -> new UsernameNotFoundException("User not found")); - JwtResponse jwtResponse = new JwtResponse(jwt, user.getId(), user.getName(), user.getEmail()); + // Generate JWT with email and roles + String jwt = jwtUtil.generateJwtToken(loginRequest.getEmail(), userDto.getRoles()); + // Build JWT response + JwtResponse jwtResponse = new JwtResponse(jwt, userDto.getId(), userDto.getName(), userDto.getEmail()); return ResponseEntity.ok(jwtResponse); } catch (BadCredentialsException e) { @@ -83,4 +58,6 @@ public ResponseEntity authenticateUser(@RequestBody LoginRequest loginRequest .body(new MessageResponse("Error: Invalid email or password")); } } + + } diff --git a/authentication-service/src/main/java/com/finpay/authentication/dtos/UserDto.java b/authentication-service/src/main/java/com/finpay/authentication/dtos/UserDto.java new file mode 100644 index 0000000..9053dbe --- /dev/null +++ b/authentication-service/src/main/java/com/finpay/authentication/dtos/UserDto.java @@ -0,0 +1,15 @@ +package com.finpay.authentication.dtos; + +import lombok.Data; + +import java.util.Set; +import java.util.UUID; + +@Data +public class UserDto { + private UUID id; + private String email; + private String password; + private String name; + private Set roles; +} diff --git a/authentication-service/src/main/java/com/finpay/authentication/dtos/UserEvent.java b/authentication-service/src/main/java/com/finpay/authentication/dtos/UserEvent.java new file mode 100644 index 0000000..2942650 --- /dev/null +++ b/authentication-service/src/main/java/com/finpay/authentication/dtos/UserEvent.java @@ -0,0 +1,11 @@ +//package com.finpay.authentication.dtos; +// +//import lombok.Data; +//import java.util.UUID; +// +//@Data +//public class UserEvent { +// private UUID id; +// private String email; +// private String roles; +//} diff --git a/authentication-service/src/main/java/com/finpay/authentication/filter/JwtAuthenticationFilter.java b/authentication-service/src/main/java/com/finpay/authentication/filter/JwtAuthenticationFilter.java index 46a9c8f..fa1cc96 100644 --- a/authentication-service/src/main/java/com/finpay/authentication/filter/JwtAuthenticationFilter.java +++ b/authentication-service/src/main/java/com/finpay/authentication/filter/JwtAuthenticationFilter.java @@ -3,6 +3,7 @@ import com.finpay.authentication.services.UserDetailsServiceImpl; import com.finpay.authentication.utils.JwtUtil; import org.springframework.security.authentication.*; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.filter.OncePerRequestFilter; @@ -11,6 +12,9 @@ import jakarta.servlet.*; import jakarta.servlet.http.*; import java.io.IOException; +import java.util.List; +import java.util.Set; +import org.springframework.security.core.authority.SimpleGrantedAuthority; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -38,21 +42,18 @@ protected void doFilterInternal(HttpServletRequest request, } if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = userDetailsService.loadUserByUsername(email); - if (jwtUtil.validateJwtToken(jwt)) { - UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails( - new WebAuthenticationDetailsSource().buildDetails(request) - ); - SecurityContextHolder.getContext().setAuthentication(authToken); - } + Set roles = jwtUtil.getRolesFromJwtToken(jwt); // Add method to extract roles + List authorities = roles.stream() + .map(SimpleGrantedAuthority::new) + .toList(); + + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(email, null, authorities); + + SecurityContextHolder.getContext().setAuthentication(authToken); } filterChain.doFilter(request, response); } + } diff --git a/authentication-service/src/main/java/com/finpay/authentication/models/User.java b/authentication-service/src/main/java/com/finpay/authentication/models/User.java deleted file mode 100644 index caa3dc5..0000000 --- a/authentication-service/src/main/java/com/finpay/authentication/models/User.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.finpay.authentication.models; -import jakarta.persistence.*; -import lombok.*; - -import java.util.UUID; - -@Entity -@Table(name = "users") -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class User { - - @Id - @GeneratedValue - private UUID id; - - @Column(nullable = false, unique = true) - private String email; - - @Column(nullable = false) - private String password; - - @Column(nullable = false) - private String name; - - -} diff --git a/authentication-service/src/main/java/com/finpay/authentication/repository/UserRepository.java b/authentication-service/src/main/java/com/finpay/authentication/repository/UserRepository.java deleted file mode 100644 index 826e577..0000000 --- a/authentication-service/src/main/java/com/finpay/authentication/repository/UserRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.finpay.authentication.repository; -import com.finpay.authentication.models.User; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; -import java.util.UUID; - -public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); - boolean existsByEmail(String email); -} \ No newline at end of file diff --git a/authentication-service/src/main/java/com/finpay/authentication/services/UserDetailsServiceImpl.java b/authentication-service/src/main/java/com/finpay/authentication/services/UserDetailsServiceImpl.java index ec3d514..f51ddd5 100644 --- a/authentication-service/src/main/java/com/finpay/authentication/services/UserDetailsServiceImpl.java +++ b/authentication-service/src/main/java/com/finpay/authentication/services/UserDetailsServiceImpl.java @@ -1,8 +1,6 @@ package com.finpay.authentication.services; - -import com.finpay.authentication.models.User; -import com.finpay.authentication.repository.UserRepository; -import org.springframework.security.core.*; +import com.finpay.authentication.clients.UserServiceClient; +import com.finpay.authentication.dtos.UserDto; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.*; import org.springframework.stereotype.Service; @@ -10,29 +8,27 @@ import java.util.*; @Service + public class UserDetailsServiceImpl implements UserDetailsService { - private final UserRepository userRepository; + private final UserServiceClient userServiceClient; // REST or gRPC client to fetch user data - // Constructor-based injection - public UserDetailsServiceImpl(UserRepository userRepository) { - this.userRepository = userRepository; + public UserDetailsServiceImpl(UserServiceClient userServiceClient) { + this.userServiceClient = userServiceClient; } @Override - public UserDetails loadUserByUsername(String email) - throws UsernameNotFoundException { - User user = userRepository.findByEmail(email) - .orElseThrow(() -> - new UsernameNotFoundException("User Not Found with email: " + email)); + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + UserDto userDto = userServiceClient.getUserByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User Not Found with email: " + email)); - // For simplicity, all users have ROLE_USER - List authorities = - Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + List authorities = userDto.getRoles().stream() + .map(SimpleGrantedAuthority::new) + .toList(); return new org.springframework.security.core.userdetails.User( - user.getEmail(), - user.getPassword(), + userDto.getEmail(), + userDto.getPassword(), authorities ); } diff --git a/authentication-service/src/main/java/com/finpay/authentication/utils/JwtUtil.java b/authentication-service/src/main/java/com/finpay/authentication/utils/JwtUtil.java index 353a94b..ee203d6 100644 --- a/authentication-service/src/main/java/com/finpay/authentication/utils/JwtUtil.java +++ b/authentication-service/src/main/java/com/finpay/authentication/utils/JwtUtil.java @@ -8,9 +8,12 @@ import javax.crypto.SecretKey; import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; @Component -@Slf4j // Lombok annotation to add SLF4J logging +@Slf4j public class JwtUtil { @Value("${jwt.secret}") @@ -20,23 +23,23 @@ public class JwtUtil { private Long jwtExpirationMs; private SecretKey getSigningKey() { - // Use the proper secret key from jwtSecret return Keys.hmacShaKeyFor(jwtSecret.getBytes()); } // Generate JWT Token - public String generateJwtToken(String email) { + public String generateJwtToken(String email, Set roles) { return Jwts.builder() .setSubject(email) + .claim("roles", roles) .setIssuedAt(new Date()) .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) - .signWith(getSigningKey(), SignatureAlgorithm.HS512) // Updated to use SecretKey + .signWith(getSigningKey(), SignatureAlgorithm.HS512) .compact(); } // Get Email from JWT Token public String getEmailFromJwtToken(String token) { - return Jwts.parserBuilder() // Updated to use parserBuilder + return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) @@ -44,6 +47,20 @@ public String getEmailFromJwtToken(String token) { .getSubject(); } + // Get Roles from JWT Token + public Set getRolesFromJwtToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + @SuppressWarnings("unchecked") + Set roles = new HashSet<>(((List) claims.get("roles"))); + + return roles; + } + // Validate JWT Token public boolean validateJwtToken(String authToken) { try { diff --git a/payment-service/pom.xml b/payment-service/pom.xml index 413b88d..9e8f1a4 100644 --- a/payment-service/pom.xml +++ b/payment-service/pom.xml @@ -38,13 +38,43 @@ org.springframework.boot spring-boot-starter-web + + com.stripe + stripe-java + 28.2.0 + + + jakarta.el + jakarta.el-api + 6.0.1 + + + jakarta.validation + jakarta.validation-api + 3.1.0 + + + org.hibernate.validator + hibernate-validator + 8.0.1.Final + + + + + + org.springframework.boot spring-boot-devtools runtime true + + org.postgresql + postgresql + runtime + org.projectlombok lombok @@ -56,9 +86,27 @@ test - org.springframework.boot - spring-boot-starter-data-jpa + com.github.docker-java + docker-java-api + 3.4.0 + compile + + org.springframework.security + spring-security-oauth2-resource-server + + + org.springframework.security + spring-security-config + + + + org.jetbrains + annotations + 17.0.0 + compile + + diff --git a/payment-service/src/main/java/com/finpay/payment_service/config/MpesaConfig.java b/payment-service/src/main/java/com/finpay/payment_service/config/MpesaConfig.java new file mode 100644 index 0000000..07d1292 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/config/MpesaConfig.java @@ -0,0 +1,21 @@ +package com.finpay.payment_service.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "mpesa") +@Data +public class MpesaConfig { + + private String consumerKey; + private String consumerSecret; + private String passkey; + private String businessShortCode; + private String initiatorName; + private String initiatorPassword; + private String callbackUrl; + private String registerUrl; + +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/config/SecurityConfig.java b/payment-service/src/main/java/com/finpay/payment_service/config/SecurityConfig.java new file mode 100644 index 0000000..a253d66 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/config/SecurityConfig.java @@ -0,0 +1,44 @@ +package com.finpay.payment_service.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableMethodSecurity(prePostEnabled = true) +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/payments/initiate").hasAnyRole("USER", "ADMIN") + .requestMatchers("/api/payments/refund").hasAnyRole("USER", "ADMIN") + .requestMatchers("/api/payments/**").authenticated() + .anyRequest().permitAll() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(jwtAuthenticationConverter()) + ) + ); + + return http.build(); + } + + private JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); // Ensure this matches your JWT + grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); + + JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter(); + authenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return authenticationConverter; + } +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/CardPaymentRequest.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/CardPaymentRequest.java new file mode 100644 index 0000000..6fd7e16 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/CardPaymentRequest.java @@ -0,0 +1,31 @@ +package com.finpay.payment_service.dtos; +import lombok.Data; +//import jakarta.validation.constraints.NotBlank; +//import jakarta.validation.constraints.Pattern; +@Data +public class CardPaymentRequest { + +// @NotBlank(message = "Card number is required") +// @Pattern(regexp = "^[0-9]{13,19}$", message = "Invalid card number") + + private String cardNumber; + +// @NotBlank(message = "Expiry month is required") +// @Pattern(regexp = "^(0[1-9]|1[0-2])$", message = "Invalid expiry month") + private String expiryMonth; + +// @NotBlank(message = "Expiry year is required") +// @Pattern(regexp = "^[0-9]{2}$", message = "Invalid expiry year") + private String expiryYear; + +// @NotBlank(message = "CVV is required") +// @Pattern(regexp = "^[0-9]{3,4}$", message = "Invalid CVV") + private String cvv; + +// @NotBlank(message = "Cardholder name is required") + private String cardHolderName; + +// @NotBlank(message = "Billing address is required") + private String billingAddress; + +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/ErrorResponse.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/ErrorResponse.java new file mode 100644 index 0000000..424d71e --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/ErrorResponse.java @@ -0,0 +1,17 @@ +package com.finpay.payment_service.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class ErrorResponse { + private UUID errorId; + private LocalDateTime timestamp; + private int status; + private String error; + private String message; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/InvoiceResponse.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/InvoiceResponse.java new file mode 100644 index 0000000..2a31be2 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/InvoiceResponse.java @@ -0,0 +1,16 @@ +package com.finpay.payment_service.dtos; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.UUID; + +@Data +public class InvoiceResponse { + private UUID id; + private UUID userId; + private BigDecimal totalAmount; + private String currency; + private String status; + // Add other relevant fields as necessary +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentGatewayResponse.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentGatewayResponse.java new file mode 100644 index 0000000..5af1a5c --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentGatewayResponse.java @@ -0,0 +1,32 @@ +package com.finpay.payment_service.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PaymentGatewayResponse { + /** + * Status of the payment operation. + * Possible values: SUCCESS, FAILURE + */ + private String status; + + /** + * Message providing additional information about the payment operation. + */ + private String message; + + /** + * Unique identifier for the payment, provided by the payment gateway. + * Example: A transaction ID or reference number. + */ + private String paymentId; + + /** + * Additional details or metadata about the payment (optional). + */ + private String additionalDetails; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentRequest.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentRequest.java new file mode 100644 index 0000000..5c03ebe --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentRequest.java @@ -0,0 +1,21 @@ +package com.finpay.payment_service.dtos; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.UUID; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PaymentRequest { + private BigDecimal amount; + private String currency; + private String paymentMethod; + private UUID invoiceId; + private String paymentGateway; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentResponse.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentResponse.java new file mode 100644 index 0000000..0a82e94 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/PaymentResponse.java @@ -0,0 +1,21 @@ +package com.finpay.payment_service.dtos; + +import com.finpay.payment_service.models.PaymentStatus; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class PaymentResponse { + private UUID id; + private String paymentReference; + private BigDecimal amount; + private String currency; + private PaymentStatus status; + private UUID invoiceId; + private String paymentGateway; + private String message; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundGatewayResponse.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundGatewayResponse.java new file mode 100644 index 0000000..3bb3a7b --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundGatewayResponse.java @@ -0,0 +1,32 @@ +package com.finpay.payment_service.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RefundGatewayResponse { + /** + * Status of the refund operation. + * Possible values: SUCCESS, FAILURE + */ + private String status; + + /** + * Message providing additional information about the refund operation. + */ + private String message; + + /** + * Unique identifier for the refund, provided by the payment gateway. + * Example: A refund transaction ID or reference number. + */ + private String refundId; + + /** + * Additional details or metadata about the refund (optional). + */ + private String additionalDetails; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundRequest.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundRequest.java new file mode 100644 index 0000000..607c5d9 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundRequest.java @@ -0,0 +1,13 @@ +package com.finpay.payment_service.dtos; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.UUID; + +@Data +public class RefundRequest { + private UUID paymentId; + private BigDecimal amount; + private String reason; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundResponse.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundResponse.java new file mode 100644 index 0000000..dad76b8 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/RefundResponse.java @@ -0,0 +1,20 @@ +package com.finpay.payment_service.dtos; + +import com.finpay.payment_service.models.RefundStatus; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class RefundResponse { + private UUID id; + private String refundReference; + private RefundStatus status; + private BigDecimal amount; + private String reason; + private LocalDateTime createdAt; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/TransactionRequest.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/TransactionRequest.java new file mode 100644 index 0000000..813c516 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/TransactionRequest.java @@ -0,0 +1,11 @@ +package com.finpay.payment_service.dtos; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class TransactionRequest { + private String transactionType; + private BigDecimal amount; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/dtos/TransactionResponse.java b/payment-service/src/main/java/com/finpay/payment_service/dtos/TransactionResponse.java new file mode 100644 index 0000000..b9b0558 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/dtos/TransactionResponse.java @@ -0,0 +1,21 @@ +package com.finpay.payment_service.dtos; + +import com.finpay.payment_service.models.TransactionStatus; +import com.finpay.payment_service.models.TransactionType; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class TransactionResponse { + private UUID id; + private String transactionReference; + private TransactionType type; + private TransactionStatus status; + private BigDecimal amount; + private LocalDateTime createdAt; +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/exceptions/GlobalExceptionHandler.java b/payment-service/src/main/java/com/finpay/payment_service/exceptions/GlobalExceptionHandler.java new file mode 100644 index 0000000..b903052 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,48 @@ +package com.finpay.payment_service.exceptions; +import com.finpay.payment_service.dtos.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.UUID; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleBadRequest(Exception ex) { + ErrorResponse error = new ErrorResponse( + UUID.randomUUID(), + LocalDateTime.now(), + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage() + ); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleConflict(Exception ex) { + ErrorResponse error = new ErrorResponse( + UUID.randomUUID(), + LocalDateTime.now(), + HttpStatus.CONFLICT.value(), + "Conflict", + ex.getMessage() + ); + return new ResponseEntity<>(error, HttpStatus.CONFLICT); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleInternalServerError(Exception ex) { + ErrorResponse error = new ErrorResponse( + UUID.randomUUID(), + LocalDateTime.now(), + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "Internal Server Error", + "An unexpected error occurred." + ); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/gateways/PaymentGateway.java b/payment-service/src/main/java/com/finpay/payment_service/gateways/PaymentGateway.java new file mode 100644 index 0000000..82fb05c --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/gateways/PaymentGateway.java @@ -0,0 +1,25 @@ +package com.finpay.payment_service.gateways; + +import com.finpay.payment_service.dtos.PaymentRequest; +import com.finpay.payment_service.dtos.PaymentGatewayResponse; +import com.finpay.payment_service.dtos.RefundRequest; +import com.finpay.payment_service.dtos.RefundGatewayResponse; + +public interface PaymentGateway { + + /** + * Processes a payment through the specific gateway. + * + * @param paymentRequest The payment details. + * @return The response from the payment gateway. + */ + PaymentGatewayResponse processPayment(PaymentRequest paymentRequest); + + /** + * Processes a refund through the specific gateway. + * + * @param refundRequest The refund details. + * @return The response from the refund gateway. + */ + RefundGatewayResponse processRefund(RefundRequest refundRequest); +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/gateways/mpesa/MpesaPaymentGateway.java b/payment-service/src/main/java/com/finpay/payment_service/gateways/mpesa/MpesaPaymentGateway.java new file mode 100644 index 0000000..87a033f --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/gateways/mpesa/MpesaPaymentGateway.java @@ -0,0 +1,165 @@ +package com.finpay.payment_service.gateways.mpesa; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.finpay.payment_service.dtos.PaymentGatewayResponse; +import com.finpay.payment_service.dtos.RefundGatewayResponse; +import com.finpay.payment_service.gateways.PaymentGateway; +import com.finpay.payment_service.config.MpesaConfig; +import com.finpay.payment_service.model.MpesaTransaction; +import com.finpay.payment_service.repository.MpesaTransactionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.Base64; + +@Component +@RequiredArgsConstructor +public class MpesaPaymentGateway implements PaymentGateway { + + private final MpesaConfig mpesaConfig; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + private final MpesaTransactionRepository mpesaTransactionRepository; + + /** + * Processes a payment through M-Pesa STK-PUSH. + * + * @param paymentRequest The payment details. + * @return The response from M-Pesa. + */ + @Override + public PaymentGatewayResponse processPayment(com.finpay.payment_service.dtos.PaymentRequest paymentRequest) { + PaymentGatewayResponse response = new PaymentGatewayResponse(); + + try { + String accessToken = getAccessToken(); + + String url = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"; + + String timestamp = LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String password = Base64.getEncoder().encodeToString( + (mpesaConfig.getBusinessShortCode() + mpesaConfig.getPasskey() + timestamp).getBytes() + ); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + + JsonNode payload = objectMapper.createObjectNode() + .put("BusinessShortCode", mpesaConfig.getBusinessShortCode()) + .put("Password", password) + .put("Timestamp", timestamp) + .put("TransactionType", "CustomerPayBillOnline") + .put("Amount", paymentRequest.getAmount()) + .put("PartyA", paymentRequest.getPhoneNumber()) + .put("PartyB", mpesaConfig.getBusinessShortCode()) + .put("PhoneNumber", paymentRequest.getPhoneNumber()) + .put("CallBackURL", mpesaConfig.getCallbackUrl()) + .put("AccountReference", paymentRequest.getAccountReference()) + .put("TransactionDesc", paymentRequest.getTransactionDesc()); + + HttpEntity entity = new HttpEntity<>(objectMapper.writeValueAsString(payload), headers); + + ResponseEntity apiResponse = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); + + if (apiResponse.getStatusCode() == HttpStatus.OK) { + JsonNode responseBody = objectMapper.readTree(apiResponse.getBody()); + + String merchantRequestID = responseBody.get("MerchantRequestID").asText(); + String checkoutRequestID = responseBody.get("CheckoutRequestID").asText(); + String responseCode = responseBody.get("ResponseCode").asText(); + String responseDescription = responseBody.get("ResponseDescription").asText(); + String customerMessage = responseBody.get("CustomerMessage").asText(); + + // Save transaction details + MpesaTransaction transaction = MpesaTransaction.builder() + .merchantRequestID(merchantRequestID) + .checkoutRequestID(checkoutRequestID) + .amount(paymentRequest.getAmount()) + .phoneNumber(paymentRequest.getPhoneNumber()) + .status("PENDING") + .createdAt(LocalDateTime.now()) + .build(); + + mpesaTransactionRepository.save(transaction); + + response.setStatus("SUCCESS"); + response.setMessage("STK-PUSH initiated successfully."); + response.setPaymentReference(checkoutRequestID); + } else { + response.setStatus("FAILURE"); + response.setMessage("Failed to initiate STK-PUSH."); + } + } catch (Exception e) { + response.setStatus("FAILURE"); + response.setMessage("Error processing M-Pesa payment: " + e.getMessage()); + // Optionally, log the error using a logger + } + + return response; + } + + /** + * Processes a refund through M-Pesa. + * + * @param refundRequest The refund details. + * @return The response from M-Pesa. + */ + @Override + public RefundGatewayResponse processRefund(com.finpay.payment_service.dtos.RefundRequest refundRequest) { + RefundGatewayResponse response = new RefundGatewayResponse(); + + // Implement refund logic if M-Pesa supports it. Currently, M-Pesa's C2B API does not support refunds. + // This is a placeholder for future implementation. + + response.setStatus("FAILED"); + response.setMessage("Refunds are not supported through M-Pesa STK-PUSH."); + return response; + } + + /** + * Retrieves the current access token, fetching a new one if necessary. + * + * @return The access token. + */ + private String getAccessToken() { + // Implement caching logic with Redis or similar if desired + String accessToken = fetchAccessToken(); + return accessToken; + } + + /** + * Fetches a new access token from M-Pesa. + * + * @return The new access token. + */ + private String fetchAccessToken() { + try { + String url = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"; + + String credentials = mpesaConfig.getConsumerKey() + ":" + mpesaConfig.getConsumerSecret(); + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Basic " + encodedCredentials); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity entity = new HttpEntity<>(null, headers); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + if (response.getStatusCode() == HttpStatus.OK) { + JsonNode node = objectMapper.readTree(response.getBody()); + return node.get("access_token").asText(); + } else { + throw new RuntimeException("Failed to fetch access token from M-Pesa"); + } + } catch (Exception e) { + throw new RuntimeException("Error fetching access token: " + e.getMessage(), e); + } + } +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/models/Payment.java b/payment-service/src/main/java/com/finpay/payment_service/models/Payment.java index 1fe0e72..def3021 100644 --- a/payment-service/src/main/java/com/finpay/payment_service/models/Payment.java +++ b/payment-service/src/main/java/com/finpay/payment_service/models/Payment.java @@ -1,7 +1,12 @@ package com.finpay.payment_service.models; +import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.Set; import java.util.UUID; @@ -18,7 +23,7 @@ public class Payment { private UUID id; @Column(nullable = false, unique = true) - private String paymentReference; + private String paymentReference; // Unique reference for the payment @Column(nullable = false) private BigDecimal amount; @@ -30,13 +35,11 @@ public class Payment { @Column(nullable = false) private PaymentStatus status; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "payment_method_id") - private PaymentMethod paymentMethod; + @Column(nullable = false) + private UUID invoiceId; // Reference to Invoice managed by Invoice Service - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "payment_gateway_id") - private PaymentGateway paymentGateway; + @Column(nullable = false) + private String paymentGateway; // e.g., Stripe, PayPal @OneToMany(mappedBy = "payment", cascade = CascadeType.ALL, orphanRemoval = true) private Set transactions; @@ -44,8 +47,10 @@ public class Payment { @OneToMany(mappedBy = "payment", cascade = CascadeType.ALL, orphanRemoval = true) private Set refunds; - @Column(nullable = false) + @CreationTimestamp + @Column(nullable = false, updatable = false) private LocalDateTime createdAt; + @UpdateTimestamp private LocalDateTime updatedAt; } diff --git a/payment-service/src/main/java/com/finpay/payment_service/models/PaymentGateway.java b/payment-service/src/main/java/com/finpay/payment_service/models/PaymentGateway.java index 774a153..5a5f3d5 100644 --- a/payment-service/src/main/java/com/finpay/payment_service/models/PaymentGateway.java +++ b/payment-service/src/main/java/com/finpay/payment_service/models/PaymentGateway.java @@ -1,5 +1,6 @@ package com.finpay.payment_service.models; +import jakarta.persistence.*; import lombok.*; import java.util.Set; import java.util.UUID; diff --git a/payment-service/src/main/java/com/finpay/payment_service/models/PaymentMethod.java b/payment-service/src/main/java/com/finpay/payment_service/models/PaymentMethod.java index 3c8324a..71f8894 100644 --- a/payment-service/src/main/java/com/finpay/payment_service/models/PaymentMethod.java +++ b/payment-service/src/main/java/com/finpay/payment_service/models/PaymentMethod.java @@ -1,5 +1,6 @@ package com.finpay.payment_service.models; +import jakarta.persistence.*; import lombok.*; import java.util.Set; import java.util.UUID; diff --git a/payment-service/src/main/java/com/finpay/payment_service/models/Refund.java b/payment-service/src/main/java/com/finpay/payment_service/models/Refund.java index de23413..fec4839 100644 --- a/payment-service/src/main/java/com/finpay/payment_service/models/Refund.java +++ b/payment-service/src/main/java/com/finpay/payment_service/models/Refund.java @@ -1,5 +1,6 @@ package com.finpay.payment_service.models; +import jakarta.persistence.*; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; diff --git a/payment-service/src/main/java/com/finpay/payment_service/models/Transaction.java b/payment-service/src/main/java/com/finpay/payment_service/models/Transaction.java index 31aabfd..4e1509a 100644 --- a/payment-service/src/main/java/com/finpay/payment_service/models/Transaction.java +++ b/payment-service/src/main/java/com/finpay/payment_service/models/Transaction.java @@ -4,6 +4,7 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.UUID; +import jakarta.persistence.*; @Entity @Table(name = "transactions") diff --git a/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentGatewayRepository.java b/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentGatewayRepository.java new file mode 100644 index 0000000..d190941 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentGatewayRepository.java @@ -0,0 +1,10 @@ +package com.finpay.payment_service.repository; + +import com.finpay.payment_service.models.PaymentGateway; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; +import java.util.UUID; + +public interface PaymentGatewayRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentMethodRepository.java b/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentMethodRepository.java new file mode 100644 index 0000000..ae2b4b9 --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentMethodRepository.java @@ -0,0 +1,10 @@ +package com.finpay.payment_service.repository; + +import com.finpay.payment_service.models.PaymentMethod; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; +import java.util.UUID; + +public interface PaymentMethodRepository extends JpaRepository { + Optional findByType(String type); +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentRepository.java b/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentRepository.java new file mode 100644 index 0000000..1dbf0fb --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/repository/PaymentRepository.java @@ -0,0 +1,10 @@ +package com.finpay.payment_service.repository; + +import com.finpay.payment_service.models.Payment; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; +import java.util.UUID; + +public interface PaymentRepository extends JpaRepository { + Optional findByPaymentReference(String paymentReference); +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/repository/RefundRepository.java b/payment-service/src/main/java/com/finpay/payment_service/repository/RefundRepository.java new file mode 100644 index 0000000..35938fa --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/repository/RefundRepository.java @@ -0,0 +1,10 @@ +package com.finpay.payment_service.repository; + +import com.finpay.payment_service.models.Refund; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; +import java.util.UUID; + +public interface RefundRepository extends JpaRepository { + Optional findByRefundReference(String refundReference); +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/repository/TransactionRepository.java b/payment-service/src/main/java/com/finpay/payment_service/repository/TransactionRepository.java new file mode 100644 index 0000000..30b864f --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/repository/TransactionRepository.java @@ -0,0 +1,10 @@ +package com.finpay.payment_service.repository; + +import com.finpay.payment_service.models.Transaction; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; +import java.util.UUID; + +public interface TransactionRepository extends JpaRepository { + Optional findByTransactionReference(String transactionReference); +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/services/PaymentService.java b/payment-service/src/main/java/com/finpay/payment_service/services/PaymentService.java new file mode 100644 index 0000000..4e13a1a --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/services/PaymentService.java @@ -0,0 +1,14 @@ +package com.finpay.payment_service.services; + +import com.finpay.payment_service.dtos.PaymentRequest; +import com.finpay.payment_service.dtos.PaymentResponse; +import com.finpay.payment_service.dtos.RefundRequest; +import com.finpay.payment_service.dtos.RefundResponse; + +import java.util.Optional; +public interface PaymentService { + PaymentResponse initiatePayment(PaymentRequest paymentRequest); + Optional getPaymentByReference(String paymentReference); + RefundResponse processRefund(RefundRequest refundRequest); + +} diff --git a/payment-service/src/main/java/com/finpay/payment_service/services/PaymentServiceImpl.java b/payment-service/src/main/java/com/finpay/payment_service/services/PaymentServiceImpl.java new file mode 100644 index 0000000..bd23b3f --- /dev/null +++ b/payment-service/src/main/java/com/finpay/payment_service/services/PaymentServiceImpl.java @@ -0,0 +1,162 @@ +package com.finpay.payment_service.services; + +import com.finpay.payment_service.dtos.*; +import com.finpay.payment_service.gateways.PaymentGateway; +import com.finpay.payment_service.gateways.PaymentGatewayFactory; +import com.finpay.payment_service.models.Payment; +import com.finpay.payment_service.models.PaymentStatus; +import com.finpay.payment_service.models.Refund; +import com.finpay.payment_service.models.RefundStatus; +import com.finpay.payment_service.repository.PaymentRepository; +import com.finpay.payment_service.repository.RefundRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; + +@Service +public class PaymentServiceImpl implements PaymentService { + + private final PaymentRepository paymentRepository; + private final RefundRepository refundRepository; + //private final InvoiceServiceClient invoiceServiceClient; + private final PaymentGatewayFactory paymentGatewayFactory; + + public PaymentServiceImpl(PaymentRepository paymentRepository, + RefundRepository refundRepository, +// InvoiceServiceClient invoiceServiceClient, + PaymentGatewayFactory paymentGatewayFactory) { + this.paymentRepository = paymentRepository; + this.refundRepository = refundRepository; +// this.invoiceServiceClient = invoiceServiceClient; + this.paymentGatewayFactory = paymentGatewayFactory; + } + + @Override + @Transactional + public PaymentResponse initiatePayment(PaymentRequest paymentRequest) { + // Validate Invoice +// InvoiceResponse invoice = invoiceServiceClient.getInvoiceById(paymentRequest.getInvoiceId()); +// if (invoice == null || !"PAID".equalsIgnoreCase(invoice.getStatus())) { +// throw new IllegalArgumentException("Invalid or unpaid invoice."); +// } + + // Create Payment entity + Payment payment = Payment.builder() + .paymentReference(UUID.randomUUID().toString()) + .amount(paymentRequest.getAmount()) + .currency(paymentRequest.getCurrency()) + .status(PaymentStatus.PENDING) + .invoiceId(paymentRequest.getInvoiceId()) + .paymentGateway(paymentRequest.getPaymentGateway()) + .createdAt(LocalDateTime.now()) + .build(); + + // Save Payment + paymentRepository.save(payment); + + // Get the appropriate PaymentGateway + PaymentGateway gateway = paymentGatewayFactory.getPaymentGateway(paymentRequest); + + // Process Payment via Gateway + PaymentGatewayResponse gatewayResponse = gateway.processPayment(paymentRequest); + + // Update Payment Status based on Gateway Response + if ("SUCCESS".equalsIgnoreCase(gatewayResponse.getStatus())) { + payment.setStatus(PaymentStatus.COMPLETED); + } else { + payment.setStatus(PaymentStatus.FAILED); + } + payment.setUpdatedAt(LocalDateTime.now()); + paymentRepository.save(payment); + + // Return Response + return new PaymentResponse( + payment.getId(), + payment.getPaymentReference(), + payment.getAmount(), + payment.getCurrency(), + payment.getStatus(), + payment.getInvoiceId(), + payment.getPaymentGateway(), + gatewayResponse.getMessage() + ); + } + + @Override + public Optional getPaymentByReference(String paymentReference) { + return paymentRepository.findByPaymentReference(paymentReference) + .map(payment -> new PaymentResponse( + payment.getId(), + payment.getPaymentReference(), + payment.getAmount(), + payment.getCurrency(), + payment.getStatus(), + payment.getInvoiceId(), + payment.getPaymentGateway(), + "Payment retrieved successfully." + )); + } + + @Override + @Transactional + public RefundResponse processRefund(RefundRequest refundRequest) { + // Validate Payment + Optional paymentOpt = paymentRepository.findById(refundRequest.getPaymentId()); + if (paymentOpt.isEmpty()) { + throw new IllegalArgumentException("Payment not found."); + } + + Payment payment = paymentOpt.get(); + if (payment.getStatus() != PaymentStatus.COMPLETED) { + throw new IllegalStateException("Only completed payments can be refunded."); + } + + // Create Refund entity + Refund refund = Refund.builder() + .refundReference(UUID.randomUUID().toString()) + .status(RefundStatus.INITIATED) + .amount(refundRequest.getAmount()) + .payment(payment) + .reason(refundRequest.getReason()) + .createdAt(LocalDateTime.now()) + .build(); + + // Save Refund + refundRepository.save(refund); + + // Get the appropriate PaymentGateway + //PaymentRequest dummyPaymentRequest = new PaymentRequest(); // Not used here + PaymentGateway gateway = paymentGatewayFactory.getPaymentGateway( + PaymentRequest.builder() + .paymentMethod(payment.getPaymentGateway()) + .build() + ); + + // Process Refund via Gateway + RefundGatewayResponse gatewayResponse = gateway.processRefund(refundRequest); + + // Update Refund Status based on Gateway Response + if ("SUCCESS".equalsIgnoreCase(gatewayResponse.getStatus())) { + refund.setStatus(RefundStatus.COMPLETED); + payment.setStatus(PaymentStatus.REFUNDED); + } else { + refund.setStatus(RefundStatus.FAILED); + } + refund.setUpdatedAt(LocalDateTime.now()); + refundRepository.save(refund); + paymentRepository.save(payment); + + // Return Response + return new RefundResponse( + refund.getId(), + refund.getRefundReference(), + refund.getStatus(), + refund.getAmount(), + refund.getReason(), + refund.getCreatedAt() + ); + } +} diff --git a/payment-service/src/main/resources/application.properties b/payment-service/src/main/resources/application.properties deleted file mode 100644 index 85f2700..0000000 --- a/payment-service/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=payment-service diff --git a/payment-service/src/main/resources/application.yml b/payment-service/src/main/resources/application.yml new file mode 100644 index 0000000..e690158 --- /dev/null +++ b/payment-service/src/main/resources/application.yml @@ -0,0 +1,21 @@ +server: + port: 8081 + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/finpay_payment + username: postgres + password: 0412@zeP + driver-class-name: org.postgresql.Driver + jpa: + hibernate: + ddl-auto: update + show-sql: true + properties: + hibernate: + format_sql: true + +eureka: + client: + service-url: + defaultZone: http://localhost:8761/eureka/ diff --git a/user-service/pom.xml b/user-service/pom.xml index 1382c65..518db85 100644 --- a/user-service/pom.xml +++ b/user-service/pom.xml @@ -47,6 +47,10 @@ org.springframework.boot spring-boot-starter-oauth2-resource-server + + org.springframework.kafka + spring-kafka + org.springframework.boot diff --git a/user-service/src/main/java/com/finpay/user_service/config/SecurityConfig.java b/user-service/src/main/java/com/finpay/user_service/config/SecurityConfig.java index 3708ca3..aa75a06 100644 --- a/user-service/src/main/java/com/finpay/user_service/config/SecurityConfig.java +++ b/user-service/src/main/java/com/finpay/user_service/config/SecurityConfig.java @@ -21,6 +21,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/users/register").permitAll() + .requestMatchers("/api/users/{id}/roles").hasRole("ADMIN") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 diff --git a/user-service/src/main/java/com/finpay/user_service/controllers/UserController.java b/user-service/src/main/java/com/finpay/user_service/controllers/UserController.java index 8c443bb..0c291fb 100644 --- a/user-service/src/main/java/com/finpay/user_service/controllers/UserController.java +++ b/user-service/src/main/java/com/finpay/user_service/controllers/UserController.java @@ -5,6 +5,7 @@ import com.finpay.user_service.dtos.UserResponse; import com.finpay.user_service.models.Role; import com.finpay.user_service.models.User; +//import com.finpay.user_service.services.KafkaProducerService; import com.finpay.user_service.services.UserService; import org.springframework.http.*; import org.springframework.security.access.prepost.PreAuthorize; @@ -20,12 +21,14 @@ public class UserController { private final UserService userService; +// private final KafkaProducerService kafkaProducerService; // Constructor-based injection public UserController(UserService userService) { this.userService = userService; } @PostMapping("/register") + public ResponseEntity registerUser(@RequestBody UserRegistrationRequest registrationRequest) { if (userService.existsByEmail(registrationRequest.getEmail())) { return ResponseEntity @@ -33,7 +36,6 @@ public ResponseEntity registerUser(@RequestBody UserRegistrationRequest regis .body("Error: Email is already in use!"); } - // Assign default role Set defaultRoles = Set.of(Role.ROLE_USER); User user = User.builder() @@ -46,17 +48,35 @@ public ResponseEntity registerUser(@RequestBody UserRegistrationRequest regis User savedUser = userService.save(user); - UserResponse userResponse = new UserResponse(savedUser.getId(), savedUser.getName(), savedUser.getEmail()); + // Publish event to Kafka + String event = String.format("{\"id\": \"%s\", \"email\": \"%s\", \"roles\": \"%s\"}", + savedUser.getId(), + savedUser.getEmail(), + defaultRoles); +// kafkaProducerService.sendMessage("user-registration", event); + UserResponse userResponse = new UserResponse(savedUser.getId(), savedUser.getName(), savedUser.getEmail()); return ResponseEntity.status(HttpStatus.CREATED).body(userResponse); } + @GetMapping("/email/{email}") + public ResponseEntity getUserByEmail(@PathVariable String email) { + Optional userOpt = userService.findByEmail(email); + + if (userOpt.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); + } + + User user = userOpt.get(); + UserResponse userResponse = new UserResponse(user.getId(), user.getName(), user.getEmail()); + return ResponseEntity.ok(userResponse); + } @GetMapping("/profile") public ResponseEntity getUserProfile(Authentication authentication) { String email = authentication.getName(); Optional userOpt = userService.findByEmail(email); - if (!userOpt.isPresent()) { + if (userOpt.isEmpty()) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); }