HttpCatalogClient.java
package com.ecommerce.trade.infrastructure.client;
import com.ecommerce.platform.common.api.ApiResponse;
import com.ecommerce.trade.application.exception.TradeError;
import com.ecommerce.trade.application.exception.TradeException;
import com.ecommerce.trade.application.port.CatalogPort;
import com.ecommerce.trade.infrastructure.config.RemoteClientProperties;
import com.ecommerce.trade.infrastructure.resilience.RemoteDependencyFailure;
import com.ecommerce.trade.infrastructure.resilience.TradeSynchronousBoundaryResilience;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestClientResponseException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
@Component
public class HttpCatalogClient implements CatalogPort {
private static final ParameterizedTypeReference<ApiResponse<ProductResponse>> RESPONSE_TYPE =
new ParameterizedTypeReference<>() {
};
private final RestClient restClient;
private final TradeSynchronousBoundaryResilience resilience;
@Autowired
public HttpCatalogClient(
RestClient.Builder tradeRestClientBuilder,
RemoteClientProperties properties,
TradeSynchronousBoundaryResilience resilience) {
this(tradeRestClientBuilder.baseUrl(properties.catalogBaseUrl()).build(), resilience);
}
HttpCatalogClient(RestClient restClient, TradeSynchronousBoundaryResilience resilience) {
this.restClient = restClient;
this.resilience = resilience;
}
@Override
public ProductSnapshot getProduct(Long productId) {
return resilience.execute(
TradeSynchronousBoundaryResilience.Boundary.CATALOG_QUERY,
() -> requestProduct(productId));
}
private ProductSnapshot requestProduct(Long productId) {
try {
ApiResponse<ProductResponse> response = restClient.get()
.uri("/api/v1/catalog/products/{productId}", productId)
.retrieve()
.body(RESPONSE_TYPE);
if (response == null || response.data() == null) {
throw RemoteDependencyFailure.invalidResponse();
}
ProductResponse product = response.data();
if (!Objects.equals(productId, product.id())) {
throw RemoteDependencyFailure.invalidResponse();
}
return new ProductSnapshot(
product.id(),
product.title(),
product.status(),
product.skus().stream().map(sku -> new SkuSnapshot(
sku.id(), sku.skuCode(), sku.name(), sku.specJson(), sku.salePrice(), sku.status())).toList(),
product.media().stream().map(media -> new MediaSnapshot(
media.skuId(), media.objectKey(), media.sortOrder())).toList());
} catch (RestClientResponseException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new TradeException(TradeError.PRODUCT_UNAVAILABLE, exception);
}
throw RemoteDependencyFailure.forHttpStatus(exception.getStatusCode(), exception);
} catch (TradeException exception) {
throw exception;
} catch (RemoteDependencyFailure exception) {
throw exception;
} catch (ResourceAccessException exception) {
throw RemoteDependencyFailure.transientFailure(exception);
} catch (RestClientException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
} catch (RuntimeException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
}
}
private record ProductResponse(
Long id,
String title,
String status,
List<SkuResponse> skus,
List<MediaResponse> media
) {
}
private record SkuResponse(
Long id,
String skuCode,
String name,
String specJson,
BigDecimal salePrice,
BigDecimal marketPrice,
String status,
int version
) {
}
private record MediaResponse(Long skuId, String objectKey, int sortOrder) {
}
}