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) {
    }
}