Skip to content

Commit

Permalink
added customers api
Browse files Browse the repository at this point in the history
  • Loading branch information
harmlessprince committed Feb 16, 2025
1 parent c677241 commit 0c7d9ec
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 22 deletions.
27 changes: 25 additions & 2 deletions src/main/java/com/harmlessprince/ecommerceApi/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
import org.springframework.security.core.context.SecurityContextHolder;

import java.lang.reflect.Field;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;


public class Utils {
Expand All @@ -29,7 +33,7 @@ public static double percentageOf(double percentage, double total) {
if (percentage > 100) {
percentage = 100;
}
return (percentage / 100) * total;
return (percentage / 100) * total;
}

public static double roundTo(double number, int places) {
Expand Down Expand Up @@ -65,7 +69,6 @@ public static Map<String, Object> extractAllowedKeys(Object obj) {
}



public static Sort.Direction getSortDirectionFiled(String input) {
try {
return Sort.Direction.valueOf(input.toUpperCase());
Expand All @@ -79,6 +82,7 @@ public static Query getTenantAwareQuery() {
Query query = new Query();
return getTenantAwareQuery(query);
}

public static Query getTenantAwareQuery(Criteria criteria) {
Query query = new Query(criteria);
return getTenantAwareQuery(query);
Expand All @@ -99,4 +103,23 @@ public static int validateLimit(int limit, int max) {
if (limit < 1) return 10; // Default to 10 if below 1
return Math.min(limit, max); // Maximum cap at 20
}


public static Instant validateCursor(String cursor) {
if (cursor == null || cursor.isEmpty()) {
return null;
}
try {
// Try parsing as Instant (ISO-8601 with timezone)
return Optional.of(Instant.parse(cursor)).get();
} catch (Exception e) {
try {
// Try parsing as LocalDateTime and convert to Instant
LocalDateTime localDateTime = LocalDateTime.parse(cursor);
return Optional.of(localDateTime.atZone(ZoneId.of("UTC")).toInstant()).get();
} catch (Exception ex) {
throw new CustomBadRequestException("Invalid cursor format. Use ISO-8601 format (e.g., 2025-02-08T10:15:30Z).");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public ResponseEntity<CustomSuccessResponse<UserResponse>> registerShopOwner(@Re
createTenantRequest.setUserId(registeredUser.getId());
Tenant createdTenant = tenantService.create(createTenantRequest);
registeredUser.setTenant(createdTenant);

userService.assignTenantToUser(registeredUser, createdTenant);
CustomSuccessResponse<UserResponse> response = new CustomSuccessResponse<UserResponse>( userMapper.fromUser(registeredUser), "Registration successful");
return ResponseEntity.ok(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ public boolean userExists(String email) {
}

public boolean userExists(String email, String tenantId) {
return userRepository.findByEmail(email).isPresent();
return userRepository.findFirstByEmailAndTenantId(email, tenantId).isPresent();
}



}
20 changes: 20 additions & 0 deletions src/main/java/com/harmlessprince/ecommerceApi/auth/UserMapper.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
package com.harmlessprince.ecommerceApi.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.harmlessprince.ecommerceApi.customer.CustomerResponse;
import com.harmlessprince.ecommerceApi.promoAd.PromoAd;
import com.harmlessprince.ecommerceApi.promoAd.PromoAdResponse;
import com.harmlessprince.ecommerceApi.role.Role;
import com.harmlessprince.ecommerceApi.role.RoleResource;
import com.harmlessprince.ecommerceApi.tenant.Tenant;
import com.harmlessprince.ecommerceApi.user.User;
import com.harmlessprince.ecommerceApi.user.UserResponse;
import com.harmlessprince.ecommerceApi.user.UserResponseWithRole;
import com.harmlessprince.ecommerceApi.user.UserSlimResponse;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@AllArgsConstructor
@Service
public class UserMapper {
private final ObjectMapper objectMapper;
public User fromUseRequest(RegisterUserRequest request) {
return User.
builder()
Expand Down Expand Up @@ -44,4 +55,13 @@ public UserResponseWithRole fromUserWithRole(User user) {

return userResponse;
}

public List<CustomerResponse> mapToCustomerResponseList(List<User> users) {
return users.stream()
.map(ad -> objectMapper.convertValue(ad, CustomerResponse.class))
.collect(Collectors.toList());
}



}
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
package com.harmlessprince.ecommerceApi.customer;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.harmlessprince.ecommerceApi.Utils;
import com.harmlessprince.ecommerceApi.auth.AuthenticationService;
import com.harmlessprince.ecommerceApi.auth.RegisterUserRequest;
import com.harmlessprince.ecommerceApi.auth.UserMapper;
import com.harmlessprince.ecommerceApi.contexts.TenantContext;
import com.harmlessprince.ecommerceApi.custom.AppConstants;
import com.harmlessprince.ecommerceApi.custom.CustomerAccess;
import com.harmlessprince.ecommerceApi.custom.PaginatedResponse;
import com.harmlessprince.ecommerceApi.custom.ShopOwnerAccess;
import com.harmlessprince.ecommerceApi.exceptions.CustomBadRequestException;
import com.harmlessprince.ecommerceApi.exceptions.CustomResourceNotFoundException;
import com.harmlessprince.ecommerceApi.exceptions.EmailTakenException;
import com.harmlessprince.ecommerceApi.handler.CustomResponse;
import com.harmlessprince.ecommerceApi.handler.CustomSuccessResponse;

import com.harmlessprince.ecommerceApi.user.ProfileUpdateRequest;
import com.harmlessprince.ecommerceApi.user.User;
import com.harmlessprince.ecommerceApi.user.UserResponse;
import com.harmlessprince.ecommerceApi.order.OrderResponse;
import com.harmlessprince.ecommerceApi.product.ProductSortField;
import com.harmlessprince.ecommerceApi.promoAd.PromoAdResponse;
import com.harmlessprince.ecommerceApi.user.*;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@AllArgsConstructor
Expand All @@ -28,14 +43,46 @@ public class CustomerController {
private final AuthenticationService authenticationService;
private final CustomerService customerService;
private final UserMapper userMapper;
private final ObjectMapper mapper;

@GetMapping
@ShopOwnerAccess
public ResponseEntity<CustomResponse<PaginatedResponse<CustomerResponse>>> getAllCustomers(
@RequestParam(required = false) String cursor,
@RequestParam(defaultValue = "10") int limit,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "CREATED_AT") String sortFieldParam,
@RequestParam(defaultValue = "DESC") String sortDirectionParam,
@RequestParam Map<String, Object> requestParams
) {
int finalLimit = Utils.validateLimit(limit, 20);
Instant cursorInstant = Utils.validateCursor(cursor);
UserSortField sortField = customerService.getCustomerSortField(sortFieldParam);
Sort.Direction sortDirection = Utils.getSortDirectionFiled(sortDirectionParam);
PaginatedResponse<CustomerResponse> userSlimResponsePaginatedResponse = this.customerService.findAll(TenantContext.getCurrentTenantID(), sortField, sortDirection, finalLimit, cursorInstant, requestParams);
return ResponseEntity.ok(CustomResponse.sendSuccessResponse(userSlimResponsePaginatedResponse, "Customers retrieved successfully"));
}

@GetMapping("/{customerId}")
@ShopOwnerAccess
public ResponseEntity<CustomResponse<CustomerResponse>> fetchCustomer(@PathVariable() String customerId) {
Optional<User> customer = this.customerService.findById(customerId);
if(customer.isEmpty()) {
throw new CustomResourceNotFoundException("Customer not found");
}
CustomerResponse customResponse = mapper.convertValue(customer, CustomerResponse.class);
return ResponseEntity.ok(CustomResponse.sendSuccessResponse(mapper.convertValue(customer, CustomerResponse.class), "Customer retrieved successfully"));
}



@PostMapping("/signup")
public ResponseEntity<CustomSuccessResponse<UserResponse>> registerCustomer(@RequestBody @Valid RegisterUserRequest registerUserRequest) {
if (authenticationService.userExists(registerUserRequest.getEmail())) {
if (authenticationService.userExists(registerUserRequest.getEmail(), TenantContext.getCurrentTenantID())) {
throw new EmailTakenException();
}
User registeredUser = customerService.createCustomer(registerUserRequest, TenantContext.getCurrentTenantID());
CustomSuccessResponse<UserResponse> response = new CustomSuccessResponse<UserResponse>( userMapper.fromUser(registeredUser), "Customer Registration successful");
CustomSuccessResponse<UserResponse> response = new CustomSuccessResponse<UserResponse>(userMapper.fromUser(registeredUser), "Customer Registration successful");
return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.harmlessprince.ecommerceApi.customer;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

@AllArgsConstructor
@Setter
@Getter
public class CustomerResponse {
private String id;
private String email;
private String fullName;
private String phoneNumber;
private String address;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package com.harmlessprince.ecommerceApi.customer;

import com.harmlessprince.ecommerceApi.Utils;
import com.harmlessprince.ecommerceApi.auth.RegisterUserRequest;
import com.harmlessprince.ecommerceApi.auth.UserMapper;
import com.harmlessprince.ecommerceApi.custom.AppConstants;
import com.harmlessprince.ecommerceApi.custom.PaginatedResponse;
import com.harmlessprince.ecommerceApi.exceptions.CustomBadRequestException;
import com.harmlessprince.ecommerceApi.product.ProductSortField;
import com.harmlessprince.ecommerceApi.role.Role;
import com.harmlessprince.ecommerceApi.role.RoleRepository;
import com.harmlessprince.ecommerceApi.tenant.Tenant;
import com.harmlessprince.ecommerceApi.tenant.TenantRepository;
import com.harmlessprince.ecommerceApi.tenant.TenantService;
import com.harmlessprince.ecommerceApi.user.ProfileUpdateRequest;
import com.harmlessprince.ecommerceApi.user.User;
import com.harmlessprince.ecommerceApi.user.UserRepository;
import com.harmlessprince.ecommerceApi.user.*;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
Expand All @@ -27,22 +35,24 @@ public class CustomerService {
private final RoleRepository roleRepository;
private final TenantRepository tenantRepository;
private final PasswordEncoder passwordEncoder;
private final UserRepositoryFilter userRepositoryFilter;
private final UserMapper userMapper;

public User createCustomer(@Valid RegisterUserRequest request, String tenantId) {
Optional<Role> role = roleRepository.findFirstBySlug(AppConstants.CUSTOMER);
if (role.isEmpty()){
if (role.isEmpty()) {
throw new IllegalArgumentException("Registration failed please try again later");
}

Optional<Tenant> tenant = tenantRepository.findById(tenantId);
if (tenant.isEmpty()){
if (tenant.isEmpty()) {
throw new CustomBadRequestException("Tenant ID is invalid");
}

User user = userMapper.fromUseRequest(request);
user.setEmail(user.getEmail().toLowerCase());
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setTenantId(tenantId);
user.setRole(role.get());
user.setTenant(tenant.get());
return userRepository.save(user);
Expand All @@ -54,4 +64,29 @@ public User updateProfile(ProfileUpdateRequest contactAddressRequest, User user)
user.setPhoneNumber(contactAddressRequest.phoneNumber());
return userRepository.save(user);
}


public UserSortField getCustomerSortField(String input) {
try {
return UserSortField.valueOf(input.toUpperCase());
} catch (IllegalArgumentException e) {
throw new CustomBadRequestException("Invalid sort field: " + input + ". Allowed values are: "
+ Arrays.toString(UserSortField.values()));
}
}


public PaginatedResponse<CustomerResponse> findAll(String currentTenantID,
UserSortField sortField,
Sort.Direction sortDirection,
int finalLimit,
Instant cursorInstant,
Map<String, Object> requestParams) {
requestParams.put("onlyCustomer", "customer");
return userRepositoryFilter.findAll(currentTenantID, sortField, sortDirection, finalLimit, cursorInstant, requestParams);
}

public Optional<User> findById(String customerId) {
return this.userRepository.findById(customerId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
response.setHeader(AppConstants.SESSION_EXPIRES_AT_FROM_HEADER, cartSessionObj.getExpiresAt());
}
}
log.info("Get cart session {}", request.getAttribute(AppConstants.SESSION_ID_FROM_HEADER));

filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.harmlessprince.ecommerceApi.productCategory.requests;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
Expand All @@ -11,6 +12,8 @@ public class CreateCategoryRequest {
private String name;
private String categoryId;
private String parentId;

@JsonIgnore
private String tenantId;

public boolean isValid() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@
import com.harmlessprince.ecommerceApi.custom.PaginatedResponse;
import com.harmlessprince.ecommerceApi.custom.ShopOwnerAccess;
import com.harmlessprince.ecommerceApi.exceptions.CustomBadRequestException;
import com.harmlessprince.ecommerceApi.exceptions.CustomResourceNotFoundException;
import com.harmlessprince.ecommerceApi.handler.CustomResponse;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.time.Instant;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

public interface RoleRepository extends MongoRepository<Role, String> {
Optional<Role> findFirstBySlug(String slug);
Optional<Role> findFirstByName(String name);
}
Loading

0 comments on commit 0c7d9ec

Please sign in to comment.