diff --git a/src/main/java/com/checkout/CheckoutApi.java b/src/main/java/com/checkout/CheckoutApi.java
index 9f0a6271..6818065f 100644
--- a/src/main/java/com/checkout/CheckoutApi.java
+++ b/src/main/java/com/checkout/CheckoutApi.java
@@ -20,6 +20,7 @@
import com.checkout.identities.addressdocumentverification.AddressDocumentVerificationClient;
import com.checkout.identities.amlscreening.AmlScreeningClient;
import com.checkout.instruments.InstrumentsClient;
+import com.checkout.inventory.InventoryClient;
import com.checkout.issuing.IssuingClient;
import com.checkout.metadata.MetadataClient;
import com.checkout.networktokens.NetworkTokensClient;
@@ -59,6 +60,8 @@ public interface CheckoutApi extends CheckoutApmApi {
ForexClient forexClient();
+ InventoryClient inventoryClient();
+
PaymentLinksClient paymentLinksClient();
PaymentMethodsClient paymentMethodsClient();
diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java
index 2062e21d..8865cfe2 100644
--- a/src/main/java/com/checkout/CheckoutApiImpl.java
+++ b/src/main/java/com/checkout/CheckoutApiImpl.java
@@ -40,6 +40,8 @@
import com.checkout.identities.amlscreening.AmlScreeningClientImpl;
import com.checkout.instruments.InstrumentsClient;
import com.checkout.instruments.InstrumentsClientImpl;
+import com.checkout.inventory.InventoryClient;
+import com.checkout.inventory.InventoryClientImpl;
import com.checkout.issuing.IssuingClient;
import com.checkout.issuing.IssuingClientImpl;
import com.checkout.metadata.MetadataClient;
@@ -87,6 +89,7 @@ public class CheckoutApiImpl extends AbstractCheckoutApmApi implements CheckoutA
private final AccountsClient accountsClient;
private final SessionsClient sessionsClient;
private final ForexClient forexClient;
+ private final InventoryClient inventoryClient;
private final PaymentLinksClient paymentLinksClient;
private final PaymentMethodsClient paymentMethodsClient;
private final HostedPaymentsClient hostedPaymentsClient;
@@ -125,6 +128,7 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) {
this.workflowsClient = new WorkflowsClientImpl(this.apiClient, configuration);
this.sessionsClient = new SessionsClientImpl(this.apiClient, configuration);
this.forexClient = new ForexClientImpl(this.apiClient, configuration);
+ this.inventoryClient = new InventoryClientImpl(this.apiClient, configuration);
this.paymentLinksClient = new PaymentLinksClientImpl(this.apiClient, configuration);
this.paymentMethodsClient = new PaymentMethodsClientImpl(this.apiClient, configuration);
this.hostedPaymentsClient = new HostedPaymentsClientImpl(this.apiClient, configuration);
@@ -207,6 +211,11 @@ public ForexClient forexClient() {
return forexClient;
}
+ @Override
+ public InventoryClient inventoryClient() {
+ return inventoryClient;
+ }
+
@Override
public PaymentLinksClient paymentLinksClient() {
return paymentLinksClient;
diff --git a/src/main/java/com/checkout/inventory/InventoryClient.java b/src/main/java/com/checkout/inventory/InventoryClient.java
new file mode 100644
index 00000000..7c397231
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryClient.java
@@ -0,0 +1,169 @@
+package com.checkout.inventory;
+
+import com.checkout.EmptyResponse;
+import com.checkout.inventory.request.InventoryAdjustmentRequest;
+import com.checkout.inventory.request.InventoryLevelsQueryFilter;
+import com.checkout.inventory.request.InventoryReservationRequest;
+import com.checkout.inventory.request.InventorySetLevelsRequest;
+import com.checkout.inventory.request.InventorySetProductRequest;
+import com.checkout.inventory.response.InventoryLevels;
+import com.checkout.inventory.response.InventoryProductKnowledge;
+import com.checkout.inventory.response.InventoryReservation;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * The Inventory client: stock levels, atomic multi-variant reservations, stock adjustments, and
+ * per-variant product knowledge for AI agents. Every operation on this client requires the
+ * {@code agentic:inventory} OAuth scope; it does not accept a secret or public API key.
+ *
+ *
Error responses ({@code 404}, {@code 409}, {@code 422}) are surfaced the same way as every
+ * other domain in this SDK: as a {@link com.checkout.CheckoutApiException} carrying the raw
+ * {@code Map} error body, not a typed class (this SDK has no domain that
+ * deserializes error bodies into a dedicated type). For the inventory endpoints, that map
+ * contains: {@code request_id} (String, for support and correlation), {@code error_type}
+ * (String, a high-level classification), {@code error_codes} (array of String, in
+ * {@code [subject]_[error]} form), and, present only on an {@code insufficient_stock} conflict,
+ * {@code variant_id} (String, the first failing variant) and {@code available} (Integer, its
+ * current sellable availability).
+ */
+public interface InventoryClient {
+
+ /**
+ * Applies a relative adjustment to a variant's on-hand stock.
+ *
+ * @param request the adjustment to apply
+ * @return the variant's resulting stock levels
+ */
+ CompletableFuture adjustInventory(InventoryAdjustmentRequest request);
+
+ /**
+ * Applies a relative adjustment to a variant's on-hand stock.
+ *
+ * @param request the adjustment to apply
+ * @param idempotencyKey an optional idempotency key sent as {@code Cko-Idempotency-Key}
+ * @return the variant's resulting stock levels
+ */
+ CompletableFuture adjustInventory(InventoryAdjustmentRequest request, String idempotencyKey);
+
+ /**
+ * Retrieves the current stock levels for a variant.
+ *
+ * @param variantId the identifier of the variant
+ * @return the variant's current stock levels
+ */
+ CompletableFuture getInventoryLevels(String variantId);
+
+ /**
+ * Retrieves the current stock levels for a variant.
+ *
+ * @param variantId the identifier of the variant
+ * @param filter optional query parameters, for example {@code expand=product}
+ * @return the variant's current stock levels
+ */
+ CompletableFuture getInventoryLevels(String variantId, InventoryLevelsQueryFilter filter);
+
+ /**
+ * Sets absolute stock levels for a variant. Creates the inventory item if it does not exist.
+ *
+ * @param variantId the identifier of the variant
+ * @param request the absolute stock levels to set
+ * @return the variant's resulting stock levels
+ */
+ CompletableFuture setInventoryLevels(String variantId, InventorySetLevelsRequest request);
+
+ /**
+ * Creates an atomic multi-variant stock hold.
+ *
+ * @param request the reservation to create
+ * @return the created reservation
+ */
+ CompletableFuture createInventoryReservation(InventoryReservationRequest request);
+
+ /**
+ * Creates an atomic multi-variant stock hold.
+ *
+ * @param request the reservation to create
+ * @param idempotencyKey an optional idempotency key sent as {@code Cko-Idempotency-Key}
+ * @return the created reservation
+ */
+ CompletableFuture createInventoryReservation(InventoryReservationRequest request, String idempotencyKey);
+
+ /**
+ * Retrieves a reservation.
+ *
+ * @param reservationId the identifier of the reservation
+ * @return the reservation
+ */
+ CompletableFuture getInventoryReservation(String reservationId);
+
+ /**
+ * Commits a held reservation, converting the hold into a permanent stock deduction.
+ *
+ * @param reservationId the identifier of the reservation
+ * @return the committed reservation
+ */
+ CompletableFuture commitInventoryReservation(String reservationId);
+
+ /**
+ * Releases a held reservation, returning the held quantities to available stock.
+ *
+ * @param reservationId the identifier of the reservation
+ * @return the released reservation
+ */
+ CompletableFuture releaseInventoryReservation(String reservationId);
+
+ /**
+ * Beta. Retrieves the product knowledge for a variant.
+ *
+ * @param variantId the identifier of the variant
+ * @return the variant's product knowledge
+ */
+ CompletableFuture getInventoryProduct(String variantId);
+
+ /**
+ * Beta. Sets (upserts) the product knowledge for a variant.
+ *
+ * @param variantId the identifier of the variant
+ * @param request the product knowledge to set
+ * @return the resulting product knowledge
+ */
+ CompletableFuture setInventoryProduct(String variantId, InventorySetProductRequest request);
+
+ /**
+ * Beta. Deletes the product knowledge for a variant.
+ *
+ * @param variantId the identifier of the variant
+ * @return an empty response
+ */
+ CompletableFuture deleteInventoryProduct(String variantId);
+
+ // Synchronous methods
+
+ InventoryLevels adjustInventorySync(InventoryAdjustmentRequest request);
+
+ InventoryLevels adjustInventorySync(InventoryAdjustmentRequest request, String idempotencyKey);
+
+ InventoryLevels getInventoryLevelsSync(String variantId);
+
+ InventoryLevels getInventoryLevelsSync(String variantId, InventoryLevelsQueryFilter filter);
+
+ InventoryLevels setInventoryLevelsSync(String variantId, InventorySetLevelsRequest request);
+
+ InventoryReservation createInventoryReservationSync(InventoryReservationRequest request);
+
+ InventoryReservation createInventoryReservationSync(InventoryReservationRequest request, String idempotencyKey);
+
+ InventoryReservation getInventoryReservationSync(String reservationId);
+
+ InventoryReservation commitInventoryReservationSync(String reservationId);
+
+ InventoryReservation releaseInventoryReservationSync(String reservationId);
+
+ InventoryProductKnowledge getInventoryProductSync(String variantId);
+
+ InventoryProductKnowledge setInventoryProductSync(String variantId, InventorySetProductRequest request);
+
+ EmptyResponse deleteInventoryProductSync(String variantId);
+
+}
diff --git a/src/main/java/com/checkout/inventory/InventoryClientImpl.java b/src/main/java/com/checkout/inventory/InventoryClientImpl.java
new file mode 100644
index 00000000..77712a41
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryClientImpl.java
@@ -0,0 +1,231 @@
+package com.checkout.inventory;
+
+import com.checkout.AbstractClient;
+import com.checkout.ApiClient;
+import com.checkout.CheckoutConfiguration;
+import com.checkout.EmptyResponse;
+import com.checkout.SdkAuthorizationType;
+import com.checkout.common.CheckoutUtils;
+import com.checkout.inventory.request.InventoryAdjustmentRequest;
+import com.checkout.inventory.request.InventoryLevelsQueryFilter;
+import com.checkout.inventory.request.InventoryReservationRequest;
+import com.checkout.inventory.request.InventorySetLevelsRequest;
+import com.checkout.inventory.request.InventorySetProductRequest;
+import com.checkout.inventory.response.InventoryLevels;
+import com.checkout.inventory.response.InventoryProductKnowledge;
+import com.checkout.inventory.response.InventoryReservation;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Every operation on this client requires the {@code agentic:inventory} OAuth scope; see
+ * {@link SdkAuthorizationType#OAUTH}. There is no secret-key or public-key fallback.
+ */
+public class InventoryClientImpl extends AbstractClient implements InventoryClient {
+
+ private static final String INVENTORY_PATH = "inventory";
+ private static final String ADJUSTMENTS_PATH = "adjustments";
+ private static final String RESERVATIONS_PATH = "reservations";
+ private static final String COMMIT_PATH = "commit";
+ private static final String RELEASE_PATH = "release";
+ private static final String PRODUCT_PATH = "product";
+
+ public InventoryClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) {
+ super(apiClient, configuration, SdkAuthorizationType.OAUTH);
+ }
+
+ @Override
+ public CompletableFuture adjustInventory(final InventoryAdjustmentRequest request) {
+ return adjustInventory(request, null);
+ }
+
+ @Override
+ public CompletableFuture adjustInventory(final InventoryAdjustmentRequest request, final String idempotencyKey) {
+ CheckoutUtils.validateParams("request", request);
+ return apiClient.postAsync(
+ buildPath(INVENTORY_PATH, ADJUSTMENTS_PATH),
+ sdkAuthorization(),
+ InventoryLevels.class,
+ request,
+ idempotencyKey);
+ }
+
+ @Override
+ public CompletableFuture getInventoryLevels(final String variantId) {
+ CheckoutUtils.validateParams("variantId", variantId);
+ return apiClient.getAsync(buildPath(INVENTORY_PATH, variantId), sdkAuthorization(), InventoryLevels.class);
+ }
+
+ @Override
+ public CompletableFuture getInventoryLevels(final String variantId, final InventoryLevelsQueryFilter filter) {
+ CheckoutUtils.validateParams("variantId", variantId, "filter", filter);
+ return apiClient.queryAsync(buildPath(INVENTORY_PATH, variantId), sdkAuthorization(), filter, InventoryLevels.class);
+ }
+
+ @Override
+ public CompletableFuture setInventoryLevels(final String variantId, final InventorySetLevelsRequest request) {
+ CheckoutUtils.validateParams("variantId", variantId, "request", request);
+ return apiClient.putAsync(buildPath(INVENTORY_PATH, variantId), sdkAuthorization(), InventoryLevels.class, request);
+ }
+
+ @Override
+ public CompletableFuture createInventoryReservation(final InventoryReservationRequest request) {
+ return createInventoryReservation(request, null);
+ }
+
+ @Override
+ public CompletableFuture createInventoryReservation(final InventoryReservationRequest request, final String idempotencyKey) {
+ CheckoutUtils.validateParams("request", request);
+ return apiClient.postAsync(
+ buildPath(INVENTORY_PATH, RESERVATIONS_PATH),
+ sdkAuthorization(),
+ InventoryReservation.class,
+ request,
+ idempotencyKey);
+ }
+
+ @Override
+ public CompletableFuture getInventoryReservation(final String reservationId) {
+ CheckoutUtils.validateParams("reservationId", reservationId);
+ return apiClient.getAsync(buildPath(INVENTORY_PATH, RESERVATIONS_PATH, reservationId), sdkAuthorization(), InventoryReservation.class);
+ }
+
+ @Override
+ public CompletableFuture commitInventoryReservation(final String reservationId) {
+ CheckoutUtils.validateParams("reservationId", reservationId);
+ return apiClient.postAsync(
+ buildPath(INVENTORY_PATH, RESERVATIONS_PATH, reservationId, COMMIT_PATH),
+ sdkAuthorization(),
+ InventoryReservation.class,
+ null,
+ null);
+ }
+
+ @Override
+ public CompletableFuture releaseInventoryReservation(final String reservationId) {
+ CheckoutUtils.validateParams("reservationId", reservationId);
+ return apiClient.postAsync(
+ buildPath(INVENTORY_PATH, RESERVATIONS_PATH, reservationId, RELEASE_PATH),
+ sdkAuthorization(),
+ InventoryReservation.class,
+ null,
+ null);
+ }
+
+ @Override
+ public CompletableFuture getInventoryProduct(final String variantId) {
+ CheckoutUtils.validateParams("variantId", variantId);
+ return apiClient.getAsync(buildPath(INVENTORY_PATH, variantId, PRODUCT_PATH), sdkAuthorization(), InventoryProductKnowledge.class);
+ }
+
+ @Override
+ public CompletableFuture setInventoryProduct(final String variantId, final InventorySetProductRequest request) {
+ CheckoutUtils.validateParams("variantId", variantId, "request", request);
+ return apiClient.putAsync(buildPath(INVENTORY_PATH, variantId, PRODUCT_PATH), sdkAuthorization(), InventoryProductKnowledge.class, request);
+ }
+
+ @Override
+ public CompletableFuture deleteInventoryProduct(final String variantId) {
+ CheckoutUtils.validateParams("variantId", variantId);
+ return apiClient.deleteAsync(buildPath(INVENTORY_PATH, variantId, PRODUCT_PATH), sdkAuthorization());
+ }
+
+ // Synchronous methods
+
+ @Override
+ public InventoryLevels adjustInventorySync(final InventoryAdjustmentRequest request) {
+ return adjustInventorySync(request, null);
+ }
+
+ @Override
+ public InventoryLevels adjustInventorySync(final InventoryAdjustmentRequest request, final String idempotencyKey) {
+ CheckoutUtils.validateParams("request", request);
+ return apiClient.post(
+ buildPath(INVENTORY_PATH, ADJUSTMENTS_PATH),
+ sdkAuthorization(),
+ InventoryLevels.class,
+ request,
+ idempotencyKey);
+ }
+
+ @Override
+ public InventoryLevels getInventoryLevelsSync(final String variantId) {
+ CheckoutUtils.validateParams("variantId", variantId);
+ return apiClient.get(buildPath(INVENTORY_PATH, variantId), sdkAuthorization(), InventoryLevels.class);
+ }
+
+ @Override
+ public InventoryLevels getInventoryLevelsSync(final String variantId, final InventoryLevelsQueryFilter filter) {
+ CheckoutUtils.validateParams("variantId", variantId, "filter", filter);
+ return apiClient.query(buildPath(INVENTORY_PATH, variantId), sdkAuthorization(), filter, InventoryLevels.class);
+ }
+
+ @Override
+ public InventoryLevels setInventoryLevelsSync(final String variantId, final InventorySetLevelsRequest request) {
+ CheckoutUtils.validateParams("variantId", variantId, "request", request);
+ return apiClient.put(buildPath(INVENTORY_PATH, variantId), sdkAuthorization(), InventoryLevels.class, request);
+ }
+
+ @Override
+ public InventoryReservation createInventoryReservationSync(final InventoryReservationRequest request) {
+ return createInventoryReservationSync(request, null);
+ }
+
+ @Override
+ public InventoryReservation createInventoryReservationSync(final InventoryReservationRequest request, final String idempotencyKey) {
+ CheckoutUtils.validateParams("request", request);
+ return apiClient.post(
+ buildPath(INVENTORY_PATH, RESERVATIONS_PATH),
+ sdkAuthorization(),
+ InventoryReservation.class,
+ request,
+ idempotencyKey);
+ }
+
+ @Override
+ public InventoryReservation getInventoryReservationSync(final String reservationId) {
+ CheckoutUtils.validateParams("reservationId", reservationId);
+ return apiClient.get(buildPath(INVENTORY_PATH, RESERVATIONS_PATH, reservationId), sdkAuthorization(), InventoryReservation.class);
+ }
+
+ @Override
+ public InventoryReservation commitInventoryReservationSync(final String reservationId) {
+ CheckoutUtils.validateParams("reservationId", reservationId);
+ return apiClient.post(
+ buildPath(INVENTORY_PATH, RESERVATIONS_PATH, reservationId, COMMIT_PATH),
+ sdkAuthorization(),
+ InventoryReservation.class,
+ null,
+ null);
+ }
+
+ @Override
+ public InventoryReservation releaseInventoryReservationSync(final String reservationId) {
+ CheckoutUtils.validateParams("reservationId", reservationId);
+ return apiClient.post(
+ buildPath(INVENTORY_PATH, RESERVATIONS_PATH, reservationId, RELEASE_PATH),
+ sdkAuthorization(),
+ InventoryReservation.class,
+ null,
+ null);
+ }
+
+ @Override
+ public InventoryProductKnowledge getInventoryProductSync(final String variantId) {
+ CheckoutUtils.validateParams("variantId", variantId);
+ return apiClient.get(buildPath(INVENTORY_PATH, variantId, PRODUCT_PATH), sdkAuthorization(), InventoryProductKnowledge.class);
+ }
+
+ @Override
+ public InventoryProductKnowledge setInventoryProductSync(final String variantId, final InventorySetProductRequest request) {
+ CheckoutUtils.validateParams("variantId", variantId, "request", request);
+ return apiClient.put(buildPath(INVENTORY_PATH, variantId, PRODUCT_PATH), sdkAuthorization(), InventoryProductKnowledge.class, request);
+ }
+
+ @Override
+ public EmptyResponse deleteInventoryProductSync(final String variantId) {
+ CheckoutUtils.validateParams("variantId", variantId);
+ return apiClient.delete(buildPath(INVENTORY_PATH, variantId, PRODUCT_PATH), sdkAuthorization());
+ }
+
+}
diff --git a/src/main/java/com/checkout/inventory/InventoryHalLink.java b/src/main/java/com/checkout/inventory/InventoryHalLink.java
new file mode 100644
index 00000000..0a8a5774
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryHalLink.java
@@ -0,0 +1,31 @@
+package com.checkout.inventory;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * A HAL link describing a related operation on an inventory resource.
+ */
+@Data
+public final class InventoryHalLink {
+
+ /**
+ * Absolute URI of the linked resource.
+ * [Optional]
+ */
+ private String href;
+
+ /**
+ * The HTTP methods supported on the linked resource.
+ * [Optional]
+ */
+ private List actions;
+
+ /**
+ * The media types supported on the linked resource.
+ * [Optional]
+ */
+ private List types;
+
+}
diff --git a/src/main/java/com/checkout/inventory/InventoryLevelsSource.java b/src/main/java/com/checkout/inventory/InventoryLevelsSource.java
new file mode 100644
index 00000000..2df43500
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryLevelsSource.java
@@ -0,0 +1,16 @@
+package com.checkout.inventory;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * How the levels are maintained. Always {@code managed} in the current version.
+ */
+public enum InventoryLevelsSource {
+
+ @SerializedName("managed")
+ MANAGED,
+
+ @SerializedName("sync")
+ SYNC,
+
+}
diff --git a/src/main/java/com/checkout/inventory/InventoryLevelsState.java b/src/main/java/com/checkout/inventory/InventoryLevelsState.java
new file mode 100644
index 00000000..26b6e1af
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryLevelsState.java
@@ -0,0 +1,19 @@
+package com.checkout.inventory;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * A derived availability state for the variant.
+ */
+public enum InventoryLevelsState {
+
+ @SerializedName("in_stock")
+ IN_STOCK,
+
+ @SerializedName("limited")
+ LIMITED,
+
+ @SerializedName("out_of_stock")
+ OUT_OF_STOCK,
+
+}
diff --git a/src/main/java/com/checkout/inventory/InventoryMoney.java b/src/main/java/com/checkout/inventory/InventoryMoney.java
new file mode 100644
index 00000000..6580d760
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryMoney.java
@@ -0,0 +1,30 @@
+package com.checkout.inventory;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+/**
+ * A monetary amount in the currency's minor units.
+ */
+@Data
+@Builder
+public final class InventoryMoney {
+
+ /**
+ * The amount in the minor currency unit.
+ * [Required]
+ */
+ @NonNull
+ private Long amount;
+
+ /**
+ * The three-letter ISO 4217 currency code.
+ * [Required]
+ * min 3 characters
+ * max 3 characters
+ */
+ @NonNull
+ private String currency;
+
+}
diff --git a/src/main/java/com/checkout/inventory/InventoryProductCondition.java b/src/main/java/com/checkout/inventory/InventoryProductCondition.java
new file mode 100644
index 00000000..254cb98f
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryProductCondition.java
@@ -0,0 +1,20 @@
+package com.checkout.inventory;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The product's condition. Always present on the response; defaults to {@code new} when not
+ * provided on the request.
+ */
+public enum InventoryProductCondition {
+
+ @SerializedName("new")
+ NEW,
+
+ @SerializedName("used")
+ USED,
+
+ @SerializedName("refurbished")
+ REFURBISHED,
+
+}
diff --git a/src/main/java/com/checkout/inventory/InventoryReservationItem.java b/src/main/java/com/checkout/inventory/InventoryReservationItem.java
new file mode 100644
index 00000000..52b04227
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryReservationItem.java
@@ -0,0 +1,30 @@
+package com.checkout.inventory;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+/**
+ * A single variant and quantity within a reservation.
+ */
+@Data
+@Builder
+public final class InventoryReservationItem {
+
+ /**
+ * The identifier of the variant to hold. The variant must already exist.
+ * [Required]
+ * max 128 characters
+ */
+ @NonNull
+ private String variantId;
+
+ /**
+ * The quantity to hold for this variant.
+ * [Required]
+ * min 1
+ */
+ @NonNull
+ private Integer quantity;
+
+}
diff --git a/src/main/java/com/checkout/inventory/InventoryReservationState.java b/src/main/java/com/checkout/inventory/InventoryReservationState.java
new file mode 100644
index 00000000..8a18b0a4
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/InventoryReservationState.java
@@ -0,0 +1,23 @@
+package com.checkout.inventory;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The current state of a reservation hold. A {@code held} reservation past its
+ * {@code expires_at} is reported as {@code expired}. Transitions out of {@code held} are terminal.
+ */
+public enum InventoryReservationState {
+
+ @SerializedName("held")
+ HELD,
+
+ @SerializedName("committed")
+ COMMITTED,
+
+ @SerializedName("released")
+ RELEASED,
+
+ @SerializedName("expired")
+ EXPIRED,
+
+}
diff --git a/src/main/java/com/checkout/inventory/request/InventoryAdjustmentRequest.java b/src/main/java/com/checkout/inventory/request/InventoryAdjustmentRequest.java
new file mode 100644
index 00000000..d391aa90
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/request/InventoryAdjustmentRequest.java
@@ -0,0 +1,40 @@
+package com.checkout.inventory.request;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+/**
+ * Beta. The request body for applying a relative adjustment to a variant's on-hand stock.
+ */
+@Data
+@Builder
+public final class InventoryAdjustmentRequest {
+
+ /**
+ * The identifier of the variant to adjust. The variant must already exist.
+ * [Required]
+ * max 128 characters
+ */
+ @NonNull
+ private String variantId;
+
+ /**
+ * The signed change to apply to {@code on_hand}. Must be non-zero. A negative delta that
+ * would drive {@code on_hand} below zero is rejected with {@code 409 conflict}.
+ * [Required]
+ */
+ @NonNull
+ private Integer delta;
+
+ /**
+ * A required free-text reason recorded in the ledger (for example, damage or found stock).
+ * Must not contain personal data.
+ * [Required]
+ * min 1 character
+ * max 256 characters
+ */
+ @NonNull
+ private String reason;
+
+}
diff --git a/src/main/java/com/checkout/inventory/request/InventoryLevelsQueryFilter.java b/src/main/java/com/checkout/inventory/request/InventoryLevelsQueryFilter.java
new file mode 100644
index 00000000..a1729c13
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/request/InventoryLevelsQueryFilter.java
@@ -0,0 +1,20 @@
+package com.checkout.inventory.request;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * Optional query parameters for retrieving a variant's stock levels.
+ */
+@Data
+@Builder
+public final class InventoryLevelsQueryFilter {
+
+ /**
+ * When set to {@code product}, embeds the variant's product knowledge in the response
+ * {@code product} field, if it exists.
+ * [Optional]
+ */
+ private String expand;
+
+}
diff --git a/src/main/java/com/checkout/inventory/request/InventoryReservationRequest.java b/src/main/java/com/checkout/inventory/request/InventoryReservationRequest.java
new file mode 100644
index 00000000..00d0490b
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/request/InventoryReservationRequest.java
@@ -0,0 +1,53 @@
+package com.checkout.inventory.request;
+
+import com.checkout.inventory.InventoryReservationItem;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+import java.util.List;
+
+/**
+ * The request body for creating an atomic multi-variant hold. All items are reserved together
+ * or none are. The hold is protocol-neutral: it is bound to an {@code owner_type} /
+ * {@code owner_reference} supplied by the calling protocol adapter (for example, a UCP session
+ * or an ACP checkout).
+ */
+@Data
+@Builder
+public final class InventoryReservationRequest {
+
+ /**
+ * The kind of caller that owns the hold.
+ * [Required]
+ * max 64 characters
+ */
+ @NonNull
+ private String ownerType;
+
+ /**
+ * An opaque reference to the owning session or checkout.
+ * [Required]
+ * max 256 characters
+ */
+ @NonNull
+ private String ownerReference;
+
+ /**
+ * The variants and quantities to hold. {@code variant_id}s must be unique within the request.
+ * [Required]
+ * min 1 item
+ * max 45 items
+ */
+ @NonNull
+ private List items;
+
+ /**
+ * How long the hold remains valid before it auto-expires. Defaults to {@code 900}.
+ * [Optional]
+ * min 60
+ * max 3600
+ */
+ private Integer ttlSeconds;
+
+}
diff --git a/src/main/java/com/checkout/inventory/request/InventorySetLevelsRequest.java b/src/main/java/com/checkout/inventory/request/InventorySetLevelsRequest.java
new file mode 100644
index 00000000..9c17d4ae
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/request/InventorySetLevelsRequest.java
@@ -0,0 +1,37 @@
+package com.checkout.inventory.request;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+/**
+ * The request body for setting absolute stock levels on a variant.
+ */
+@Data
+@Builder
+public final class InventorySetLevelsRequest {
+
+ /**
+ * The absolute physical stock to set for the variant.
+ * [Required]
+ * min 0
+ */
+ @NonNull
+ private Integer onHand;
+
+ /**
+ * The buffer quantity to withhold from sale. Defaults to {@code 0} when the item is
+ * created and is left unchanged on update if omitted.
+ * [Optional]
+ * min 0
+ */
+ private Integer safetyStock;
+
+ /**
+ * An optional free-text reason recorded in the ledger. Must not contain personal data.
+ * [Optional]
+ * max 256 characters
+ */
+ private String reason;
+
+}
diff --git a/src/main/java/com/checkout/inventory/request/InventorySetProductRequest.java b/src/main/java/com/checkout/inventory/request/InventorySetProductRequest.java
new file mode 100644
index 00000000..6605b4b2
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/request/InventorySetProductRequest.java
@@ -0,0 +1,271 @@
+package com.checkout.inventory.request;
+
+import com.checkout.inventory.InventoryMoney;
+import com.checkout.inventory.InventoryProductCondition;
+import com.google.gson.annotations.SerializedName;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NonNull;
+
+import java.time.Instant;
+import java.util.List;
+
+/**
+ * Beta. The request body for setting the product knowledge for a variant. The call is an upsert.
+ */
+@Data
+@Builder
+public final class InventorySetProductRequest {
+
+ /**
+ * The product's display title.
+ * [Required]
+ * max 512 characters
+ */
+ @NonNull
+ private String title;
+
+ /**
+ * The product's display description.
+ * [Required]
+ * max 4000 characters
+ */
+ @NonNull
+ private String description;
+
+ /**
+ * The canonical URL for the product page.
+ * [Required]
+ * max 2048 characters
+ */
+ @NonNull
+ private String productUrl;
+
+ /**
+ * The URL of the primary product image.
+ * [Required]
+ * max 2048 characters
+ */
+ @NonNull
+ private String imageUrl;
+
+ /**
+ * Additional product image URLs, beyond {@code image_url}.
+ * [Optional]
+ */
+ private List additionalImageUrls;
+
+ /**
+ * The URL of a product video.
+ * [Optional]
+ */
+ private String videoUrl;
+
+ /**
+ * The URL of a 3D model of the product.
+ * [Optional]
+ */
+ @SerializedName("model_3d_url")
+ private String model3dUrl;
+
+ /**
+ * The merchant's stock-keeping unit for the product.
+ * [Optional]
+ * max 128 characters
+ */
+ private String sku;
+
+ /**
+ * The product's Global Trade Item Number (UPC, EAN, ISBN, or JAN).
+ * [Optional]
+ */
+ private String gtin;
+
+ /**
+ * The product's Manufacturer Part Number.
+ * [Optional]
+ */
+ private String mpn;
+
+ /**
+ * The product's brand name.
+ * [Optional]
+ */
+ private String brand;
+
+ /**
+ * The merchant's category for the product.
+ * [Optional]
+ */
+ private String category;
+
+ /**
+ * The product's regular price. When provided together with {@code sale_price}, {@code sale_price}
+ * must use the same currency and be less than or equal to {@code price}.
+ * [Optional]
+ */
+ private InventoryMoney price;
+
+ /**
+ * The product's discounted price. Must share {@code price}'s currency and be less than or
+ * equal to {@code price}.
+ * [Optional]
+ */
+ private InventoryMoney salePrice;
+
+ /**
+ * The date and time from which {@code sale_price} applies. Paired with {@code sale_price}.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant salePriceStartsAt;
+
+ /**
+ * The date and time after which {@code sale_price} no longer applies. Paired with
+ * {@code sale_price}.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant salePriceEndsAt;
+
+ /**
+ * The identifier shared by all variants of the same product (for example, the same belt in
+ * different sizes). When set, {@code color} and {@code size} are both required.
+ * [Optional]
+ */
+ private String groupId;
+
+ /**
+ * A display title for the variant group.
+ * [Optional]
+ */
+ private String groupTitle;
+
+ /**
+ * The variant's color. Required when {@code group_id} is set.
+ * [Optional]
+ */
+ private String color;
+
+ /**
+ * The variant's size. Required when {@code group_id} is set.
+ * [Optional]
+ */
+ private String size;
+
+ /**
+ * The sizing system that {@code size} is expressed in.
+ * [Optional]
+ */
+ private String sizeSystem;
+
+ /**
+ * The target gender for the product.
+ * [Optional]
+ */
+ private String gender;
+
+ /**
+ * The product's condition, as an exact lowercase match of one of the enum values.
+ * Defaults to {@code new} when omitted.
+ * [Optional]
+ * Enum: "new" "used" "refurbished"
+ */
+ private InventoryProductCondition condition;
+
+ /**
+ * The product's primary material.
+ * [Optional]
+ */
+ private String material;
+
+ /**
+ * The target age group for the product.
+ * [Optional]
+ */
+ private String ageGroup;
+
+ /**
+ * The product's length. {@code length}, {@code width}, {@code height} and
+ * {@code dimension_unit} must be provided together, or not at all.
+ * [Optional]
+ */
+ private Double length;
+
+ /**
+ * The product's width. {@code length}, {@code width}, {@code height} and
+ * {@code dimension_unit} must be provided together, or not at all.
+ * [Optional]
+ */
+ private Double width;
+
+ /**
+ * The product's height. {@code length}, {@code width}, {@code height} and
+ * {@code dimension_unit} must be provided together, or not at all.
+ * [Optional]
+ */
+ private Double height;
+
+ /**
+ * The unit that {@code length}, {@code width} and {@code height} are expressed in.
+ * Required when any of {@code length}, {@code width} or {@code height} is set.
+ * [Optional]
+ */
+ private String dimensionUnit;
+
+ /**
+ * The product's weight. Must be provided together with {@code weight_unit}, or not at all.
+ * [Optional]
+ */
+ private Double weight;
+
+ /**
+ * The unit that {@code weight} is expressed in. Required when {@code weight} is set.
+ * [Optional]
+ */
+ private String weightUnit;
+
+ /**
+ * The date and time after which the product should no longer be offered.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant expirationDate;
+
+ /**
+ * The product's Harmonized System (HS) code, for customs purposes.
+ * [Optional]
+ */
+ private String harmonizedSystemCode;
+
+ /**
+ * The two-letter ISO 3166-1 alpha-2 country of origin.
+ * [Optional]
+ */
+ private String countryOfOrigin;
+
+ /**
+ * The name of the seller of record, when different from the merchant.
+ * [Optional]
+ */
+ private String sellerName;
+
+ /**
+ * The URL of the seller of record.
+ * [Optional]
+ */
+ private String sellerUrl;
+
+ /**
+ * The URL of the seller's privacy policy.
+ * [Optional]
+ */
+ private String sellerPrivacyPolicy;
+
+ /**
+ * The URL of the seller's terms of service.
+ * [Optional]
+ */
+ private String sellerTos;
+
+}
diff --git a/src/main/java/com/checkout/inventory/response/InventoryLevels.java b/src/main/java/com/checkout/inventory/response/InventoryLevels.java
new file mode 100644
index 00000000..2c728669
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/response/InventoryLevels.java
@@ -0,0 +1,94 @@
+package com.checkout.inventory.response;
+
+import com.checkout.HttpMetadata;
+import com.checkout.inventory.InventoryLevelsSource;
+import com.checkout.inventory.InventoryLevelsState;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.Instant;
+
+/**
+ * The current stock levels and sellable availability for a single variant. Returned by
+ * getInventoryLevels, setInventoryLevels and adjustInventory (both the {@code 200} idempotent
+ * replay and the {@code 201} created responses share this schema).
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class InventoryLevels extends HttpMetadata {
+
+ /**
+ * The merchant-provided identifier for the variant.
+ * [Optional]
+ */
+ private String variantId;
+
+ /**
+ * Physical stock known to Checkout.com.
+ * [Optional]
+ */
+ private Integer onHand;
+
+ /**
+ * The sum of all currently active holds against this variant.
+ * [Optional]
+ */
+ private Integer reserved;
+
+ /**
+ * A buffer quantity withheld from sale.
+ * [Optional]
+ */
+ private Integer safetyStock;
+
+ /**
+ * The sellable quantity, clamped to zero: {@code max(0, on_hand - reserved - safety_stock)}.
+ * This is the value an availability feed should publish.
+ * [Optional]
+ */
+ private Integer available;
+
+ /**
+ * A derived availability state for the variant.
+ * [Optional]
+ * Enum: "in_stock" "limited" "out_of_stock"
+ */
+ private InventoryLevelsState state;
+
+ /**
+ * How the levels are maintained. Always {@code managed} in the current version.
+ * [Optional]
+ * Enum: "managed" "sync"
+ */
+ private InventoryLevelsSource source;
+
+ /**
+ * The date and time the inventory item was created.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant createdOn;
+
+ /**
+ * The date and time the inventory item was last modified.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant modifiedOn;
+
+ /**
+ * The variant's product knowledge, embedded when {@code expand=product} is passed on the
+ * request and product knowledge exists for the variant. Omitted otherwise.
+ * [Optional]
+ */
+ private InventoryProductKnowledge product;
+
+ /**
+ * Links to related operations on the variant.
+ * [Optional]
+ */
+ @SerializedName("_links")
+ private InventoryLevelsLinks links;
+
+}
diff --git a/src/main/java/com/checkout/inventory/response/InventoryLevelsLinks.java b/src/main/java/com/checkout/inventory/response/InventoryLevelsLinks.java
new file mode 100644
index 00000000..b6e4eafd
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/response/InventoryLevelsLinks.java
@@ -0,0 +1,24 @@
+package com.checkout.inventory.response;
+
+import com.checkout.inventory.InventoryHalLink;
+import lombok.Data;
+
+/**
+ * Links to related operations on the variant.
+ */
+@Data
+public final class InventoryLevelsLinks {
+
+ /**
+ * The link to retrieve the current stock levels for the variant.
+ * [Optional]
+ */
+ private InventoryHalLink self;
+
+ /**
+ * The link to set absolute stock levels for the variant.
+ * [Optional]
+ */
+ private InventoryHalLink set;
+
+}
diff --git a/src/main/java/com/checkout/inventory/response/InventoryProductKnowledge.java b/src/main/java/com/checkout/inventory/response/InventoryProductKnowledge.java
new file mode 100644
index 00000000..93234972
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/response/InventoryProductKnowledge.java
@@ -0,0 +1,287 @@
+package com.checkout.inventory.response;
+
+import com.checkout.HttpMetadata;
+import com.checkout.inventory.InventoryMoney;
+import com.checkout.inventory.InventoryProductCondition;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.Instant;
+import java.util.List;
+
+/**
+ * Beta. The product knowledge for a single variant: the merchandising fields an AI agent needs
+ * to present, compare and recommend a product, independent of stock and pricing operations.
+ * Optional fields are omitted from the response entirely when not set.
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class InventoryProductKnowledge extends HttpMetadata {
+
+ /**
+ * The merchant-provided identifier for the variant.
+ * [Required]
+ */
+ private String variantId;
+
+ /**
+ * The product's display title.
+ * [Required]
+ */
+ private String title;
+
+ /**
+ * The product's display description.
+ * [Required]
+ */
+ private String description;
+
+ /**
+ * The canonical URL for the product page.
+ * [Required]
+ */
+ private String productUrl;
+
+ /**
+ * The URL of the primary product image.
+ * [Required]
+ */
+ private String imageUrl;
+
+ /**
+ * Additional product image URLs, beyond {@code image_url}.
+ * [Optional]
+ */
+ private List additionalImageUrls;
+
+ /**
+ * The URL of a product video.
+ * [Optional]
+ */
+ private String videoUrl;
+
+ /**
+ * The URL of a 3D model of the product.
+ * [Optional]
+ */
+ @SerializedName("model_3d_url")
+ private String model3dUrl;
+
+ /**
+ * The merchant's stock-keeping unit for the product.
+ * [Optional]
+ */
+ private String sku;
+
+ /**
+ * The product's Global Trade Item Number (UPC, EAN, ISBN, or JAN).
+ * [Optional]
+ */
+ private String gtin;
+
+ /**
+ * The product's Manufacturer Part Number.
+ * [Optional]
+ */
+ private String mpn;
+
+ /**
+ * The product's brand name.
+ * [Optional]
+ */
+ private String brand;
+
+ /**
+ * The merchant's category for the product.
+ * [Optional]
+ */
+ private String category;
+
+ /**
+ * The product's regular price.
+ * [Optional]
+ */
+ private InventoryMoney price;
+
+ /**
+ * The product's discounted price.
+ * [Optional]
+ */
+ private InventoryMoney salePrice;
+
+ /**
+ * The date and time from which {@code sale_price} applies. Paired with {@code sale_price}.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant salePriceStartsAt;
+
+ /**
+ * The date and time after which {@code sale_price} no longer applies. Paired with
+ * {@code sale_price}.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant salePriceEndsAt;
+
+ /**
+ * The identifier shared by all variants of the same product (for example, the same belt in
+ * different sizes). When set, {@code color} and {@code size} are both required.
+ * [Optional]
+ */
+ private String groupId;
+
+ /**
+ * A display title for the variant group.
+ * [Optional]
+ */
+ private String groupTitle;
+
+ /**
+ * The variant's color. Required when {@code group_id} is set.
+ * [Optional]
+ */
+ private String color;
+
+ /**
+ * The variant's size. Required when {@code group_id} is set.
+ * [Optional]
+ */
+ private String size;
+
+ /**
+ * The sizing system that {@code size} is expressed in.
+ * [Optional]
+ */
+ private String sizeSystem;
+
+ /**
+ * The target gender for the product.
+ * [Optional]
+ */
+ private String gender;
+
+ /**
+ * The product's condition. Always present; defaults to {@code new} when not provided.
+ * [Required]
+ * Enum: "new" "used" "refurbished"
+ */
+ private InventoryProductCondition condition;
+
+ /**
+ * The product's primary material.
+ * [Optional]
+ */
+ private String material;
+
+ /**
+ * The target age group for the product.
+ * [Optional]
+ */
+ private String ageGroup;
+
+ /**
+ * The product's length. Provided together with {@code width}, {@code height} and
+ * {@code dimension_unit}, or not at all.
+ * [Optional]
+ */
+ private Double length;
+
+ /**
+ * The product's width. Provided together with {@code length}, {@code height} and
+ * {@code dimension_unit}, or not at all.
+ * [Optional]
+ */
+ private Double width;
+
+ /**
+ * The product's height. Provided together with {@code length}, {@code width} and
+ * {@code dimension_unit}, or not at all.
+ * [Optional]
+ */
+ private Double height;
+
+ /**
+ * The unit that {@code length}, {@code width} and {@code height} are expressed in.
+ * [Optional]
+ */
+ private String dimensionUnit;
+
+ /**
+ * The product's weight. Provided together with {@code weight_unit}, or not at all.
+ * [Optional]
+ */
+ private Double weight;
+
+ /**
+ * The unit that {@code weight} is expressed in.
+ * [Optional]
+ */
+ private String weightUnit;
+
+ /**
+ * The date and time after which the product should no longer be offered.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant expirationDate;
+
+ /**
+ * The product's Harmonized System (HS) code, for customs purposes.
+ * [Optional]
+ */
+ private String harmonizedSystemCode;
+
+ /**
+ * The two-letter ISO 3166-1 alpha-2 country of origin.
+ * [Optional]
+ */
+ private String countryOfOrigin;
+
+ /**
+ * The name of the seller of record, when different from the merchant.
+ * [Optional]
+ */
+ private String sellerName;
+
+ /**
+ * The URL of the seller of record.
+ * [Optional]
+ */
+ private String sellerUrl;
+
+ /**
+ * The URL of the seller's privacy policy.
+ * [Optional]
+ */
+ private String sellerPrivacyPolicy;
+
+ /**
+ * The URL of the seller's terms of service.
+ * [Optional]
+ */
+ private String sellerTos;
+
+ /**
+ * The date and time the product knowledge was created.
+ * [Required]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant createdOn;
+
+ /**
+ * The date and time the product knowledge was last modified.
+ * [Required]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant modifiedOn;
+
+ /**
+ * Links to related operations on the variant's product knowledge.
+ * [Required]
+ */
+ @SerializedName("_links")
+ private InventoryProductLinks links;
+
+}
diff --git a/src/main/java/com/checkout/inventory/response/InventoryProductLinks.java b/src/main/java/com/checkout/inventory/response/InventoryProductLinks.java
new file mode 100644
index 00000000..60e49b8e
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/response/InventoryProductLinks.java
@@ -0,0 +1,30 @@
+package com.checkout.inventory.response;
+
+import com.checkout.inventory.InventoryHalLink;
+import lombok.Data;
+
+/**
+ * Links to related operations on the variant's product knowledge.
+ */
+@Data
+public final class InventoryProductLinks {
+
+ /**
+ * The link to retrieve the product knowledge for the variant.
+ * [Optional]
+ */
+ private InventoryHalLink self;
+
+ /**
+ * The link to set the product knowledge for the variant.
+ * [Optional]
+ */
+ private InventoryHalLink set;
+
+ /**
+ * The link to delete the product knowledge for the variant.
+ * [Optional]
+ */
+ private InventoryHalLink delete;
+
+}
diff --git a/src/main/java/com/checkout/inventory/response/InventoryReservation.java b/src/main/java/com/checkout/inventory/response/InventoryReservation.java
new file mode 100644
index 00000000..e678099f
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/response/InventoryReservation.java
@@ -0,0 +1,77 @@
+package com.checkout.inventory.response;
+
+import com.checkout.HttpMetadata;
+import com.checkout.inventory.InventoryReservationItem;
+import com.checkout.inventory.InventoryReservationState;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.Instant;
+import java.util.List;
+
+/**
+ * A stock hold and its current lifecycle state. Returned by createInventoryReservation (both
+ * the {@code 200} idempotent replay and the {@code 201} created responses share this schema),
+ * getInventoryReservation, commitInventoryReservation and releaseInventoryReservation.
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class InventoryReservation extends HttpMetadata {
+
+ /**
+ * The reservation identifier, in the {@code rsv_{base32-encoded GUID}} format.
+ * [Optional]
+ */
+ private String id;
+
+ /**
+ * The current state of the hold. A {@code held} reservation past its {@code expires_at} is
+ * reported as {@code expired}. Transitions out of {@code held} are terminal.
+ * [Optional]
+ * Enum: "held" "committed" "released" "expired"
+ */
+ private InventoryReservationState state;
+
+ /**
+ * Echo of the owning caller kind.
+ * [Optional]
+ */
+ private String ownerType;
+
+ /**
+ * Echo of the owning session or checkout reference.
+ * [Optional]
+ */
+ private String ownerReference;
+
+ /**
+ * The variants and quantities held by this reservation.
+ * [Optional]
+ */
+ private List items;
+
+ /**
+ * The server-authoritative time at which the hold expires.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant expiresAt;
+
+ /**
+ * The date and time the reservation was created.
+ * [Optional]
+ * Format: date-time (RFC 3339)
+ */
+ private Instant createdOn;
+
+ /**
+ * Links to the operations valid for the reservation's current state. A {@code held}
+ * reservation exposes {@code self}, {@code commit} and {@code release}; a terminal
+ * reservation exposes {@code self} only.
+ * [Optional]
+ */
+ @SerializedName("_links")
+ private InventoryReservationLinks links;
+
+}
diff --git a/src/main/java/com/checkout/inventory/response/InventoryReservationLinks.java b/src/main/java/com/checkout/inventory/response/InventoryReservationLinks.java
new file mode 100644
index 00000000..b90ec69f
--- /dev/null
+++ b/src/main/java/com/checkout/inventory/response/InventoryReservationLinks.java
@@ -0,0 +1,32 @@
+package com.checkout.inventory.response;
+
+import com.checkout.inventory.InventoryHalLink;
+import lombok.Data;
+
+/**
+ * Links to the operations valid for the reservation's current state. A {@code held}
+ * reservation exposes {@code self}, {@code commit} and {@code release}; a terminal
+ * reservation exposes {@code self} only.
+ */
+@Data
+public final class InventoryReservationLinks {
+
+ /**
+ * The link to retrieve the reservation.
+ * [Optional]
+ */
+ private InventoryHalLink self;
+
+ /**
+ * The link to commit the reservation. Present only while the reservation is {@code held}.
+ * [Optional]
+ */
+ private InventoryHalLink commit;
+
+ /**
+ * The link to release the reservation. Present only while the reservation is {@code held}.
+ * [Optional]
+ */
+ private InventoryHalLink release;
+
+}
diff --git a/src/test/java/com/checkout/inventory/InventoryClientImplTest.java b/src/test/java/com/checkout/inventory/InventoryClientImplTest.java
new file mode 100644
index 00000000..5c026694
--- /dev/null
+++ b/src/test/java/com/checkout/inventory/InventoryClientImplTest.java
@@ -0,0 +1,243 @@
+package com.checkout.inventory;
+
+import com.checkout.ApiClient;
+import com.checkout.CheckoutConfiguration;
+import com.checkout.EmptyResponse;
+import com.checkout.SdkAuthorization;
+import com.checkout.SdkAuthorizationType;
+import com.checkout.SdkCredentials;
+import com.checkout.inventory.request.InventoryAdjustmentRequest;
+import com.checkout.inventory.request.InventoryReservationRequest;
+import com.checkout.inventory.request.InventorySetLevelsRequest;
+import com.checkout.inventory.request.InventorySetProductRequest;
+import com.checkout.inventory.response.InventoryLevels;
+import com.checkout.inventory.response.InventoryProductKnowledge;
+import com.checkout.inventory.response.InventoryReservation;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verifies InventoryClientImpl uses SdkAuthorizationType.OAUTH exclusively (the
+ * {@code agentic:inventory} scope) with no ApiSecretKey/ApiPublicKey fallback, and that each of
+ * the 10 inventory operations builds the expected path and forwards to ApiClient correctly.
+ */
+@ExtendWith(MockitoExtension.class)
+class InventoryClientImplTest {
+
+ private InventoryClient client;
+
+ @Mock
+ private ApiClient apiClient;
+
+ @Mock
+ private CheckoutConfiguration configuration;
+
+ @Mock
+ private SdkCredentials sdkCredentials;
+
+ @Mock
+ private SdkAuthorization authorization;
+
+ @BeforeEach
+ void setUp() {
+ client = new InventoryClientImpl(apiClient, configuration);
+ }
+
+ @Test
+ void shouldAdjustInventory() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventoryAdjustmentRequest request = InventoryAdjustmentRequest.builder()
+ .variantId("var_123")
+ .delta(-3)
+ .reason("damaged in warehouse")
+ .build();
+ final InventoryLevels response = mock(InventoryLevels.class);
+
+ when(apiClient.postAsync(eq("inventory/adjustments"), eq(authorization), eq(InventoryLevels.class),
+ eq(request), isNull()))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.adjustInventory(request);
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldAdjustInventoryWithIdempotencyKey() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventoryAdjustmentRequest request = InventoryAdjustmentRequest.builder()
+ .variantId("var_123")
+ .delta(-3)
+ .reason("damaged in warehouse")
+ .build();
+ final InventoryLevels response = mock(InventoryLevels.class);
+
+ when(apiClient.postAsync(eq("inventory/adjustments"), eq(authorization), eq(InventoryLevels.class),
+ eq(request), eq("idem-key")))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.adjustInventory(request, "idem-key");
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldGetInventoryLevels() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventoryLevels response = mock(InventoryLevels.class);
+
+ when(apiClient.getAsync(eq("inventory/var_123"), eq(authorization), eq(InventoryLevels.class)))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.getInventoryLevels("var_123");
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldSetInventoryLevels() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventorySetLevelsRequest request = InventorySetLevelsRequest.builder().onHand(25).build();
+ final InventoryLevels response = mock(InventoryLevels.class);
+
+ when(apiClient.putAsync(eq("inventory/var_123"), eq(authorization), eq(InventoryLevels.class), eq(request)))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.setInventoryLevels("var_123", request);
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldCreateInventoryReservation() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventoryReservationRequest request = InventoryReservationRequest.builder()
+ .ownerType("ucp_session")
+ .ownerReference("cs_8f42")
+ .items(java.util.List.of(InventoryReservationItem.builder().variantId("var_123").quantity(2).build()))
+ .build();
+ final InventoryReservation response = mock(InventoryReservation.class);
+
+ when(apiClient.postAsync(eq("inventory/reservations"), eq(authorization), eq(InventoryReservation.class),
+ eq(request), isNull()))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.createInventoryReservation(request);
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldGetInventoryReservation() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventoryReservation response = mock(InventoryReservation.class);
+
+ when(apiClient.getAsync(eq("inventory/reservations/rsv_123"), eq(authorization), eq(InventoryReservation.class)))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.getInventoryReservation("rsv_123");
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldCommitInventoryReservation() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventoryReservation response = mock(InventoryReservation.class);
+
+ when(apiClient.postAsync(eq("inventory/reservations/rsv_123/commit"), eq(authorization),
+ eq(InventoryReservation.class), isNull(), isNull()))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.commitInventoryReservation("rsv_123");
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldReleaseInventoryReservation() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventoryReservation response = mock(InventoryReservation.class);
+
+ when(apiClient.postAsync(eq("inventory/reservations/rsv_123/release"), eq(authorization),
+ eq(InventoryReservation.class), isNull(), isNull()))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.releaseInventoryReservation("rsv_123");
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldGetInventoryProduct() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventoryProductKnowledge response = mock(InventoryProductKnowledge.class);
+
+ when(apiClient.getAsync(eq("inventory/var_123/product"), eq(authorization), eq(InventoryProductKnowledge.class)))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.getInventoryProduct("var_123");
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldSetInventoryProduct() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final InventorySetProductRequest request = InventorySetProductRequest.builder()
+ .title("Belt").description("desc").productUrl("https://example.com").imageUrl("https://example.com/i.jpg")
+ .build();
+ final InventoryProductKnowledge response = mock(InventoryProductKnowledge.class);
+
+ when(apiClient.putAsync(eq("inventory/var_123/product"), eq(authorization), eq(InventoryProductKnowledge.class), eq(request)))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.setInventoryProduct("var_123", request);
+
+ assertEquals(response, future.get());
+ }
+
+ @Test
+ void shouldDeleteInventoryProduct() throws ExecutionException, InterruptedException {
+ setupMockCredentials();
+
+ final EmptyResponse response = mock(EmptyResponse.class);
+
+ when(apiClient.deleteAsync(eq("inventory/var_123/product"), eq(authorization)))
+ .thenReturn(CompletableFuture.completedFuture(response));
+
+ final CompletableFuture future = client.deleteInventoryProduct("var_123");
+
+ assertEquals(response, future.get());
+ }
+
+ // Common methods
+
+ private void setupMockCredentials() {
+ when(sdkCredentials.getAuthorization(SdkAuthorizationType.OAUTH)).thenReturn(authorization);
+ when(configuration.getSdkCredentials()).thenReturn(sdkCredentials);
+ }
+
+}
diff --git a/src/test/java/com/checkout/inventory/InventoryCommonSerializationTest.java b/src/test/java/com/checkout/inventory/InventoryCommonSerializationTest.java
new file mode 100644
index 00000000..2010be15
--- /dev/null
+++ b/src/test/java/com/checkout/inventory/InventoryCommonSerializationTest.java
@@ -0,0 +1,145 @@
+package com.checkout.inventory;
+
+import com.checkout.GsonSerializer;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Schema validation tests for the shared Inventory types: InventoryHalLink, InventoryMoney and
+ * InventoryReservationItem. Fields verified against shared/swagger.json.
+ */
+class InventoryCommonSerializationTest {
+
+ private final GsonSerializer serializer = new GsonSerializer();
+
+ // ------------------------------------------------------------------------
+ // InventoryHalLink
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldRoundTripInventoryHalLink() {
+ final InventoryHalLink original = new InventoryHalLink();
+ original.setHref("https://api.checkout.com/inventory/var_123");
+ original.setActions(List.of("GET"));
+ original.setTypes(List.of("application/json"));
+
+ final String json = serializer.toJson(original);
+ final InventoryHalLink deserialized = serializer.fromJson(json, InventoryHalLink.class);
+
+ assertEquals(original, deserialized);
+ assertEquals("https://api.checkout.com/inventory/var_123", deserialized.getHref());
+ assertEquals(List.of("GET"), deserialized.getActions());
+ assertEquals(List.of("application/json"), deserialized.getTypes());
+ }
+
+ @Test
+ void shouldDeserializeInventoryHalLinkSwaggerExample() {
+ final String swaggerJson = "{"
+ + "\"href\": \"https://api.checkout.com/inventory/var_123\","
+ + "\"actions\": [\"GET\"],"
+ + "\"types\": [\"application/json\"]"
+ + "}";
+
+ final InventoryHalLink link = serializer.fromJson(swaggerJson, InventoryHalLink.class);
+
+ assertNotNull(link);
+ assertEquals("https://api.checkout.com/inventory/var_123", link.getHref());
+ }
+
+ // ------------------------------------------------------------------------
+ // InventoryMoney
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldSerializeInventoryMoneyWithRequiredFields() {
+ final InventoryMoney money = InventoryMoney.builder()
+ .amount(1999L)
+ .currency("USD")
+ .build();
+
+ final String json = serializer.toJson(money);
+
+ assertNotNull(json);
+ assertTrue(json.contains("\"amount\":1999"));
+ assertTrue(json.contains("\"currency\":\"USD\""));
+ }
+
+ @Test
+ void shouldRoundTripInventoryMoney() {
+ final InventoryMoney original = InventoryMoney.builder()
+ .amount(1999L)
+ .currency("USD")
+ .build();
+
+ final String json = serializer.toJson(original);
+ final InventoryMoney deserialized = serializer.fromJson(json, InventoryMoney.class);
+
+ assertEquals(original, deserialized);
+ }
+
+ // ------------------------------------------------------------------------
+ // InventoryReservationItem
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldSerializeInventoryReservationItemWithRequiredFields() {
+ final InventoryReservationItem item = InventoryReservationItem.builder()
+ .variantId("var_123")
+ .quantity(2)
+ .build();
+
+ final String json = serializer.toJson(item);
+
+ assertNotNull(json);
+ assertTrue(json.contains("\"variant_id\":\"var_123\""));
+ assertTrue(json.contains("\"quantity\":2"));
+ }
+
+ @Test
+ void shouldRoundTripInventoryReservationItem() {
+ final InventoryReservationItem original = InventoryReservationItem.builder()
+ .variantId("var_123")
+ .quantity(2)
+ .build();
+
+ final String json = serializer.toJson(original);
+ final InventoryReservationItem deserialized = serializer.fromJson(json, InventoryReservationItem.class);
+
+ assertEquals(original, deserialized);
+ }
+
+ // ------------------------------------------------------------------------
+ // Enums: InventoryLevelsState, InventoryLevelsSource, InventoryReservationState,
+ // InventoryProductCondition
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldSerializeInventoryLevelsStateAsSnakeCase() {
+ assertEquals("\"limited\"", serializer.toJson(InventoryLevelsState.LIMITED));
+ assertEquals("\"out_of_stock\"", serializer.toJson(InventoryLevelsState.OUT_OF_STOCK));
+ }
+
+ @Test
+ void shouldSerializeInventoryLevelsSourceAsSnakeCase() {
+ assertEquals("\"managed\"", serializer.toJson(InventoryLevelsSource.MANAGED));
+ assertEquals("\"sync\"", serializer.toJson(InventoryLevelsSource.SYNC));
+ }
+
+ @Test
+ void shouldSerializeInventoryReservationStateAsSnakeCase() {
+ assertEquals("\"held\"", serializer.toJson(InventoryReservationState.HELD));
+ assertEquals("\"expired\"", serializer.toJson(InventoryReservationState.EXPIRED));
+ }
+
+ @Test
+ void shouldSerializeInventoryProductConditionAsLowerCase() {
+ assertEquals("\"new\"", serializer.toJson(InventoryProductCondition.NEW));
+ assertEquals("\"refurbished\"", serializer.toJson(InventoryProductCondition.REFURBISHED));
+ }
+
+}
diff --git a/src/test/java/com/checkout/inventory/request/InventoryRequestSerializationTest.java b/src/test/java/com/checkout/inventory/request/InventoryRequestSerializationTest.java
new file mode 100644
index 00000000..0c2ea7b9
--- /dev/null
+++ b/src/test/java/com/checkout/inventory/request/InventoryRequestSerializationTest.java
@@ -0,0 +1,267 @@
+package com.checkout.inventory.request;
+
+import com.checkout.GsonSerializer;
+import com.checkout.inventory.InventoryMoney;
+import com.checkout.inventory.InventoryProductCondition;
+import com.checkout.inventory.InventoryReservationItem;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Schema validation tests for the Inventory request classes: InventoryAdjustmentRequest,
+ * InventoryReservationRequest, InventorySetLevelsRequest, InventorySetProductRequest and
+ * InventoryLevelsQueryFilter. Fields verified against shared/swagger.json.
+ */
+class InventoryRequestSerializationTest {
+
+ private final GsonSerializer serializer = new GsonSerializer();
+
+ // ------------------------------------------------------------------------
+ // InventoryAdjustmentRequest
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldSerializeInventoryAdjustmentRequest() {
+ final InventoryAdjustmentRequest request = InventoryAdjustmentRequest.builder()
+ .variantId("var_123")
+ .delta(-3)
+ .reason("damaged in warehouse")
+ .build();
+
+ final String json = serializer.toJson(request);
+
+ assertNotNull(json);
+ assertTrue(json.contains("\"variant_id\":\"var_123\""));
+ assertTrue(json.contains("\"delta\":-3"));
+ assertTrue(json.contains("\"reason\":\"damaged in warehouse\""));
+ }
+
+ @Test
+ void shouldRoundTripInventoryAdjustmentRequest() {
+ final InventoryAdjustmentRequest original = InventoryAdjustmentRequest.builder()
+ .variantId("var_123")
+ .delta(5)
+ .reason("found stock")
+ .build();
+
+ final String json = serializer.toJson(original);
+ final InventoryAdjustmentRequest deserialized = serializer.fromJson(json, InventoryAdjustmentRequest.class);
+
+ assertEquals(original, deserialized);
+ }
+
+ // ------------------------------------------------------------------------
+ // InventoryReservationRequest
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldSerializeInventoryReservationRequestWithRequiredFields() {
+ final InventoryReservationRequest request = InventoryReservationRequest.builder()
+ .ownerType("ucp_session")
+ .ownerReference("cs_8f42")
+ .items(List.of(InventoryReservationItem.builder().variantId("var_123").quantity(2).build()))
+ .build();
+
+ final String json = serializer.toJson(request);
+
+ assertNotNull(json);
+ assertTrue(json.contains("\"owner_type\":\"ucp_session\""));
+ assertTrue(json.contains("\"owner_reference\":\"cs_8f42\""));
+ assertTrue(json.contains("\"items\""));
+ assertFalse(json.contains("ttl_seconds"));
+ }
+
+ @Test
+ void shouldSerializeInventoryReservationRequestWithAllFields() {
+ final InventoryReservationRequest request = InventoryReservationRequest.builder()
+ .ownerType("ucp_session")
+ .ownerReference("cs_8f42")
+ .items(List.of(InventoryReservationItem.builder().variantId("var_123").quantity(2).build()))
+ .ttlSeconds(900)
+ .build();
+
+ final String json = serializer.toJson(request);
+
+ assertTrue(json.contains("\"ttl_seconds\":900"));
+ }
+
+ @Test
+ void shouldRoundTripInventoryReservationRequest() {
+ final InventoryReservationRequest original = InventoryReservationRequest.builder()
+ .ownerType("ucp_session")
+ .ownerReference("cs_8f42")
+ .items(List.of(InventoryReservationItem.builder().variantId("var_123").quantity(2).build()))
+ .ttlSeconds(900)
+ .build();
+
+ final String json = serializer.toJson(original);
+ final InventoryReservationRequest deserialized = serializer.fromJson(json, InventoryReservationRequest.class);
+
+ assertEquals(original, deserialized);
+ }
+
+ // ------------------------------------------------------------------------
+ // InventorySetLevelsRequest
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldSerializeInventorySetLevelsRequestWithRequiredFields() {
+ final InventorySetLevelsRequest request = InventorySetLevelsRequest.builder()
+ .onHand(25)
+ .build();
+
+ final String json = serializer.toJson(request);
+
+ assertTrue(json.contains("\"on_hand\":25"));
+ assertFalse(json.contains("safety_stock"));
+ assertFalse(json.contains("reason"));
+ }
+
+ @Test
+ void shouldSerializeInventorySetLevelsRequestWithAllFields() {
+ final InventorySetLevelsRequest request = InventorySetLevelsRequest.builder()
+ .onHand(25)
+ .safetyStock(2)
+ .reason("stock take 2026-07")
+ .build();
+
+ final String json = serializer.toJson(request);
+
+ assertTrue(json.contains("\"on_hand\":25"));
+ assertTrue(json.contains("\"safety_stock\":2"));
+ assertTrue(json.contains("\"reason\":\"stock take 2026-07\""));
+ }
+
+ @Test
+ void shouldRoundTripInventorySetLevelsRequest() {
+ final InventorySetLevelsRequest original = InventorySetLevelsRequest.builder()
+ .onHand(25)
+ .safetyStock(2)
+ .reason("stock take 2026-07")
+ .build();
+
+ final String json = serializer.toJson(original);
+ final InventorySetLevelsRequest deserialized = serializer.fromJson(json, InventorySetLevelsRequest.class);
+
+ assertEquals(original, deserialized);
+ }
+
+ // ------------------------------------------------------------------------
+ // InventorySetProductRequest - covers all fields, including nested InventoryMoney and the
+ // model_3d_url snake_case override.
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldSerializeInventorySetProductRequestWithRequiredFields() {
+ final InventorySetProductRequest request = InventorySetProductRequest.builder()
+ .title("Classic leather belt, brown")
+ .description("A full-grain leather belt with a brushed nickel buckle.")
+ .productUrl("https://merchant.example.com/products/classic-leather-belt-brown")
+ .imageUrl("https://merchant.example.com/images/belt-brown-main.jpg")
+ .build();
+
+ final String json = serializer.toJson(request);
+
+ assertTrue(json.contains("\"title\":\"Classic leather belt, brown\""));
+ assertTrue(json.contains("\"description\""));
+ assertTrue(json.contains("\"product_url\""));
+ assertTrue(json.contains("\"image_url\""));
+ assertFalse(json.contains("\"condition\""));
+ }
+
+ @Test
+ void shouldSerializeInventorySetProductRequestWithAllFields() {
+ final InventorySetProductRequest request = InventorySetProductRequest.builder()
+ .title("Classic leather belt, brown")
+ .description("A full-grain leather belt with a brushed nickel buckle.")
+ .productUrl("https://merchant.example.com/products/classic-leather-belt-brown")
+ .imageUrl("https://merchant.example.com/images/belt-brown-main.jpg")
+ .additionalImageUrls(List.of("https://merchant.example.com/images/belt-brown-alt1.jpg"))
+ .videoUrl("https://merchant.example.com/videos/belt-brown.mp4")
+ .model3dUrl("https://merchant.example.com/models/belt-brown.glb")
+ .sku("BELT-BRN-001")
+ .gtin("00012345678905")
+ .mpn("MPN-4471")
+ .brand("Acme Leathercraft")
+ .category("Apparel & Accessories > Belts")
+ .price(InventoryMoney.builder().amount(1999L).currency("USD").build())
+ .salePrice(InventoryMoney.builder().amount(1499L).currency("USD").build())
+ .salePriceStartsAt(Instant.parse("2026-08-01T00:00:00Z"))
+ .salePriceEndsAt(Instant.parse("2026-08-31T23:59:59Z"))
+ .groupId("grp_belt_classic")
+ .groupTitle("Classic leather belt")
+ .color("Brown")
+ .size("M")
+ .sizeSystem("US")
+ .gender("unisex")
+ .condition(InventoryProductCondition.NEW)
+ .material("Full-grain leather")
+ .ageGroup("adult")
+ .length(110d)
+ .width(3.5)
+ .height(0.5)
+ .dimensionUnit("cm")
+ .weight(0.2)
+ .weightUnit("kg")
+ .expirationDate(Instant.parse("2027-01-01T00:00:00Z"))
+ .harmonizedSystemCode("4203.30")
+ .countryOfOrigin("IT")
+ .sellerName("Acme Leathercraft Ltd")
+ .sellerUrl("https://acme-leathercraft.example.com")
+ .sellerPrivacyPolicy("https://acme-leathercraft.example.com/privacy")
+ .sellerTos("https://acme-leathercraft.example.com/terms")
+ .build();
+
+ final String json = serializer.toJson(request);
+
+ assertTrue(json.contains("\"model_3d_url\":\"https://merchant.example.com/models/belt-brown.glb\""));
+ assertTrue(json.contains("\"condition\":\"new\""));
+ assertTrue(json.contains("\"price\""));
+ assertTrue(json.contains("\"sale_price\""));
+ assertTrue(json.contains("\"sale_price_starts_at\""));
+ assertTrue(json.contains("\"group_id\":\"grp_belt_classic\""));
+ }
+
+ @Test
+ void shouldRoundTripInventorySetProductRequest() {
+ final InventorySetProductRequest original = InventorySetProductRequest.builder()
+ .title("Classic leather belt, brown")
+ .description("A full-grain leather belt with a brushed nickel buckle.")
+ .productUrl("https://merchant.example.com/products/classic-leather-belt-brown")
+ .imageUrl("https://merchant.example.com/images/belt-brown-main.jpg")
+ .model3dUrl("https://merchant.example.com/models/belt-brown.glb")
+ .price(InventoryMoney.builder().amount(1999L).currency("USD").build())
+ .condition(InventoryProductCondition.NEW)
+ .build();
+
+ final String json = serializer.toJson(original);
+ final InventorySetProductRequest deserialized = serializer.fromJson(json, InventorySetProductRequest.class);
+
+ assertEquals(original, deserialized);
+ assertEquals("https://merchant.example.com/models/belt-brown.glb", deserialized.getModel3dUrl());
+ assertEquals(InventoryProductCondition.NEW, deserialized.getCondition());
+ }
+
+ // ------------------------------------------------------------------------
+ // InventoryLevelsQueryFilter
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldSerializeInventoryLevelsQueryFilterWithExpand() {
+ final InventoryLevelsQueryFilter filter = InventoryLevelsQueryFilter.builder()
+ .expand("product")
+ .build();
+
+ final String json = serializer.toJson(filter);
+
+ assertTrue(json.contains("\"expand\":\"product\""));
+ }
+
+}
diff --git a/src/test/java/com/checkout/inventory/response/InventoryResponseSerializationTest.java b/src/test/java/com/checkout/inventory/response/InventoryResponseSerializationTest.java
new file mode 100644
index 00000000..93fadf87
--- /dev/null
+++ b/src/test/java/com/checkout/inventory/response/InventoryResponseSerializationTest.java
@@ -0,0 +1,255 @@
+package com.checkout.inventory.response;
+
+import com.checkout.GsonSerializer;
+import com.checkout.inventory.InventoryHalLink;
+import com.checkout.inventory.InventoryLevelsSource;
+import com.checkout.inventory.InventoryLevelsState;
+import com.checkout.inventory.InventoryMoney;
+import com.checkout.inventory.InventoryProductCondition;
+import com.checkout.inventory.InventoryReservationItem;
+import com.checkout.inventory.InventoryReservationState;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Schema validation tests for the Inventory response classes: InventoryLevels,
+ * InventoryReservation and InventoryProductKnowledge. Fields verified against
+ * shared/swagger.json.
+ */
+class InventoryResponseSerializationTest {
+
+ private final GsonSerializer serializer = new GsonSerializer();
+
+ // ------------------------------------------------------------------------
+ // InventoryLevels - covers state/source enums, embedded product and _links.
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldDeserializeInventoryLevelsSwaggerExample() {
+ final String swaggerJson = "{"
+ + "\"variant_id\":\"var_123\","
+ + "\"on_hand\":10,"
+ + "\"reserved\":2,"
+ + "\"safety_stock\":1,"
+ + "\"available\":7,"
+ + "\"state\":\"in_stock\","
+ + "\"source\":\"managed\","
+ + "\"created_on\":\"2026-07-01T09:15:00Z\","
+ + "\"modified_on\":\"2026-07-13T14:02:11Z\","
+ + "\"_links\":{\"self\":{\"href\":\"https://api.checkout.com/inventory/var_123\",\"actions\":[\"GET\"]},"
+ + "\"set\":{\"href\":\"https://api.checkout.com/inventory/var_123\",\"actions\":[\"PUT\"]}}"
+ + "}";
+
+ final InventoryLevels levels = serializer.fromJson(swaggerJson, InventoryLevels.class);
+
+ assertNotNull(levels);
+ assertEquals("var_123", levels.getVariantId());
+ assertEquals(10, levels.getOnHand());
+ assertEquals(7, levels.getAvailable());
+ assertEquals(InventoryLevelsState.IN_STOCK, levels.getState());
+ assertEquals(InventoryLevelsSource.MANAGED, levels.getSource());
+ assertEquals(Instant.parse("2026-07-01T09:15:00Z"), levels.getCreatedOn());
+ assertNotNull(levels.getLinks());
+ assertEquals("https://api.checkout.com/inventory/var_123", levels.getLinks().getSelf().getHref());
+ assertNull(levels.getProduct());
+ }
+
+ @Test
+ void shouldDeserializeInventoryLevelsWithEmbeddedProduct() {
+ final String swaggerJson = "{"
+ + "\"variant_id\":\"var_123\","
+ + "\"state\":\"limited\","
+ + "\"product\":{"
+ + "\"variant_id\":\"var_123\",\"title\":\"Belt\",\"description\":\"desc\","
+ + "\"product_url\":\"https://example.com\",\"image_url\":\"https://example.com/i.jpg\","
+ + "\"condition\":\"new\",\"created_on\":\"2026-08-01T09:15:00Z\",\"modified_on\":\"2026-08-13T14:02:11Z\","
+ + "\"_links\":{\"self\":{\"href\":\"https://example.com/self\"}}"
+ + "}"
+ + "}";
+
+ final InventoryLevels levels = serializer.fromJson(swaggerJson, InventoryLevels.class);
+
+ assertNotNull(levels.getProduct());
+ assertEquals("Belt", levels.getProduct().getTitle());
+ assertEquals(InventoryProductCondition.NEW, levels.getProduct().getCondition());
+ }
+
+ @Test
+ void shouldRoundTripInventoryLevels() {
+ final InventoryLevels original = new InventoryLevels();
+ original.setVariantId("var_123");
+ original.setOnHand(10);
+ original.setReserved(2);
+ original.setSafetyStock(1);
+ original.setAvailable(7);
+ original.setState(InventoryLevelsState.OUT_OF_STOCK);
+ original.setSource(InventoryLevelsSource.SYNC);
+ original.setCreatedOn(Instant.parse("2026-07-01T09:15:00Z"));
+ original.setModifiedOn(Instant.parse("2026-07-13T14:02:11Z"));
+ final InventoryLevelsLinks links = new InventoryLevelsLinks();
+ final InventoryHalLink self = new InventoryHalLink();
+ self.setHref("https://api.checkout.com/inventory/var_123");
+ links.setSelf(self);
+ original.setLinks(links);
+
+ final String json = serializer.toJson(original);
+ final InventoryLevels deserialized = serializer.fromJson(json, InventoryLevels.class);
+
+ assertEquals(original.getVariantId(), deserialized.getVariantId());
+ assertEquals(original.getState(), deserialized.getState());
+ assertEquals(original.getSource(), deserialized.getSource());
+ assertEquals(original.getLinks().getSelf().getHref(), deserialized.getLinks().getSelf().getHref());
+ }
+
+ // ------------------------------------------------------------------------
+ // InventoryReservation - covers state enum, items and the three-way _links.
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldDeserializeInventoryReservationSwaggerExample() {
+ final String swaggerJson = "{"
+ + "\"id\":\"rsv_tkoi5db4hryu5cei5vwoabr7we\","
+ + "\"state\":\"held\","
+ + "\"owner_type\":\"ucp_session\","
+ + "\"owner_reference\":\"cs_8f42\","
+ + "\"items\":[{\"variant_id\":\"var_123\",\"quantity\":2}],"
+ + "\"expires_at\":\"2026-07-14T08:47:00Z\","
+ + "\"created_on\":\"2026-07-14T08:32:00Z\","
+ + "\"_links\":{\"self\":{\"href\":\"https://example.com/self\"},"
+ + "\"commit\":{\"href\":\"https://example.com/commit\"},"
+ + "\"release\":{\"href\":\"https://example.com/release\"}}"
+ + "}";
+
+ final InventoryReservation reservation = serializer.fromJson(swaggerJson, InventoryReservation.class);
+
+ assertNotNull(reservation);
+ assertEquals("rsv_tkoi5db4hryu5cei5vwoabr7we", reservation.getId());
+ assertEquals(InventoryReservationState.HELD, reservation.getState());
+ assertEquals(1, reservation.getItems().size());
+ assertEquals("var_123", reservation.getItems().get(0).getVariantId());
+ assertNotNull(reservation.getLinks().getCommit());
+ assertNotNull(reservation.getLinks().getRelease());
+ }
+
+ @Test
+ void shouldRoundTripInventoryReservation() {
+ final InventoryReservation original = new InventoryReservation();
+ original.setId("rsv_tkoi5db4hryu5cei5vwoabr7we");
+ original.setState(InventoryReservationState.COMMITTED);
+ original.setOwnerType("ucp_session");
+ original.setOwnerReference("cs_8f42");
+ original.setItems(List.of(InventoryReservationItem.builder().variantId("var_123").quantity(2).build()));
+ original.setExpiresAt(Instant.parse("2026-07-14T08:47:00Z"));
+ original.setCreatedOn(Instant.parse("2026-07-14T08:32:00Z"));
+ final InventoryReservationLinks links = new InventoryReservationLinks();
+ final InventoryHalLink self = new InventoryHalLink();
+ self.setHref("https://example.com/self");
+ links.setSelf(self);
+ original.setLinks(links);
+
+ final String json = serializer.toJson(original);
+ final InventoryReservation deserialized = serializer.fromJson(json, InventoryReservation.class);
+
+ assertEquals(original.getId(), deserialized.getId());
+ assertEquals(original.getState(), deserialized.getState());
+ assertEquals(original.getItems(), deserialized.getItems());
+ }
+
+ // ------------------------------------------------------------------------
+ // InventoryProductKnowledge - covers all fields, including nested InventoryMoney,
+ // condition enum and _links (self/set/delete).
+ // ------------------------------------------------------------------------
+
+ @Test
+ void shouldDeserializeInventoryProductKnowledgeSwaggerExample() {
+ final String swaggerJson = "{"
+ + "\"variant_id\":\"var_123\",\"title\":\"Classic leather belt, brown\","
+ + "\"description\":\"A full-grain leather belt with a brushed nickel buckle.\","
+ + "\"product_url\":\"https://merchant.example.com/products/classic-leather-belt-brown\","
+ + "\"image_url\":\"https://merchant.example.com/images/belt-brown-main.jpg\","
+ + "\"model_3d_url\":\"https://merchant.example.com/models/belt-brown.glb\","
+ + "\"price\":{\"amount\":1999,\"currency\":\"USD\"},"
+ + "\"sale_price\":{\"amount\":1499,\"currency\":\"USD\"},"
+ + "\"condition\":\"new\","
+ + "\"created_on\":\"2026-08-01T09:15:00Z\",\"modified_on\":\"2026-08-13T14:02:11Z\","
+ + "\"_links\":{\"self\":{\"href\":\"https://example.com/self\"},"
+ + "\"set\":{\"href\":\"https://example.com/set\"},"
+ + "\"delete\":{\"href\":\"https://example.com/delete\"}}"
+ + "}";
+
+ final InventoryProductKnowledge product = serializer.fromJson(swaggerJson, InventoryProductKnowledge.class);
+
+ assertNotNull(product);
+ assertEquals("var_123", product.getVariantId());
+ assertEquals("https://merchant.example.com/models/belt-brown.glb", product.getModel3dUrl());
+ assertEquals(1999L, product.getPrice().getAmount());
+ assertEquals("USD", product.getPrice().getCurrency());
+ assertEquals(InventoryProductCondition.NEW, product.getCondition());
+ assertNotNull(product.getLinks().getDelete());
+ }
+
+ @Test
+ void shouldRoundTripInventoryProductKnowledgeWithAllOptionalFields() {
+ final InventoryProductKnowledge original = new InventoryProductKnowledge();
+ original.setVariantId("var_123");
+ original.setTitle("Classic leather belt, brown");
+ original.setDescription("A full-grain leather belt with a brushed nickel buckle.");
+ original.setProductUrl("https://merchant.example.com/products/classic-leather-belt-brown");
+ original.setImageUrl("https://merchant.example.com/images/belt-brown-main.jpg");
+ original.setAdditionalImageUrls(List.of("https://merchant.example.com/images/belt-brown-alt1.jpg"));
+ original.setVideoUrl("https://merchant.example.com/videos/belt-brown.mp4");
+ original.setModel3dUrl("https://merchant.example.com/models/belt-brown.glb");
+ original.setSku("BELT-BRN-001");
+ original.setGtin("00012345678905");
+ original.setMpn("MPN-4471");
+ original.setBrand("Acme Leathercraft");
+ original.setCategory("Apparel & Accessories > Belts");
+ original.setPrice(InventoryMoney.builder().amount(1999L).currency("USD").build());
+ original.setSalePrice(InventoryMoney.builder().amount(1499L).currency("USD").build());
+ original.setSalePriceStartsAt(Instant.parse("2026-08-01T00:00:00Z"));
+ original.setSalePriceEndsAt(Instant.parse("2026-08-31T23:59:59Z"));
+ original.setGroupId("grp_belt_classic");
+ original.setGroupTitle("Classic leather belt");
+ original.setColor("Brown");
+ original.setSize("M");
+ original.setSizeSystem("US");
+ original.setGender("unisex");
+ original.setCondition(InventoryProductCondition.USED);
+ original.setMaterial("Full-grain leather");
+ original.setAgeGroup("adult");
+ original.setLength(110d);
+ original.setWidth(3.5);
+ original.setHeight(0.5);
+ original.setDimensionUnit("cm");
+ original.setWeight(0.2);
+ original.setWeightUnit("kg");
+ original.setExpirationDate(Instant.parse("2027-01-01T00:00:00Z"));
+ original.setHarmonizedSystemCode("4203.30");
+ original.setCountryOfOrigin("IT");
+ original.setSellerName("Acme Leathercraft Ltd");
+ original.setSellerUrl("https://acme-leathercraft.example.com");
+ original.setSellerPrivacyPolicy("https://acme-leathercraft.example.com/privacy");
+ original.setSellerTos("https://acme-leathercraft.example.com/terms");
+ final InventoryProductLinks links = new InventoryProductLinks();
+ final InventoryHalLink self = new InventoryHalLink();
+ self.setHref("https://example.com/self");
+ links.setSelf(self);
+ original.setLinks(links);
+
+ final String json = serializer.toJson(original);
+ final InventoryProductKnowledge deserialized = serializer.fromJson(json, InventoryProductKnowledge.class);
+
+ assertEquals(original.getVariantId(), deserialized.getVariantId());
+ assertEquals(original.getModel3dUrl(), deserialized.getModel3dUrl());
+ assertEquals(original.getPrice(), deserialized.getPrice());
+ assertEquals(original.getCondition(), deserialized.getCondition());
+ assertEquals(original.getSalePriceStartsAt(), deserialized.getSalePriceStartsAt());
+ }
+
+}