HttpMarketingClient.java
package com.ecommerce.trade.infrastructure.client;
import com.ecommerce.platform.common.api.ApiResponse;
import com.ecommerce.trade.application.port.MarketingPort;
import com.ecommerce.trade.infrastructure.config.InternalClientProperties;
import com.ecommerce.trade.infrastructure.config.RemoteClientProperties;
import com.ecommerce.trade.infrastructure.resilience.MarketingPricingLockFailure;
import com.ecommerce.trade.infrastructure.resilience.TradeMarketingPricingLockResilience;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatusCode;
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.util.Set;
import java.util.Objects;
@Component
public class HttpMarketingClient implements MarketingPort {
private static final Set<String> BUSINESS_REJECTIONS = Set.of(
"BENEFIT_NOT_ELIGIBLE", "DUPLICATE_BENEFIT_TYPE", "IDEMPOTENCY_CONFLICT",
"INVALID_PRICING_REQUEST", "INVALID_STATE");
private static final ParameterizedTypeReference<ApiResponse<PricingLock>> RESPONSE_TYPE =
new ParameterizedTypeReference<>() {
};
private final RestClient restClient;
private final InternalClientProperties properties;
private final ObjectMapper objectMapper;
private final TradeMarketingPricingLockResilience resilience;
@Autowired
public HttpMarketingClient(
@Qualifier("tradeMarketingRestClientBuilder") RestClient.Builder tradeRestClientBuilder,
InternalClientProperties properties,
RemoteClientProperties clientProperties,
ObjectMapper objectMapper,
TradeMarketingPricingLockResilience resilience) {
this(tradeRestClientBuilder.baseUrl(clientProperties.marketingBaseUrl()).build(),
properties, objectMapper, resilience);
}
HttpMarketingClient(
RestClient restClient,
InternalClientProperties properties,
ObjectMapper objectMapper,
TradeMarketingPricingLockResilience resilience) {
this.restClient = restClient;
this.properties = properties;
this.objectMapper = objectMapper;
this.resilience = resilience;
}
@Override
public PricingLock lockPricing(PricingCommand command) {
return resilience.execute(() -> requestPricingLock(command));
}
private PricingLock requestPricingLock(PricingCommand command) {
try {
ApiResponse<PricingLock> response = restClient.post()
.uri("/api/v1/marketing/internal/pricing-locks")
.header("X-Internal-Service", properties.caller())
.header("X-Internal-Token", properties.token())
.body(command)
.retrieve().body(RESPONSE_TYPE);
if (response == null || response.data() == null) {
throw MarketingPricingLockFailure.invalidResponse();
}
PricingLock lock = response.data();
if (!Objects.equals(command.orderNo(), lock.orderNo())
|| !Objects.equals(command.userId(), lock.userId())) {
throw MarketingPricingLockFailure.invalidResponse();
}
return lock;
} catch (RestClientResponseException exception) {
if (isBusinessRejection(exception)) {
throw new PricingRejectedException("Marketing rejected the selected benefits");
}
throw MarketingPricingLockFailure.forHttpStatus(exception.getStatusCode(), exception);
} catch (MarketingPricingLockFailure exception) {
throw exception;
} catch (ResourceAccessException exception) {
throw MarketingPricingLockFailure.transientFailure(exception);
} catch (RestClientException exception) {
throw MarketingPricingLockFailure.invalidResponse(exception);
} catch (RuntimeException exception) {
throw MarketingPricingLockFailure.invalidResponse(exception);
}
}
private boolean isBusinessRejection(RestClientResponseException exception) {
HttpStatusCode status = exception.getStatusCode();
if (!status.is4xxClientError()) {
return false;
}
try {
JsonNode response = objectMapper.readTree(exception.getResponseBodyAsByteArray());
return BUSINESS_REJECTIONS.contains(response.path("code").asText());
} catch (Exception ignored) {
return false;
}
}
}