HttpInventoryClient.java
package com.ecommerce.trade.infrastructure.client;
import com.ecommerce.platform.common.api.ApiResponse;
import com.ecommerce.trade.application.port.InventoryPort;
import com.ecommerce.trade.infrastructure.config.InternalClientProperties;
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.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.time.Instant;
import java.util.List;
import java.util.Objects;
@Component
public class HttpInventoryClient implements InventoryPort {
private static final ParameterizedTypeReference<ApiResponse<WarehouseResponse>> WAREHOUSE_TYPE =
new ParameterizedTypeReference<>() {
};
private static final ParameterizedTypeReference<ApiResponse<ReservationResponse>> RESERVATION_TYPE =
new ParameterizedTypeReference<>() {
};
private final RestClient restClient;
private final InternalClientProperties properties;
private final TradeSynchronousBoundaryResilience resilience;
@Autowired
public HttpInventoryClient(
RestClient.Builder tradeRestClientBuilder,
InternalClientProperties properties,
RemoteClientProperties clientProperties,
TradeSynchronousBoundaryResilience resilience) {
this(
tradeRestClientBuilder.baseUrl(clientProperties.inventoryBaseUrl()).build(),
properties,
resilience);
}
HttpInventoryClient(
RestClient restClient,
InternalClientProperties properties,
TradeSynchronousBoundaryResilience resilience) {
this.restClient = restClient;
this.properties = properties;
this.resilience = resilience;
}
@Override
public WarehouseSnapshot getWarehouse(String code) {
return resilience.execute(
TradeSynchronousBoundaryResilience.Boundary.INVENTORY_QUERY,
() -> requestWarehouse(code));
}
private WarehouseSnapshot requestWarehouse(String code) {
try {
ApiResponse<WarehouseResponse> response = restClient.get()
.uri("/api/v1/inventory/internal/warehouses/{code}", code)
.header("X-Internal-Service", properties.caller())
.header("X-Internal-Token", properties.token())
.retrieve().body(WAREHOUSE_TYPE);
if (response == null || response.data() == null) {
throw RemoteDependencyFailure.invalidResponse();
}
WarehouseResponse warehouse = response.data();
if (!Objects.equals(code, warehouse.code())) {
throw RemoteDependencyFailure.invalidResponse();
}
return new WarehouseSnapshot(warehouse.id(), warehouse.code(), warehouse.status());
} catch (RemoteDependencyFailure exception) {
throw exception;
} catch (RestClientResponseException exception) {
throw RemoteDependencyFailure.forHttpStatus(exception.getStatusCode(), exception);
} catch (ResourceAccessException exception) {
throw RemoteDependencyFailure.transientFailure(exception);
} catch (RestClientException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
} catch (RuntimeException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
}
}
@Override
public ReservationSnapshot reserve(ReservationCommand command) {
return resilience.execute(
TradeSynchronousBoundaryResilience.Boundary.INVENTORY_COMMAND,
() -> requestReservation(command));
}
private ReservationSnapshot requestReservation(ReservationCommand command) {
ReservationRequest body = new ReservationRequest(
command.reservationNo(), command.orderNo(), command.warehouseId(), command.expiresAt(),
command.items().stream().map(item -> new ReservationLineRequest(item.skuId(), item.quantity())).toList());
try {
ApiResponse<ReservationResponse> response = restClient.post()
.uri("/api/v1/inventory/internal/reservations")
.header("X-Internal-Service", properties.caller())
.header("X-Internal-Token", properties.token())
.body(body).retrieve().body(RESERVATION_TYPE);
return reservation(response, command.reservationNo());
} catch (RemoteDependencyFailure exception) {
throw exception;
} catch (RestClientResponseException exception) {
throw RemoteDependencyFailure.forHttpStatus(exception.getStatusCode(), exception);
} catch (ResourceAccessException exception) {
throw RemoteDependencyFailure.transientFailure(exception);
} catch (RestClientException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
} catch (RuntimeException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
}
}
@Override
public ReservationSnapshot getReservation(String reservationNo) {
return resilience.execute(
TradeSynchronousBoundaryResilience.Boundary.INVENTORY_QUERY,
() -> requestReservation(reservationNo));
}
private ReservationSnapshot requestReservation(String reservationNo) {
try {
ApiResponse<ReservationResponse> response = restClient.get()
.uri("/api/v1/inventory/internal/reservations/{reservationNo}", reservationNo)
.header("X-Internal-Service", properties.caller())
.header("X-Internal-Token", properties.token())
.retrieve().body(RESERVATION_TYPE);
return reservation(response, reservationNo);
} catch (RemoteDependencyFailure exception) {
throw exception;
} catch (RestClientResponseException exception) {
throw RemoteDependencyFailure.forHttpStatus(exception.getStatusCode(), exception);
} catch (ResourceAccessException exception) {
throw RemoteDependencyFailure.transientFailure(exception);
} catch (RestClientException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
} catch (RuntimeException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
}
}
@Override
public ReservationSnapshot confirm(String reservationNo) {
return resilience.execute(
TradeSynchronousBoundaryResilience.Boundary.INVENTORY_COMMAND,
() -> requestConfirm(reservationNo));
}
private ReservationSnapshot requestConfirm(String reservationNo) {
try {
ApiResponse<ReservationResponse> response = restClient.post()
.uri("/api/v1/inventory/internal/reservations/{reservationNo}/confirm", reservationNo)
.header("X-Internal-Service", properties.caller())
.header("X-Internal-Token", properties.token())
.retrieve().body(RESERVATION_TYPE);
return reservation(response, reservationNo);
} catch (RemoteDependencyFailure exception) {
throw exception;
} catch (RestClientResponseException exception) {
throw RemoteDependencyFailure.forHttpStatus(exception.getStatusCode(), exception);
} catch (ResourceAccessException exception) {
throw RemoteDependencyFailure.transientFailure(exception);
} catch (RestClientException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
} catch (RuntimeException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
}
}
@Override
public ReservationSnapshot release(String reservationNo) {
return resilience.execute(
TradeSynchronousBoundaryResilience.Boundary.INVENTORY_COMMAND,
() -> requestRelease(reservationNo));
}
private ReservationSnapshot requestRelease(String reservationNo) {
try {
ApiResponse<ReservationResponse> response = restClient.post()
.uri("/api/v1/inventory/internal/reservations/{reservationNo}/release", reservationNo)
.header("X-Internal-Service", properties.caller())
.header("X-Internal-Token", properties.token())
.retrieve().body(RESERVATION_TYPE);
return reservation(response, reservationNo);
} catch (RemoteDependencyFailure exception) {
throw exception;
} catch (RestClientResponseException exception) {
throw RemoteDependencyFailure.forHttpStatus(exception.getStatusCode(), exception);
} catch (ResourceAccessException exception) {
throw RemoteDependencyFailure.transientFailure(exception);
} catch (RestClientException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
} catch (RuntimeException exception) {
throw RemoteDependencyFailure.invalidResponse(exception);
}
}
private ReservationSnapshot reservation(
ApiResponse<ReservationResponse> response,
String expectedReservationNo) {
if (response == null || response.data() == null) {
throw RemoteDependencyFailure.invalidResponse();
}
if (!Objects.equals(expectedReservationNo, response.data().reservationNo())) {
throw RemoteDependencyFailure.invalidResponse();
}
return new ReservationSnapshot(
response.data().reservationNo(),
response.data().orderNo(),
response.data().status(),
response.data().warehouseId(),
response.data().expiresAt(),
response.data().items().stream()
.map(item -> new ReservationLine(item.skuId(), item.quantity()))
.toList());
}
private record WarehouseResponse(Long id, String code, String status) {
}
private record ReservationRequest(
String reservationNo,
String orderNo,
Long warehouseId,
Instant expiresAt,
List<ReservationLineRequest> items
) {
}
private record ReservationLineRequest(Long skuId, long quantity) {
}
private record ReservationResponse(
String reservationNo,
String orderNo,
Long warehouseId,
String status,
Instant expiresAt,
List<ReservationLineResponse> items
) {
}
private record ReservationLineResponse(Long skuId, long quantity) {
}
}