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 {