diff --git a/.env b/.env index 27589a97cf..09f9e201b5 100644 --- a/.env +++ b/.env @@ -82,10 +82,28 @@ YAS_SERVICES_INVENTORY=http://inventory/inventory YAS_SERVICES_RATING=http://rating/rating YAS_SERVICES_SAMPLE_DATA=http://sampledata/sampledata YAS_SERVICES_RECOMMENDATION=http://recommendation/recommendation +YAS_SERVICES_DELIVERY=http://delivery/delivery + SERVER_PORT=80 # Swagger UI -URLS=[{ url: 'http://api.yas.local/product/v3/api-docs', name: 'Product' },{ url: 'http://api.yas.local/media/v3/api-docs', name: 'Media' },{ url: 'http://api.yas.local/customer/v3/api-docs', name: 'Customer' },{ url: 'http://api.yas.local/cart/v3/api-docs', name: 'Cart'},{ url: 'http://api.yas.local/rating/v3/api-docs', name: 'Rating' }, { url: 'http://api.yas.local/order/v3/api-docs', name: 'Order'},{ url: 'http://api.yas.local/payment/v3/api-docs', name: 'Payment'},{ url: 'http://api.yas.local/payment-paypal/v3/api-docs', name: 'Payment-paypal'},{ url: 'http://api.yas.local/location/v3/api-docs', name: 'Location'}, { url: 'http://api.yas.local/inventory/v3/api-docs', name: 'Inventory'},{ url: 'http://api.yas.local/tax/v3/api-docs', name: 'Tax' },{ url: 'http://api.yas.local/promotion/v3/api-docs', name: 'Promotion'},{ url: 'http://api.yas.local/search/v3/api-docs', name: 'Search'}, { url: 'http://api.yas.local/webhook/v3/api-docs', name: 'Webhook'}] +URLS='[ + { url: "http://api.yas.local/product/v3/api-docs", name: "Product" }, + { url: "http://api.yas.local/media/v3/api-docs", name: "Media" }, + { url: "http://api.yas.local/customer/v3/api-docs", name: "Customer" }, + { url: "http://api.yas.local/cart/v3/api-docs", name: "Cart" }, + { url: "http://api.yas.local/rating/v3/api-docs", name: "Rating" }, + { url: "http://api.yas.local/order/v3/api-docs", name: "Order" }, + { url: "http://api.yas.local/payment/v3/api-docs", name: "Payment" }, + { url: "http://api.yas.local/payment-paypal/v3/api-docs", name: "Payment-paypal" }, + { url: "http://api.yas.local/location/v3/api-docs", name: "Location" }, + { url: "http://api.yas.local/inventory/v3/api-docs", name: "Inventory" }, + { url: "http://api.yas.local/tax/v3/api-docs", name: "Tax" }, + { url: "http://api.yas.local/promotion/v3/api-docs", name: "Promotion" }, + { url: "http://api.yas.local/search/v3/api-docs", name: "Search" }, + { url: "http://api.yas.local/webhook/v3/api-docs", name: "Webhook" }, + { url: "http://api.yas.local/delivery/v3/api-docs", name: "Delivery" } +]' # Start all service when run docker compose up COMPOSE_FILE=docker-compose.yml:docker-compose.search.yml:docker-compose.o11y.yml diff --git a/delivery/pom.xml b/delivery/pom.xml index 45f4720bc7..bd11ab242d 100644 --- a/delivery/pom.xml +++ b/delivery/pom.xml @@ -21,4 +21,49 @@ nashtech-garage_yas-delivery + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-validation + + + com.yas + common-library + ${revision} + + + com.yas + common-library + ${revision} + tests + test-jar + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.jacoco + jacoco-maven-plugin + + + + diff --git a/delivery/src/main/java/com/yas/delivery/DeliveryApplication.java b/delivery/src/main/java/com/yas/delivery/DeliveryApplication.java index f521563579..272decba3b 100644 --- a/delivery/src/main/java/com/yas/delivery/DeliveryApplication.java +++ b/delivery/src/main/java/com/yas/delivery/DeliveryApplication.java @@ -1,9 +1,17 @@ package com.yas.delivery; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -@SpringBootApplication +@SpringBootApplication(scanBasePackages = {"com.yas.delivery", "com.yas.commonlibrary"}) +@EnableAutoConfiguration(exclude = { + DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + HibernateJpaAutoConfiguration.class}) public class DeliveryApplication { public static void main(String[] args) { SpringApplication.run(DeliveryApplication.class, args); diff --git a/delivery/src/main/java/com/yas/delivery/config/SecurityConfig.java b/delivery/src/main/java/com/yas/delivery/config/SecurityConfig.java new file mode 100644 index 0000000000..3fd356a864 --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/config/SecurityConfig.java @@ -0,0 +1,48 @@ +package com.yas.delivery.config; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + return http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/actuator/prometheus", "/actuator/health/**", + "/swagger-ui", "/swagger-ui/**", "/error", "/v3/api-docs/**").permitAll() + .requestMatchers("/storefront/**").permitAll() + .requestMatchers("/backoffice/**").hasRole("ADMIN") + .anyRequest().authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) + .build(); + } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() { + Converter> jwtGrantedAuthoritiesConverter = jwt -> { + Map> realmAccess = jwt.getClaim("realm_access"); + Collection roles = realmAccess.get("roles"); + return roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .collect(Collectors.toList()); + }; + + var jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); + + return jwtAuthenticationConverter; + } +} diff --git a/delivery/src/main/java/com/yas/delivery/config/SwaggerConfig.java b/delivery/src/main/java/com/yas/delivery/config/SwaggerConfig.java new file mode 100644 index 0000000000..b73ed038e2 --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/config/SwaggerConfig.java @@ -0,0 +1,24 @@ +package com.yas.delivery.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.OAuthFlow; +import io.swagger.v3.oas.annotations.security.OAuthFlows; +import io.swagger.v3.oas.annotations.security.OAuthScope; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.servers.Server; + +@OpenAPIDefinition(info = @Info(title = "Delivery Service API", description = "Delivery API documentation", + version = "1.0"), + security = @SecurityRequirement(name = "oauth2_bearer"), + servers = {@Server(url = "${server.servlet.context-path}", description = "Default Server URL")}) +@SecurityScheme(name = "oauth2_bearer", type = SecuritySchemeType.OAUTH2, + flows = @OAuthFlows(authorizationCode = + @OAuthFlow(authorizationUrl = "${springdoc.oauthflow.authorization-url}", + tokenUrl = "${springdoc.oauthflow.token-url}", scopes = { + @OAuthScope(name = "openid", description = "openid") + }))) +public class SwaggerConfig { +} diff --git a/delivery/src/main/java/com/yas/delivery/controller/DeliveryController.java b/delivery/src/main/java/com/yas/delivery/controller/DeliveryController.java index 90454b12c2..abca43820d 100644 --- a/delivery/src/main/java/com/yas/delivery/controller/DeliveryController.java +++ b/delivery/src/main/java/com/yas/delivery/controller/DeliveryController.java @@ -1,7 +1,31 @@ package com.yas.delivery.controller; +import com.yas.delivery.service.DeliveryService; +import com.yas.delivery.viewmodel.CalculateDeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryProviderVm; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController +@RequiredArgsConstructor public class DeliveryController { + private final DeliveryService deliveryService; + + @GetMapping("/storefront/delivery/providers") + public ResponseEntity> getDeliveryProviders() { + return ResponseEntity.ok(deliveryService.getDeliveryProviders()); + } + + @PostMapping("/storefront/delivery/calculate") + public ResponseEntity calculateDeliveryFee( + @Valid @RequestBody CalculateDeliveryFeeVm calculateDeliveryFeeVm) { + return ResponseEntity.ok(deliveryService.calculateDeliveryFee(calculateDeliveryFeeVm)); + } } diff --git a/delivery/src/main/java/com/yas/delivery/model/DeliveryProvider.java b/delivery/src/main/java/com/yas/delivery/model/DeliveryProvider.java new file mode 100644 index 0000000000..25ae01e9aa --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/model/DeliveryProvider.java @@ -0,0 +1,17 @@ +package com.yas.delivery.model; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@lombok.Getter +@lombok.Setter +@Builder +public class DeliveryProvider { + private String id; + private String name; + private List serviceTypes; +} diff --git a/delivery/src/main/java/com/yas/delivery/model/DeliveryServiceType.java b/delivery/src/main/java/com/yas/delivery/model/DeliveryServiceType.java new file mode 100644 index 0000000000..f5beed6183 --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/model/DeliveryServiceType.java @@ -0,0 +1,18 @@ +package com.yas.delivery.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@lombok.Getter +@lombok.Setter +@Builder +public class DeliveryServiceType { + private String id; + private String name; + private double totalCost; + private double totalTax; + private String expectedDeliveryTime; +} diff --git a/delivery/src/main/java/com/yas/delivery/service/DeliveryService.java b/delivery/src/main/java/com/yas/delivery/service/DeliveryService.java index 6ac53153ca..90b07b5a52 100644 --- a/delivery/src/main/java/com/yas/delivery/service/DeliveryService.java +++ b/delivery/src/main/java/com/yas/delivery/service/DeliveryService.java @@ -1,7 +1,123 @@ package com.yas.delivery.service; +import static com.yas.delivery.utils.Constants.ErrorCode.INVALID_DELIVERY_PROVIDER; + +import com.yas.commonlibrary.exception.BadRequestException; +import com.yas.delivery.model.DeliveryProvider; +import com.yas.delivery.model.DeliveryServiceType; +import com.yas.delivery.viewmodel.CalculateDeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryOption; +import com.yas.delivery.viewmodel.DeliveryProviderVm; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; import org.springframework.stereotype.Service; @Service public class DeliveryService { + + private static final List deliveryProviders; + + static { + DeliveryProvider fedexProvider = buildFedexProvider(); + DeliveryProvider upsProvider = buildUPSProvider(); + deliveryProviders = Arrays.asList(fedexProvider, upsProvider); + } + + private static DeliveryProvider buildFedexProvider() { + ZonedDateTime now = ZonedDateTime.now(); + + DeliveryProvider fedexProvider = new DeliveryProvider(); + fedexProvider.setId("FEDEX"); + fedexProvider.setName("FedEx"); + List fedexServiceTypes = Arrays.asList( + DeliveryServiceType.builder() + .id("FEDEX_INTERNATIONAL_PRIORITY") + .name("FedEx International Priority") + .totalCost(20.0) + .totalTax(2.0) + .expectedDeliveryTime(now.plusDays(1).format(DateTimeFormatter.ISO_INSTANT)) + .build(), + DeliveryServiceType.builder() + .id("INTERNATIONAL_ECONOMY") + .name("FedEx International Economy") + .totalCost(30.0) + .totalTax(3.0) + .expectedDeliveryTime(now.plusDays(3).format(DateTimeFormatter.ISO_INSTANT)) + .build() + ); + fedexProvider.setServiceTypes(fedexServiceTypes); + return fedexProvider; + } + + private static DeliveryProvider buildUPSProvider() { + ZonedDateTime now = ZonedDateTime.now(); + + DeliveryProvider upsProvider = new DeliveryProvider(); + upsProvider.setId("UPS"); + upsProvider.setName("UPS"); + List upsServiceTypes = Arrays.asList( + DeliveryServiceType.builder() + .id("07") + .name("UPS Worldwide Express") + .totalCost(10.0) + .totalTax(1.0) + .expectedDeliveryTime(now.plusDays(5).format(DateTimeFormatter.ISO_INSTANT)) + .build(), + DeliveryServiceType.builder() + .id("11") + .name("UPS Standard") + .totalCost(15.0) + .expectedDeliveryTime(now.plusDays(7).format(DateTimeFormatter.ISO_INSTANT)) + .totalTax(1.5) + .build() + ); + upsProvider.setServiceTypes(upsServiceTypes); + return upsProvider; + } + + /** + * Retrieves a list of available delivery providers. + * + * @return a list of {@link DeliveryProviderVm} representing the available delivery providers. + */ + public List getDeliveryProviders() { + return deliveryProviders.stream().map(deliveryProvider -> + new DeliveryProviderVm(deliveryProvider.getId(), deliveryProvider.getName()) + ).toList(); + } + + /** + * Calculates the delivery fee based on the provided delivery information. + * + * @param calculateDeliveryFeeVm the delivery information including delivery provider ID, warehouse address, + * recipient address, and delivery items. + * @return a {@link DeliveryFeeVm} containing the calculated delivery fee options. + * @throws IllegalArgumentException if the delivery provider ID is invalid. + */ + public DeliveryFeeVm calculateDeliveryFee(CalculateDeliveryFeeVm calculateDeliveryFeeVm) { + DeliveryProvider deliveryProvider = deliveryProviders.stream() + .filter(provider -> provider.getId().equals(calculateDeliveryFeeVm.deliveryProviderId())) + .findFirst() + .orElseThrow( + () -> new BadRequestException(INVALID_DELIVERY_PROVIDER, calculateDeliveryFeeVm.deliveryProviderId())); + return new DeliveryFeeVm(getDeliveryOptionsByProvider(deliveryProvider)); + } + + public List getDeliveryOptionsByProvider(DeliveryProvider deliveryProvider) { + return deliveryProvider.getServiceTypes().stream() + .map(serviceType -> DeliveryOption + .builder() + .deliveryProviderId(deliveryProvider.getId()) + .deliveryProviderName(deliveryProvider.getName()) + .deliveryServiceTypeId(serviceType.getId()) + .deliveryServiceTypeName(serviceType.getName()) + .totalCost(serviceType.getTotalCost()) + .totalTax(serviceType.getTotalTax()) + .expectedDeliveryTime(serviceType.getExpectedDeliveryTime()) + .build()) + .toList(); + } } diff --git a/delivery/src/main/java/com/yas/delivery/utils/Constants.java b/delivery/src/main/java/com/yas/delivery/utils/Constants.java new file mode 100644 index 0000000000..10296d4ea9 --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/utils/Constants.java @@ -0,0 +1,13 @@ +package com.yas.delivery.utils; + +public class Constants { + + private Constants(){} + + public static final class ErrorCode { + + private ErrorCode(){} + + public static final String INVALID_DELIVERY_PROVIDER = "INVALID_DELIVERY_PROVIDER"; + } +} diff --git a/delivery/src/main/java/com/yas/delivery/viewmodel/CalculateDeliveryFeeVm.java b/delivery/src/main/java/com/yas/delivery/viewmodel/CalculateDeliveryFeeVm.java new file mode 100644 index 0000000000..210a643bf3 --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/viewmodel/CalculateDeliveryFeeVm.java @@ -0,0 +1,15 @@ +package com.yas.delivery.viewmodel; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.Builder; + +@Builder(toBuilder = true) +public record CalculateDeliveryFeeVm( + @NotNull(message = "Delivery provider is required") String deliveryProviderId, + @Valid @NotNull(message = "Warehouse address is required") DeliveryAddressVm warehouseAddress, + @Valid @NotNull(message = "Recipient address is required") DeliveryAddressVm recipientAddress, + @NotEmpty(message = "Delivery items are required") List<@Valid DeliveryItemVm> deliveryItems) { +} \ No newline at end of file diff --git a/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryAddressVm.java b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryAddressVm.java new file mode 100644 index 0000000000..a40e4a7fea --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryAddressVm.java @@ -0,0 +1,14 @@ +package com.yas.delivery.viewmodel; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +@Builder(toBuilder = true) +public record DeliveryAddressVm(@NotNull(message = "Delivery address ID is required") Long id, + @NotNull(message = "Address line 1 is required") String addressLine1, + String city, + @NotNull(message = "Zipcode is required") String zipCode, + Long districtId, + Long stateOrProvinceId, + @NotNull(message = "Country ID is required") Long countryId) { +} \ No newline at end of file diff --git a/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryFeeVm.java b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryFeeVm.java new file mode 100644 index 0000000000..29f8846e29 --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryFeeVm.java @@ -0,0 +1,6 @@ +package com.yas.delivery.viewmodel; + +import java.util.List; + +public record DeliveryFeeVm(List deliveryOptions) { +} diff --git a/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryItemVm.java b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryItemVm.java new file mode 100644 index 0000000000..900f2c0edf --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryItemVm.java @@ -0,0 +1,12 @@ +package com.yas.delivery.viewmodel; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +@Builder(toBuilder = true) +public record DeliveryItemVm(@NotNull(message = "Delivery item product ID is required") String productId, + @NotNull(message = "Delivery item quantity is required") Integer quantity, + @NotNull(message = "Delivery item weight is required") Double weight, + Double length, + Double width, + Double height) {} \ No newline at end of file diff --git a/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryOption.java b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryOption.java new file mode 100644 index 0000000000..a9fcd6f576 --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryOption.java @@ -0,0 +1,13 @@ +package com.yas.delivery.viewmodel; + +import lombok.Builder; + +@Builder +public record DeliveryOption(String deliveryProviderId, + String deliveryProviderName, + String deliveryServiceTypeId, + String deliveryServiceTypeName, + Double totalCost, + Double totalTax, + String expectedDeliveryTime) { +} diff --git a/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryProviderVm.java b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryProviderVm.java new file mode 100644 index 0000000000..f6e21ff987 --- /dev/null +++ b/delivery/src/main/java/com/yas/delivery/viewmodel/DeliveryProviderVm.java @@ -0,0 +1,4 @@ +package com.yas.delivery.viewmodel; + +public record DeliveryProviderVm(String id, + String name) {} diff --git a/delivery/src/main/resources/application.properties b/delivery/src/main/resources/application.properties index b657efe9cb..85540d3f35 100644 --- a/delivery/src/main/resources/application.properties +++ b/delivery/src/main/resources/application.properties @@ -1,7 +1,18 @@ +server.port=8095 server.servlet.context-path=/delivery + spring.application.name=delivery +spring.threads.virtual.enabled=true + +management.tracing.sampling.probability=1.0 +management.endpoints.web.exposure.include=prometheus +management.metrics.distribution.percentiles-histogram.http.server.requests=true +management.metrics.tags.application=${spring.application.name} + logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] +spring.security.oauth2.resourceserver.jwt.issuer-uri=http://identity/realms/Yas + # swagger-ui custom path springdoc.swagger-ui.path=/swagger-ui springdoc.packagesToScan=com.yas.delivery @@ -9,3 +20,5 @@ springdoc.swagger-ui.oauth.use-pkce-with-authorization-code-grant=true springdoc.swagger-ui.oauth.client-id=swagger-ui springdoc.oauthflow.authorization-url=http://identity/realms/Yas/protocol/openid-connect/auth springdoc.oauthflow.token-url=http://identity/realms/Yas/protocol/openid-connect/token + +cors.allowed-origins=* diff --git a/delivery/src/main/resources/messages/messages.properties b/delivery/src/main/resources/messages/messages.properties new file mode 100644 index 0000000000..a9c3189c2f --- /dev/null +++ b/delivery/src/main/resources/messages/messages.properties @@ -0,0 +1 @@ +INVALID_DELIVERY_PROVIDER=Invalid delivery provider: {} \ No newline at end of file diff --git a/delivery/src/test/java/com/yas/delivery/controller/DeliveryControllerTest.java b/delivery/src/test/java/com/yas/delivery/controller/DeliveryControllerTest.java new file mode 100644 index 0000000000..8b92c79a27 --- /dev/null +++ b/delivery/src/test/java/com/yas/delivery/controller/DeliveryControllerTest.java @@ -0,0 +1,203 @@ +package com.yas.delivery.controller; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yas.commonlibrary.exception.ApiExceptionHandler; +import com.yas.delivery.service.DeliveryService; +import com.yas.delivery.utils.InvalidCalculateDeliveryFeeVmTestCase; +import com.yas.delivery.utils.TestUtils; +import com.yas.delivery.viewmodel.CalculateDeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryOption; +import com.yas.delivery.viewmodel.DeliveryProviderVm; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +@ExtendWith(SpringExtension.class) +@WebMvcTest +@ContextConfiguration(classes = {DeliveryController.class, ApiExceptionHandler.class}) +@AutoConfigureMockMvc(addFilters = false) +class DeliveryControllerTest { + + private static final String UPS_PROVIDER_ID = "UPS"; + private static final String UPS_PROVIDER_NAME = "UPS"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private DeliveryService deliveryService; + + @Nested + class GetDeliveryProvidersTest { + + @Test + void testGetDeliveryProviders_whenRequestIsValid_shouldReturnDeliveryProviders() throws Exception { + DeliveryProviderVm upsProvider = new DeliveryProviderVm(UPS_PROVIDER_ID, UPS_PROVIDER_NAME); + + when(deliveryService.getDeliveryProviders()).thenReturn(List.of(upsProvider)); + + mockMvc.perform(get("/storefront/delivery/providers")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(upsProvider.id())) + .andExpect(jsonPath("$[0].name").value(upsProvider.name())); + + verify(deliveryService).getDeliveryProviders(); + } + } + + @Nested + class CalculateDeliveryFeeTest { + + private static final CalculateDeliveryFeeVm calculateDeliveryFeeVm = TestUtils.generateCalculateDeliveryFeeVm(); + + @Test + void testCalculateDeliveryFee_whenRequestIsValid_shouldReturnDeliveryFee() throws Exception { + DeliveryOption deliveryOption = DeliveryOption + .builder() + .deliveryProviderId(UPS_PROVIDER_ID) + .deliveryProviderName(UPS_PROVIDER_NAME) + .deliveryServiceTypeId("07") + .deliveryServiceTypeName("UPS Worldwide Express") + .totalCost(10.0) + .totalTax(1.0) + .build(); + DeliveryFeeVm expectedDeliveryFeeVm = new DeliveryFeeVm(List.of(deliveryOption)); + + when(deliveryService.calculateDeliveryFee(calculateDeliveryFeeVm)).thenReturn(expectedDeliveryFeeVm); + + performCalculateFeeAndExpectValid(deliveryOption); + + verify(deliveryService).calculateDeliveryFee(calculateDeliveryFeeVm); + } + + @ParameterizedTest(name = "should return bad request when {0}") + @MethodSource({"invalidDeliveryProviderCases", "invalidAddressesCases", "invalidDeliveryItemsCases"}) + void testCalculateDeliveryFee_whenInputIsInvalid_shouldReturnBadRequest( + InvalidCalculateDeliveryFeeVmTestCase testCase) throws Exception { + performCalculateFeeAndExpectBadRequest(testCase.getInput()); + } + + private void performCalculateFeeAndExpectValid(DeliveryOption deliveryOption) throws Exception { + mockMvc.perform(post("/storefront/delivery/calculate") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(calculateDeliveryFeeVm))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.deliveryOptions[0].deliveryProviderId") + .value(deliveryOption.deliveryProviderId())) + .andExpect(jsonPath("$.deliveryOptions[0].deliveryProviderName") + .value(deliveryOption.deliveryProviderName())) + .andExpect(jsonPath("$.deliveryOptions[0].deliveryServiceTypeId") + .value(deliveryOption.deliveryServiceTypeId())) + .andExpect(jsonPath("$.deliveryOptions[0].deliveryServiceTypeName") + .value(deliveryOption.deliveryServiceTypeName())) + .andExpect(jsonPath("$.deliveryOptions[0].totalCost") + .value(deliveryOption.totalCost())) + .andExpect(jsonPath("$.deliveryOptions[0].totalTax") + .value(deliveryOption.totalTax())); + } + + + @SuppressWarnings("unused") + private static Stream invalidDeliveryProviderCases() { + return Stream.of( + new InvalidCalculateDeliveryFeeVmTestCase( + "delivery provider ID is null", + calculateDeliveryFeeVm.toBuilder().deliveryProviderId(null).build() + ) + ); + } + + @SuppressWarnings("unused") + private static Stream invalidAddressesCases() { + return Stream.of( + new InvalidCalculateDeliveryFeeVmTestCase( + "warehouse address is null", + calculateDeliveryFeeVm.toBuilder().warehouseAddress(null).build() + ), + new InvalidCalculateDeliveryFeeVmTestCase( + "warehouse address ID is null", + calculateDeliveryFeeVm.toBuilder() + .warehouseAddress(calculateDeliveryFeeVm.warehouseAddress().toBuilder().id(null).build()) + .build() + ), + new InvalidCalculateDeliveryFeeVmTestCase( + "warehouse zip code is null", + calculateDeliveryFeeVm.toBuilder() + .warehouseAddress(calculateDeliveryFeeVm.warehouseAddress().toBuilder().zipCode(null).build()) + .build() + ), + new InvalidCalculateDeliveryFeeVmTestCase( + "warehouse country ID is null", + calculateDeliveryFeeVm.toBuilder() + .warehouseAddress(calculateDeliveryFeeVm.warehouseAddress().toBuilder().countryId(null).build()) + .build() + ), + new InvalidCalculateDeliveryFeeVmTestCase( + "recipient address is null", + calculateDeliveryFeeVm.toBuilder().recipientAddress(null).build() + ) + ); + } + + @SuppressWarnings("unused") + private static Stream invalidDeliveryItemsCases() { + return Stream.of( + new InvalidCalculateDeliveryFeeVmTestCase( + "delivery items is empty", + calculateDeliveryFeeVm.toBuilder().deliveryItems(List.of()).build() + ), + new InvalidCalculateDeliveryFeeVmTestCase( + "delivery item product ID is null", + calculateDeliveryFeeVm.toBuilder() + .deliveryItems( + List.of(calculateDeliveryFeeVm.deliveryItems().getFirst().toBuilder().productId(null).build())) + .build() + ), + new InvalidCalculateDeliveryFeeVmTestCase( + "delivery item quantity is null", + calculateDeliveryFeeVm.toBuilder() + .deliveryItems( + List.of(calculateDeliveryFeeVm.deliveryItems().getFirst().toBuilder().quantity(null).build())) + .build() + ), + new InvalidCalculateDeliveryFeeVmTestCase( + "delivery item weight is null", + calculateDeliveryFeeVm.toBuilder() + .deliveryItems( + List.of(calculateDeliveryFeeVm.deliveryItems().getFirst().toBuilder().weight(null).build())) + .build() + ) + ); + } + + void performCalculateFeeAndExpectBadRequest(CalculateDeliveryFeeVm calculateDeliveryFeeVm) throws Exception { + mockMvc.perform(post("/storefront/delivery/calculate") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(calculateDeliveryFeeVm))) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/delivery/src/test/java/com/yas/delivery/service/DeliveryServiceTest.java b/delivery/src/test/java/com/yas/delivery/service/DeliveryServiceTest.java new file mode 100644 index 0000000000..c4c3235466 --- /dev/null +++ b/delivery/src/test/java/com/yas/delivery/service/DeliveryServiceTest.java @@ -0,0 +1,73 @@ +package com.yas.delivery.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.yas.commonlibrary.exception.BadRequestException; +import com.yas.delivery.utils.TestUtils; +import com.yas.delivery.viewmodel.CalculateDeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryOption; +import com.yas.delivery.viewmodel.DeliveryProviderVm; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class DeliveryServiceTest { + private static final String FEDEX_PROVIDER_ID = "FEDEX"; + private static final String FEDEX_PROVIDER_NAME = "FedEx"; + + private final DeliveryService deliveryService = new DeliveryService(); + + @Nested + class GetDeliveryProvidersTest { + + @Test + void testGetDeliveryProviders_shouldReturnDeliveryProviders() { + List deliveryProviders = deliveryService.getDeliveryProviders(); + + assertNotNull(deliveryProviders); + assertEquals(2, deliveryProviders.size()); + assertTrue(deliveryProviders.stream().anyMatch(p -> p.id().equals(FEDEX_PROVIDER_ID) && p.name().equals( + FEDEX_PROVIDER_NAME))); + } + } + + @Nested + class CalculateDeliveryFeeTest { + + private CalculateDeliveryFeeVm calculateDeliveryFeeVm; + + @BeforeEach + void setUp() { + calculateDeliveryFeeVm = TestUtils.generateCalculateDeliveryFeeVm(); + } + + @Test + void testCalculateDeliveryFee_whenProviderIdIsInvalid_shouldThrowBadRequestException() { + String invalidProviderId = "INVALID"; + CalculateDeliveryFeeVm invalidCalculateDeliveryFeeVm = + calculateDeliveryFeeVm.toBuilder().deliveryProviderId(invalidProviderId).build(); + + assertThrows(BadRequestException.class, + () -> deliveryService.calculateDeliveryFee(invalidCalculateDeliveryFeeVm)); + } + + @Test + void testCalculateDeliveryFee_whenCalled_shouldReturnDeliveryFee() { + DeliveryFeeVm deliveryFee = deliveryService.calculateDeliveryFee(calculateDeliveryFeeVm); + + assertNotNull(deliveryFee); + DeliveryOption firstDeliveryOption = deliveryFee.deliveryOptions().getFirst(); + assertEquals(calculateDeliveryFeeVm.deliveryProviderId(), firstDeliveryOption.deliveryProviderId()); + assertEquals(FEDEX_PROVIDER_NAME, firstDeliveryOption.deliveryProviderName()); + assertEquals("FEDEX_INTERNATIONAL_PRIORITY", firstDeliveryOption.deliveryServiceTypeId()); + assertEquals("FedEx International Priority", firstDeliveryOption.deliveryServiceTypeName()); + assertEquals(20.0, firstDeliveryOption.totalCost()); + assertEquals(2.0, firstDeliveryOption.totalTax()); + } + } +} diff --git a/delivery/src/test/java/com/yas/delivery/utils/InvalidCalculateDeliveryFeeVmTestCase.java b/delivery/src/test/java/com/yas/delivery/utils/InvalidCalculateDeliveryFeeVmTestCase.java new file mode 100644 index 0000000000..0087da5aee --- /dev/null +++ b/delivery/src/test/java/com/yas/delivery/utils/InvalidCalculateDeliveryFeeVmTestCase.java @@ -0,0 +1,10 @@ +package com.yas.delivery.utils; + +import com.yas.delivery.viewmodel.CalculateDeliveryFeeVm; + +public class InvalidCalculateDeliveryFeeVmTestCase extends InvalidTestCase { + + public InvalidCalculateDeliveryFeeVmTestCase(String description, CalculateDeliveryFeeVm input) { + super(description, input); + } +} diff --git a/delivery/src/test/java/com/yas/delivery/utils/InvalidTestCase.java b/delivery/src/test/java/com/yas/delivery/utils/InvalidTestCase.java new file mode 100644 index 0000000000..5134a718e8 --- /dev/null +++ b/delivery/src/test/java/com/yas/delivery/utils/InvalidTestCase.java @@ -0,0 +1,16 @@ +package com.yas.delivery.utils; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class InvalidTestCase { + private String description; + private T input; + + @Override + public String toString() { + return description; + } +} diff --git a/delivery/src/test/java/com/yas/delivery/utils/TestUtils.java b/delivery/src/test/java/com/yas/delivery/utils/TestUtils.java new file mode 100644 index 0000000000..43f04e31a0 --- /dev/null +++ b/delivery/src/test/java/com/yas/delivery/utils/TestUtils.java @@ -0,0 +1,37 @@ +package com.yas.delivery.utils; + +import com.yas.delivery.viewmodel.CalculateDeliveryFeeVm; +import com.yas.delivery.viewmodel.DeliveryAddressVm; +import com.yas.delivery.viewmodel.DeliveryItemVm; +import java.util.List; + +public class TestUtils { + public static CalculateDeliveryFeeVm generateCalculateDeliveryFeeVm() { + DeliveryAddressVm warehouseAddress = DeliveryAddressVm.builder() + .id(1L) + .addressLine1("123 Warehouse Street") + .zipCode("12345") + .countryId(100L) + .build(); + + DeliveryAddressVm recipientAddress = DeliveryAddressVm.builder() + .id(2L) + .addressLine1("456 Recipient Avenue") + .zipCode("67890") + .countryId(200L) + .build(); + + DeliveryItemVm deliveryItem = DeliveryItemVm.builder() + .productId("P12345") + .quantity(2) + .weight(1.5) + .build(); + + return CalculateDeliveryFeeVm.builder() + .deliveryProviderId("FEDEX") + .warehouseAddress(warehouseAddress) + .recipientAddress(recipientAddress) + .deliveryItems(List.of(deliveryItem)) + .build(); + } +} diff --git a/delivery/src/test/resources/application.properties b/delivery/src/test/resources/application.properties new file mode 100644 index 0000000000..5f0a711fca --- /dev/null +++ b/delivery/src/test/resources/application.properties @@ -0,0 +1,13 @@ +server.port=8095 +server.servlet.context-path=/delivery + +spring.jpa.hibernate.ddl-auto=update +spring.liquibase.enabled=false + +# Setting Spring profile +spring.profiles.active=test + +spring.security.oauth2.resourceserver.jwt.issuer-uri=test +springdoc.oauthflow.authorization-url=test +springdoc.oauthflow.token-url=test +cors.allowed-origins=* \ No newline at end of file diff --git a/delivery/src/test/resources/logback-spring.xml b/delivery/src/test/resources/logback-spring.xml new file mode 100644 index 0000000000..0bf554c884 --- /dev/null +++ b/delivery/src/test/resources/logback-spring.xml @@ -0,0 +1,15 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/delivery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/delivery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/delivery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/nginx/templates/default.conf.template b/nginx/templates/default.conf.template index 7938c937f2..4ce43e5199 100644 --- a/nginx/templates/default.conf.template +++ b/nginx/templates/default.conf.template @@ -56,6 +56,9 @@ server { set $docker_recommendation_host "recommendation"; proxy_pass http://$docker_recommendation_host; } + location /delivery/ { + proxy_pass http://host.docker.internal:8095; # temporary value for current development, will be replaced once the delivery service is dockerized + } } server {