Skip to content

Shared Model Types

These types appear across multiple clients in the SDK. All are immutable Java records unless noted.

Page<T>

Returned by every list() method. Carries the current page of items and an optional cursor for fetching the next page.

java
import de.joesst.dev.fulfillmenttools.model.Page;
import de.joesst.dev.fulfillmenttools.facilities.Facility;
import de.joesst.dev.fulfillmenttools.facilities.FacilityListRequest;

Page<Facility> page = client.facilities().list(
    FacilityListRequest.builder().size(20).build()
);

System.out.println("Items on this page: " + page.items().size());
System.out.println("Has more: " + page.hasMore());

if (page.hasMore()) {
    Page<Facility> next = client.facilities().list(
        FacilityListRequest.builder()
            .size(20)
            .startAfterId(page.nextCursor())
            .build()
    );
}
MethodReturnsDescription
items()List<T>Items on the current page
nextCursor()String (nullable)Cursor to pass as startAfterId for the next page
hasMore()booleantrue when nextCursor is non-null and non-empty

See the Pagination guide for automatic full-iteration with listAll().

Coordinates

Geographic coordinates for a facility location.

java
import de.joesst.dev.fulfillmenttools.model.Coordinates;

Coordinates coords = Coordinates.builder()
    .lat(52.520008)
    .lon(13.404954)
    .build();

System.out.println(coords.lat() + ", " + coords.lon());
FieldTypeDescription
lat()doubleLatitude in degrees
lon()doubleLongitude in degrees

TagReference

A reference to a tag, carrying the tag's typed ID and its value. Used on facilities, listings, and other resources that support tagging.

java
import de.joesst.dev.fulfillmenttools.model.TagReference;
import de.joesst.dev.fulfillmenttools.id.TagId;

TagReference tag = TagReference.builder()
    .id(TagId.builder().value("tag-001").build())
    .value("priority")
    .build();

System.out.println(tag.id().value() + " = " + tag.value());
FieldTypeDescription
id()TagIdThe tag's unique identifier
value()StringThe tag's label or value

AssignedUser

A user assigned to a pick job, pack job, or similar work item.

java
import de.joesst.dev.fulfillmenttools.model.AssignedUser;

// Reading from a job response:
AssignedUser user = pickJob.assignedUser();
System.out.println(user.username() + " (" + user.userId().value() + ")");
FieldTypeDescription
username()StringThe user's login name
userId()UserIdThe user's platform identifier

TimeZone

Time zone metadata for a facility or location.

java
import de.joesst.dev.fulfillmenttools.model.TimeZone;

// Reading from a facility response:
TimeZone tz = facility.timeZone();
System.out.println(tz.timeZoneId());   // e.g. "Europe/Berlin"
System.out.println(tz.timeZoneName()); // e.g. "Central European Time"
System.out.println(tz.offsetInSeconds()); // e.g. 3600.0
FieldTypeDescription
timeZoneId()StringIANA time zone identifier (e.g. "Europe/Berlin")
timeZoneName()StringHuman-readable name
offsetInSeconds()Double (nullable)UTC offset in seconds; null if unknown
source()StringSystem that determined this time zone

CustomAttributes

Tenant-defined extension data attached to a resource. Every model that exposes a customAttributes field returns a CustomAttributes instance instead of a raw Map<String, Object>.

Constructing for a request

Use CustomAttributes.of(Map) when building a request:

java
import de.joesst.dev.fulfillmenttools.model.CustomAttributes;

CustomAttributes attrs = CustomAttributes.of(Map.of(
    "priority", "high",
    "source",   "ERP"
));

Pass the result directly to any builder method that accepts customAttributes:

java
import de.joesst.dev.fulfillmenttools.stocks.CreateStockRequest;

CreateStockRequest request = CreateStockRequest.builder()
    .tenantArticleId(new TenantArticleId("SKU-001"))
    .value(100)
    .facilityRef(new FacilityId("fac-001"))
    .customAttributes(CustomAttributes.of(Map.of("warehouseZone", "A3")))
    .build();

Note: Builder methods that previously accepted Map<String, Object> directly now require CustomAttributes. Wrap existing maps with CustomAttributes.of(Map.of(...)).

Reading attributes from a response

attributes() returns the raw map (unmodifiable):

java
CustomAttributes attrs = stock.customAttributes();
Map<String, Object> raw = attrs.attributes();
System.out.println(raw.get("warehouseZone")); // "A3"

Converting to a typed POJO

as(Class<T>) converts the attributes map to your own type using the SDK's Jackson ObjectMapper — no I/O is performed. Map keys must match the record component names exactly (camelCase by default):

java
record WarehouseMeta(String warehouseZone, int shelfNumber) {}

WarehouseMeta meta = stock.customAttributes().as(WarehouseMeta.class);
System.out.println(meta.warehouseZone()); // "A3"
System.out.println(meta.shelfNumber());   // 7

Conversion fails with IllegalArgumentException if the attributes map is missing required fields or contains incompatible types.

Wiring into a custom ObjectMapper

If you configure your own Jackson ObjectMapper with field-level visibility (e.g. PropertyAccessor.FIELD = ANY), register the SDK's module so that as() uses that same mapper:

java
import com.fasterxml.jackson.databind.ObjectMapper;
import de.joesst.dev.fulfillmenttools.model.CustomAttributes;

ObjectMapper myMapper = new ObjectMapper();
// ... your own configuration ...
myMapper.registerModule(CustomAttributes.jacksonModule(myMapper));

Without this registration, as() still works — it falls back to the SDK's default mapper — but the conversion ignores any custom serializers you may have configured.

API reference

Method / FactoryReturnsDescription
CustomAttributes.of(Map<String, Object>)CustomAttributesStatic factory for building requests
attributes()Map<String, Object>Raw attributes map (unmodifiable)
as(Class<T> type)TConverts attributes to the given POJO type
CustomAttributes.jacksonModule(ObjectMapper)SimpleModuleJackson module to wire a custom mapper into as()

Money

A monetary amount represented as an integer-like value paired with a decimal-places indicator. This design avoids floating-point conversion errors: value is always a whole number and decimalPlaces tells you how many digits to shift right to get the real amount.

java
import de.joesst.dev.fulfillmenttools.model.Money;

Money price = Money.builder()
    .value(499.0)
    .currency("EUR")
    .decimalPlaces(2.0)
    .build();

System.out.println(price.value());         // 499.0
System.out.println(price.currency());      // "EUR"
System.out.println(price.decimalPlaces()); // 2.0
System.out.println(price.toAmount());      // 4.99
Field / MethodTypeDescription
value()DoubleThe amount as a whole number (e.g. 499 represents €4.99). Stored without decimal shift to avoid floating-point errors.
currency()StringISO 4217 currency code (e.g. "EUR", "USD")
decimalPlaces()DoubleHow many decimal places to shift value right to get the real amount (e.g. 2 means divide by 100). Set by whoever creates the value, not derived from the currency.
toAmount()DoubleComputes the real amount: value / 10^decimalPlaces (e.g. 499 / 10^2 = 4.99). Returns value unchanged when decimalPlaces is null or zero; returns null when value is null.

Exceptions

All SDK exceptions extend FulfillmenttoolsException, which extends RuntimeException. You never need to declare them in method signatures.

Exception hierarchy

FulfillmenttoolsException  (base — all SDK errors)
├── NotFoundException        (404)
├── ValidationException      (400)
├── ConflictException        (409)
├── AuthenticationException  (401)
├── AuthorizationException   (403)
├── RateLimitException       (429)
├── ServerException          (5xx)
└── TransportException       (network / IO failures)

FulfillmenttoolsException

The base class provides structured access to the error response:

java
import de.joesst.dev.fulfillmenttools.id.FacilityId;
import de.joesst.dev.fulfillmenttools.FulfillmenttoolsException;
import de.joesst.dev.fulfillmenttools.ApiError;

try {
    client.facilities().get(FacilityId.builder().value("fac-001").build());
} catch (FulfillmenttoolsException e) {
    System.out.println("HTTP status: " + e.statusCode());
    System.out.println("Request ID: " + e.requestId());
    for (ApiError error : e.errors()) {
        System.out.println(error.summary() + ": " + error.description());
    }
}
MethodReturnsDescription
statusCode()intHTTP status code, or -1 for non-HTTP failures
requestId()String (nullable)Value of the x-request-id response header
errors()List<ApiError>Structured error details; empty for non-HTTP failures
getMessage()StringHuman-readable summary (first error's summary, or "HTTP <code>")

NotFoundException

Thrown for 404 responses. Catch it before FulfillmenttoolsException when you want to handle "not found" separately:

java
import de.joesst.dev.fulfillmenttools.id.FacilityId;
import de.joesst.dev.fulfillmenttools.NotFoundException;
import de.joesst.dev.fulfillmenttools.FulfillmenttoolsException;

try {
    client.facilities().get(FacilityId.builder().value("fac-001").build());
} catch (NotFoundException e) {
    System.out.println("Facility does not exist");
} catch (FulfillmenttoolsException e) {
    System.out.println("Request failed: " + e.getMessage());
}

RateLimitException

Thrown for 429 responses. Provides the optional Retry-After delay:

java
import de.joesst.dev.fulfillmenttools.RateLimitException;
import java.time.Duration;

try {
    client.facilities().list(FacilityListRequest.builder().build());
} catch (RateLimitException e) {
    Duration wait = e.retryAfter().orElse(Duration.ofSeconds(60));
    System.out.println("Rate limited — retry after " + wait.toSeconds() + "s");
}

ApiError

Structured error detail returned inside FulfillmenttoolsException.errors():

FieldTypeDescription
summary()StringShort error title
description()StringDetailed error message
requestVersion()intRequest format version
version()intError response format version