diff --git a/CHANGES.MD b/CHANGES.MD index 408f53d..7a05939 100644 --- a/CHANGES.MD +++ b/CHANGES.MD @@ -1,3 +1,10 @@ +3.23.0 (2026-08-18) +================= +- Added support for the Global Profile API: `GlobalProfileFieldSet` / `GlobalProfileRequest` for + `GET /v3/accounts/{accountId}/global_profile/users/{userId}`, and + `GlobalProfileLookupFieldSet` / `GlobalProfileLookupRequest` for + `POST /v3/accounts/{accountId}/global_profile/lookup` + 3.22.0 (2026-01-05) ================= - Added support for `$user_email`, `$review.$reviewed_user_id` fields to `$create_content`, `$update_content` events diff --git a/README.md b/README.md index efe027b..77a4f0f 100644 --- a/README.md +++ b/README.md @@ -332,3 +332,48 @@ DecisionStatusRequest request = client.buildRequest(new DecisionStatusFieldSet() .setUserId("a_user_id") .setEntityId("a_content_id")); ``` + +### Global Profile API + +[API Docs](https://sift.com/developers/docs/java/global-profile-api) + +The Global Profile API returns cross-tenant identity, decision, chargeback, order, transaction, and +location signals for a user, either by user ID or by looking them up via email/phone. + +#### Get a user's global profile + +To retrieve a global profile by user id, build a request with a `GlobalProfileFieldSet`. +```java +GlobalProfileRequest request = client.buildRequest(new GlobalProfileFieldSet() + .setUserId("a_user_id")); +``` + +`global_only` (excludes the requesting tenant's own network connections, defaults to `false`) and +`include_own_data` (includes the requested user's own feature values, defaults to `true`) may +optionally be set. +```java +GlobalProfileRequest request = client.buildRequest(new GlobalProfileFieldSet() + .setUserId("a_user_id") + .setGlobalOnly(true) + .setIncludeOwnData(false)); + +GlobalProfileResponse response = request.send(); +response.getBody().getProfileSummary().getIdentityFound(); +``` + +If `identity_found` is `false`, every field on the response body other than `profile_summary` will +be `null`. + +#### Look up a global profile by email or phone + +To look up a global profile without a Sift user id, build a request with a +`GlobalProfileLookupFieldSet`, providing at least one of `email` or `phone`. +```java +GlobalProfileLookupRequest request = client.buildRequest(new GlobalProfileLookupFieldSet() + .setEmail("jane.doe@example.com") + .setPhone("+15551234567")); + +GlobalProfileLookupResponse response = request.send(); +``` + +Omitting both `email` and `phone` will raise a `MissingFieldException` before any request is sent. diff --git a/build.gradle b/build.gradle index b46c813..38b05ae 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'signing' apply plugin: 'java-library-distribution' group = 'com.siftscience' -version = '3.22.0' +version = '3.23.0' repositories { mavenCentral() diff --git a/src/main/java/com/siftscience/GlobalProfileLookupRequest.java b/src/main/java/com/siftscience/GlobalProfileLookupRequest.java new file mode 100644 index 0000000..b32971c --- /dev/null +++ b/src/main/java/com/siftscience/GlobalProfileLookupRequest.java @@ -0,0 +1,37 @@ +package com.siftscience; + +import java.io.IOException; + +import com.siftscience.model.GlobalProfileLookupFieldSet; +import okhttp3.Credentials; +import okhttp3.HttpUrl; +import okhttp3.Request; + +public class GlobalProfileLookupRequest extends SiftRequest { + + GlobalProfileLookupRequest(HttpUrl baseUrl, String accountId, HttpClient httpClient, + GlobalProfileLookupFieldSet fields) { + super(baseUrl, accountId, httpClient, fields); + } + + @Override + protected HttpUrl path(HttpUrl baseUrl) { + return baseUrl.newBuilder("/v3/accounts") + .addPathSegment(getAccountId()) + .addPathSegment("global_profile") + .addPathSegment("lookup") + .build(); + } + + @Override + GlobalProfileLookupResponse buildResponse(okhttp3.Response response, FieldSet requestFields) + throws IOException { + return new GlobalProfileLookupResponse(response, requestFields); + } + + @Override + protected void modifyRequestBuilder(Request.Builder builder) { + super.modifyRequestBuilder(builder); + builder.header("Authorization", Credentials.basic(fieldSet.getApiKey(), "")); + } +} diff --git a/src/main/java/com/siftscience/GlobalProfileLookupResponse.java b/src/main/java/com/siftscience/GlobalProfileLookupResponse.java new file mode 100644 index 0000000..957ad0c --- /dev/null +++ b/src/main/java/com/siftscience/GlobalProfileLookupResponse.java @@ -0,0 +1,20 @@ +package com.siftscience; + +import com.siftscience.model.GlobalProfileResponseBody; +import okhttp3.Response; + +import java.io.IOException; + +import static com.siftscience.FieldSet.gson; + +public class GlobalProfileLookupResponse extends SiftResponse { + + public GlobalProfileLookupResponse(Response okResponse, FieldSet requestBody) throws IOException { + super(okResponse, requestBody); + } + + @Override + void populateBodyFromJson(String jsonBody) { + body = gson.fromJson(jsonBody, GlobalProfileResponseBody.class); + } +} diff --git a/src/main/java/com/siftscience/GlobalProfileRequest.java b/src/main/java/com/siftscience/GlobalProfileRequest.java new file mode 100644 index 0000000..694cb46 --- /dev/null +++ b/src/main/java/com/siftscience/GlobalProfileRequest.java @@ -0,0 +1,66 @@ +package com.siftscience; + +import java.io.IOException; + +import com.siftscience.model.GlobalProfileFieldSet; +import okhttp3.Credentials; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; + +public class GlobalProfileRequest extends SiftRequest { + + GlobalProfileRequest(HttpUrl baseUrl, String accountId, HttpClient httpClient, + GlobalProfileFieldSet fields) { + super(baseUrl, accountId, httpClient, fields); + } + + public enum Query { + GLOBAL_ONLY("global_only"), + INCLUDE_OWN_DATA("include_own_data"); + + private final String value; + + Query(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + + @Override + protected HttpUrl path(HttpUrl baseUrl) { + GlobalProfileFieldSet fieldSet = (GlobalProfileFieldSet) this.fieldSet; + HttpUrl.Builder path = baseUrl.newBuilder("/v3/accounts") + .addPathSegment(getAccountId()) + .addPathSegment("global_profile") + .addPathSegment("users") + .addPathSegment(fieldSet.getUserId()); + + if (fieldSet.getGlobalOnly() != null) { + path.addQueryParameter(Query.GLOBAL_ONLY.toString(), + String.valueOf(fieldSet.getGlobalOnly())); + } + if (fieldSet.getIncludeOwnData() != null) { + path.addQueryParameter(Query.INCLUDE_OWN_DATA.toString(), + String.valueOf(fieldSet.getIncludeOwnData())); + } + + return path.build(); + } + + @Override + GlobalProfileResponse buildResponse(Response response, FieldSet requestFields) + throws IOException { + return new GlobalProfileResponse(response, requestFields); + } + + @Override + protected void modifyRequestBuilder(Request.Builder builder) { + super.modifyRequestBuilder(builder); + builder.header("Authorization", Credentials.basic(fieldSet.getApiKey(), "")).get(); + } +} diff --git a/src/main/java/com/siftscience/GlobalProfileResponse.java b/src/main/java/com/siftscience/GlobalProfileResponse.java new file mode 100644 index 0000000..eeb2f88 --- /dev/null +++ b/src/main/java/com/siftscience/GlobalProfileResponse.java @@ -0,0 +1,20 @@ +package com.siftscience; + +import com.siftscience.model.GlobalProfileResponseBody; +import okhttp3.Response; + +import java.io.IOException; + +import static com.siftscience.FieldSet.gson; + +public class GlobalProfileResponse extends SiftResponse { + + public GlobalProfileResponse(Response okResponse, FieldSet requestBody) throws IOException { + super(okResponse, requestBody); + } + + @Override + void populateBodyFromJson(String jsonBody) { + body = gson.fromJson(jsonBody, GlobalProfileResponseBody.class); + } +} diff --git a/src/main/java/com/siftscience/SiftClient.java b/src/main/java/com/siftscience/SiftClient.java index e90071b..9174b0c 100644 --- a/src/main/java/com/siftscience/SiftClient.java +++ b/src/main/java/com/siftscience/SiftClient.java @@ -3,6 +3,8 @@ import com.siftscience.model.ApplyDecisionFieldSet; import com.siftscience.model.DecisionStatusFieldSet; import com.siftscience.model.GetDecisionFieldSet; +import com.siftscience.model.GlobalProfileFieldSet; +import com.siftscience.model.GlobalProfileLookupFieldSet; import com.siftscience.model.LabelFieldSet; import com.siftscience.model.ScoreFieldSet; import com.siftscience.model.UnlabelFieldSet; @@ -108,6 +110,18 @@ public GetDecisionsRequest buildRequest(GetDecisionFieldSet fields) { return new GetDecisionsRequest(baseUrl, getAccountId(), httpClient, fields); } + public GlobalProfileRequest buildRequest(GlobalProfileFieldSet fields) { + assertAccountIdIsNotNull(); + setupApiKey(fields); + return new GlobalProfileRequest(baseUrl, getAccountId(), httpClient, fields); + } + + public GlobalProfileLookupRequest buildRequest(GlobalProfileLookupFieldSet fields) { + assertAccountIdIsNotNull(); + setupApiKey(fields); + return new GlobalProfileLookupRequest(baseUrl, getAccountId(), httpClient, fields); + } + public DecisionStatusRequest buildRequest(DecisionStatusFieldSet fields) { assertAccountIdIsNotNull(); setupApiKey(fields); diff --git a/src/main/java/com/siftscience/model/GlobalProfileChargebacks.java b/src/main/java/com/siftscience/model/GlobalProfileChargebacks.java new file mode 100644 index 0000000..5d4686f --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileChargebacks.java @@ -0,0 +1,57 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class GlobalProfileChargebacks { + @Expose @SerializedName("total") private Long total; + @Expose @SerializedName("fraudulent") private Long fraudulent; + @Expose @SerializedName("other") private Long other; + @Expose @SerializedName("last_timestamp") private Long lastTimestamp; + @Expose @SerializedName("last_fraudulent_timestamp") private Long lastFraudulentTimestamp; + + public Long getTotal() { + return total; + } + + public GlobalProfileChargebacks setTotal(Long total) { + this.total = total; + return this; + } + + public Long getFraudulent() { + return fraudulent; + } + + public GlobalProfileChargebacks setFraudulent(Long fraudulent) { + this.fraudulent = fraudulent; + return this; + } + + public Long getOther() { + return other; + } + + public GlobalProfileChargebacks setOther(Long other) { + this.other = other; + return this; + } + + public Long getLastTimestamp() { + return lastTimestamp; + } + + public GlobalProfileChargebacks setLastTimestamp(Long lastTimestamp) { + this.lastTimestamp = lastTimestamp; + return this; + } + + public Long getLastFraudulentTimestamp() { + return lastFraudulentTimestamp; + } + + public GlobalProfileChargebacks setLastFraudulentTimestamp(Long lastFraudulentTimestamp) { + this.lastFraudulentTimestamp = lastFraudulentTimestamp; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileFieldSet.java b/src/main/java/com/siftscience/model/GlobalProfileFieldSet.java new file mode 100644 index 0000000..98108c9 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileFieldSet.java @@ -0,0 +1,64 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import com.siftscience.FieldSet; +import com.siftscience.exception.MissingFieldException; + +/** + * Field set for {@code GET /v3/accounts/{accountId}/global_profile/users/{userId}}. + */ +public class GlobalProfileFieldSet extends FieldSet { + @Expose @SerializedName("user_id") private String userId; + @Expose @SerializedName("global_only") private Boolean globalOnly; + @Expose @SerializedName("include_own_data") private Boolean includeOwnData; + + public GlobalProfileFieldSet() {} + + public static GlobalProfileFieldSet fromJson(String json) { + return gson.fromJson(json, GlobalProfileFieldSet.class); + } + + @Override + public void validate() { + super.validate(); + if (userId == null || userId.isEmpty()) { + throw new MissingFieldException("'userId' is required for a global profile request."); + } + } + + public String getUserId() { + return userId; + } + + public GlobalProfileFieldSet setUserId(String userId) { + this.userId = userId; + return this; + } + + /** + * If true, excludes the requesting tenant's own network connections from the response. + * Defaults to false server-side if not set. + */ + public Boolean getGlobalOnly() { + return globalOnly; + } + + public GlobalProfileFieldSet setGlobalOnly(Boolean globalOnly) { + this.globalOnly = globalOnly; + return this; + } + + /** + * If true, includes the requested user's own feature values in the response. Defaults to + * true server-side if not set. + */ + public Boolean getIncludeOwnData() { + return includeOwnData; + } + + public GlobalProfileFieldSet setIncludeOwnData(Boolean includeOwnData) { + this.includeOwnData = includeOwnData; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileIdentityAge.java b/src/main/java/com/siftscience/model/GlobalProfileIdentityAge.java new file mode 100644 index 0000000..3a76f06 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileIdentityAge.java @@ -0,0 +1,37 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class GlobalProfileIdentityAge { + @Expose @SerializedName("oldest_account_age_timestamp") private Long oldestAccountAgeTimestamp; + @Expose @SerializedName("newest_account_age_timestamp") private Long newestAccountAgeTimestamp; + @Expose @SerializedName("average_account_age_timestamp") private Long averageAccountAgeTimestamp; + + public Long getOldestAccountAgeTimestamp() { + return oldestAccountAgeTimestamp; + } + + public GlobalProfileIdentityAge setOldestAccountAgeTimestamp(Long oldestAccountAgeTimestamp) { + this.oldestAccountAgeTimestamp = oldestAccountAgeTimestamp; + return this; + } + + public Long getNewestAccountAgeTimestamp() { + return newestAccountAgeTimestamp; + } + + public GlobalProfileIdentityAge setNewestAccountAgeTimestamp(Long newestAccountAgeTimestamp) { + this.newestAccountAgeTimestamp = newestAccountAgeTimestamp; + return this; + } + + public Long getAverageAccountAgeTimestamp() { + return averageAccountAgeTimestamp; + } + + public GlobalProfileIdentityAge setAverageAccountAgeTimestamp(Long averageAccountAgeTimestamp) { + this.averageAccountAgeTimestamp = averageAccountAgeTimestamp; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileLocationAccount.java b/src/main/java/com/siftscience/model/GlobalProfileLocationAccount.java new file mode 100644 index 0000000..9f290f3 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileLocationAccount.java @@ -0,0 +1,41 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * A single connected account location entry as returned within a Global Profile API response's + * `locations.location_connected_accounts` list. + */ +public class GlobalProfileLocationAccount { + @Expose @SerializedName("city") private String city; + @Expose @SerializedName("region") private String region; + @Expose @SerializedName("country") private String country; + + public String getCity() { + return city; + } + + public GlobalProfileLocationAccount setCity(String city) { + this.city = city; + return this; + } + + public String getRegion() { + return region; + } + + public GlobalProfileLocationAccount setRegion(String region) { + this.region = region; + return this; + } + + public String getCountry() { + return country; + } + + public GlobalProfileLocationAccount setCountry(String country) { + this.country = country; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileLocations.java b/src/main/java/com/siftscience/model/GlobalProfileLocations.java new file mode 100644 index 0000000..0c2977c --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileLocations.java @@ -0,0 +1,71 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class GlobalProfileLocations { + @Expose @SerializedName("unique_billing_addresses") private Long uniqueBillingAddresses; + @Expose @SerializedName("unique_shipping_addresses") private Long uniqueShippingAddresses; + @Expose @SerializedName("distinct_countries_count") private Long distinctCountriesCount; + @Expose @SerializedName("distinct_regions_count") private Long distinctRegionsCount; + @Expose @SerializedName("location_connected_accounts") + private List locationConnectedAccounts; + @Expose @SerializedName("location_last_used_timestamp") private Long locationLastUsedTimestamp; + + public Long getUniqueBillingAddresses() { + return uniqueBillingAddresses; + } + + public GlobalProfileLocations setUniqueBillingAddresses(Long uniqueBillingAddresses) { + this.uniqueBillingAddresses = uniqueBillingAddresses; + return this; + } + + public Long getUniqueShippingAddresses() { + return uniqueShippingAddresses; + } + + public GlobalProfileLocations setUniqueShippingAddresses(Long uniqueShippingAddresses) { + this.uniqueShippingAddresses = uniqueShippingAddresses; + return this; + } + + public Long getDistinctCountriesCount() { + return distinctCountriesCount; + } + + public GlobalProfileLocations setDistinctCountriesCount(Long distinctCountriesCount) { + this.distinctCountriesCount = distinctCountriesCount; + return this; + } + + public Long getDistinctRegionsCount() { + return distinctRegionsCount; + } + + public GlobalProfileLocations setDistinctRegionsCount(Long distinctRegionsCount) { + this.distinctRegionsCount = distinctRegionsCount; + return this; + } + + public List getLocationConnectedAccounts() { + return locationConnectedAccounts; + } + + public GlobalProfileLocations setLocationConnectedAccounts( + List locationConnectedAccounts) { + this.locationConnectedAccounts = locationConnectedAccounts; + return this; + } + + public Long getLocationLastUsedTimestamp() { + return locationLastUsedTimestamp; + } + + public GlobalProfileLocations setLocationLastUsedTimestamp(Long locationLastUsedTimestamp) { + this.locationLastUsedTimestamp = locationLastUsedTimestamp; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileLookupFieldSet.java b/src/main/java/com/siftscience/model/GlobalProfileLookupFieldSet.java new file mode 100644 index 0000000..22fdeb1 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileLookupFieldSet.java @@ -0,0 +1,48 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import com.siftscience.FieldSet; +import com.siftscience.exception.MissingFieldException; + +/** + * Field set for {@code POST /v3/accounts/{accountId}/global_profile/lookup}. At least one of + * {@code email} or {@code phone} is required. + */ +public class GlobalProfileLookupFieldSet extends FieldSet { + @Expose @SerializedName("email") private String email; + @Expose @SerializedName("phone") private String phone; + + public GlobalProfileLookupFieldSet() {} + + public static GlobalProfileLookupFieldSet fromJson(String json) { + return gson.fromJson(json, GlobalProfileLookupFieldSet.class); + } + + public String getEmail() { + return email; + } + + public GlobalProfileLookupFieldSet setEmail(String email) { + this.email = email; + return this; + } + + public String getPhone() { + return phone; + } + + public GlobalProfileLookupFieldSet setPhone(String phone) { + this.phone = phone; + return this; + } + + @Override + public void validate() { + super.validate(); + if ((email == null || email.isEmpty()) && (phone == null || phone.isEmpty())) { + throw new MissingFieldException( + "At least one of 'email' or 'phone' is required for a global profile lookup."); + } + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileOrders.java b/src/main/java/com/siftscience/model/GlobalProfileOrders.java new file mode 100644 index 0000000..0a3aa47 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileOrders.java @@ -0,0 +1,67 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class GlobalProfileOrders { + @Expose @SerializedName("total") private Long total; + @Expose @SerializedName("blocked") private Long blocked; + @Expose @SerializedName("watched") private Long watched; + @Expose @SerializedName("accepted") private Long accepted; + @Expose @SerializedName("last_timestamp") private Long lastTimestamp; + @Expose @SerializedName("last_blocked_timestamp") private Long lastBlockedTimestamp; + + public Long getTotal() { + return total; + } + + public GlobalProfileOrders setTotal(Long total) { + this.total = total; + return this; + } + + public Long getBlocked() { + return blocked; + } + + public GlobalProfileOrders setBlocked(Long blocked) { + this.blocked = blocked; + return this; + } + + public Long getWatched() { + return watched; + } + + public GlobalProfileOrders setWatched(Long watched) { + this.watched = watched; + return this; + } + + public Long getAccepted() { + return accepted; + } + + public GlobalProfileOrders setAccepted(Long accepted) { + this.accepted = accepted; + return this; + } + + public Long getLastTimestamp() { + return lastTimestamp; + } + + public GlobalProfileOrders setLastTimestamp(Long lastTimestamp) { + this.lastTimestamp = lastTimestamp; + return this; + } + + public Long getLastBlockedTimestamp() { + return lastBlockedTimestamp; + } + + public GlobalProfileOrders setLastBlockedTimestamp(Long lastBlockedTimestamp) { + this.lastBlockedTimestamp = lastBlockedTimestamp; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileResponseBody.java b/src/main/java/com/siftscience/model/GlobalProfileResponseBody.java new file mode 100644 index 0000000..4552734 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileResponseBody.java @@ -0,0 +1,105 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * Response body for the Global Profile API, returned by both the + * {@code GET /v3/accounts/{accountId}/global_profile/users/{userId}} and + * {@code POST /v3/accounts/{accountId}/global_profile/lookup} endpoints. + * + * When {@code profile_summary.identity_found} is {@code false}, every field other than + * {@code profile_summary} will be {@code null}. + */ +public class GlobalProfileResponseBody extends BaseResponseBody { + @Expose @SerializedName("error_code") private Integer errorCode; + @Expose @SerializedName("lookback_months") private Integer lookbackMonths; + @Expose @SerializedName("profile_summary") private GlobalProfileSummary profileSummary; + @Expose @SerializedName("identity_age") private GlobalProfileIdentityAge identityAge; + @Expose @SerializedName("user_decisions") private GlobalProfileUserDecisions userDecisions; + @Expose @SerializedName("chargebacks") private GlobalProfileChargebacks chargebacks; + @Expose @SerializedName("orders") private GlobalProfileOrders orders; + @Expose @SerializedName("transactions") private GlobalProfileTransactions transactions; + @Expose @SerializedName("locations") private GlobalProfileLocations locations; + + public Integer getErrorCode() { + return errorCode; + } + + public GlobalProfileResponseBody setErrorCode(Integer errorCode) { + this.errorCode = errorCode; + return this; + } + + public Integer getLookbackMonths() { + return lookbackMonths; + } + + public GlobalProfileResponseBody setLookbackMonths(Integer lookbackMonths) { + this.lookbackMonths = lookbackMonths; + return this; + } + + public GlobalProfileSummary getProfileSummary() { + return profileSummary; + } + + public GlobalProfileResponseBody setProfileSummary(GlobalProfileSummary profileSummary) { + this.profileSummary = profileSummary; + return this; + } + + public GlobalProfileIdentityAge getIdentityAge() { + return identityAge; + } + + public GlobalProfileResponseBody setIdentityAge(GlobalProfileIdentityAge identityAge) { + this.identityAge = identityAge; + return this; + } + + public GlobalProfileUserDecisions getUserDecisions() { + return userDecisions; + } + + public GlobalProfileResponseBody setUserDecisions(GlobalProfileUserDecisions userDecisions) { + this.userDecisions = userDecisions; + return this; + } + + public GlobalProfileChargebacks getChargebacks() { + return chargebacks; + } + + public GlobalProfileResponseBody setChargebacks(GlobalProfileChargebacks chargebacks) { + this.chargebacks = chargebacks; + return this; + } + + public GlobalProfileOrders getOrders() { + return orders; + } + + public GlobalProfileResponseBody setOrders(GlobalProfileOrders orders) { + this.orders = orders; + return this; + } + + public GlobalProfileTransactions getTransactions() { + return transactions; + } + + public GlobalProfileResponseBody setTransactions(GlobalProfileTransactions transactions) { + this.transactions = transactions; + return this; + } + + public GlobalProfileLocations getLocations() { + return locations; + } + + public GlobalProfileResponseBody setLocations(GlobalProfileLocations locations) { + this.locations = locations; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileSummary.java b/src/main/java/com/siftscience/model/GlobalProfileSummary.java new file mode 100644 index 0000000..651d4b3 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileSummary.java @@ -0,0 +1,54 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +/** + * Summary information about a user's global profile, as returned by the Global Profile API. + */ +public class GlobalProfileSummary { + @Expose @SerializedName("identity_found") private Boolean identityFound; + @Expose @SerializedName("has_links") private Boolean hasLinks; + @Expose @SerializedName("link_count") private Long linkCount; + @Expose @SerializedName("linked_accounts_count_per_industry") + private Map linkedAccountsCountPerIndustry; + + public Boolean getIdentityFound() { + return identityFound; + } + + public GlobalProfileSummary setIdentityFound(Boolean identityFound) { + this.identityFound = identityFound; + return this; + } + + public Boolean getHasLinks() { + return hasLinks; + } + + public GlobalProfileSummary setHasLinks(Boolean hasLinks) { + this.hasLinks = hasLinks; + return this; + } + + public Long getLinkCount() { + return linkCount; + } + + public GlobalProfileSummary setLinkCount(Long linkCount) { + this.linkCount = linkCount; + return this; + } + + public Map getLinkedAccountsCountPerIndustry() { + return linkedAccountsCountPerIndustry; + } + + public GlobalProfileSummary setLinkedAccountsCountPerIndustry( + Map linkedAccountsCountPerIndustry) { + this.linkedAccountsCountPerIndustry = linkedAccountsCountPerIndustry; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileTransactions.java b/src/main/java/com/siftscience/model/GlobalProfileTransactions.java new file mode 100644 index 0000000..fd16768 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileTransactions.java @@ -0,0 +1,67 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class GlobalProfileTransactions { + @Expose @SerializedName("total") private Long total; + @Expose @SerializedName("failed_fraud") private Long failedFraud; + @Expose @SerializedName("failed_other") private Long failedOther; + @Expose @SerializedName("successful") private Long successful; + @Expose @SerializedName("last_timestamp") private Long lastTimestamp; + @Expose @SerializedName("last_failed_fraud_timestamp") private Long lastFailedFraudTimestamp; + + public Long getTotal() { + return total; + } + + public GlobalProfileTransactions setTotal(Long total) { + this.total = total; + return this; + } + + public Long getFailedFraud() { + return failedFraud; + } + + public GlobalProfileTransactions setFailedFraud(Long failedFraud) { + this.failedFraud = failedFraud; + return this; + } + + public Long getFailedOther() { + return failedOther; + } + + public GlobalProfileTransactions setFailedOther(Long failedOther) { + this.failedOther = failedOther; + return this; + } + + public Long getSuccessful() { + return successful; + } + + public GlobalProfileTransactions setSuccessful(Long successful) { + this.successful = successful; + return this; + } + + public Long getLastTimestamp() { + return lastTimestamp; + } + + public GlobalProfileTransactions setLastTimestamp(Long lastTimestamp) { + this.lastTimestamp = lastTimestamp; + return this; + } + + public Long getLastFailedFraudTimestamp() { + return lastFailedFraudTimestamp; + } + + public GlobalProfileTransactions setLastFailedFraudTimestamp(Long lastFailedFraudTimestamp) { + this.lastFailedFraudTimestamp = lastFailedFraudTimestamp; + return this; + } +} diff --git a/src/main/java/com/siftscience/model/GlobalProfileUserDecisions.java b/src/main/java/com/siftscience/model/GlobalProfileUserDecisions.java new file mode 100644 index 0000000..f7354e0 --- /dev/null +++ b/src/main/java/com/siftscience/model/GlobalProfileUserDecisions.java @@ -0,0 +1,87 @@ +package com.siftscience.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class GlobalProfileUserDecisions { + @Expose @SerializedName("total") private Long total; + @Expose @SerializedName("blocked") private Long blocked; + @Expose @SerializedName("watched") private Long watched; + @Expose @SerializedName("accepted") private Long accepted; + @Expose @SerializedName("manual") private Long manual; + @Expose @SerializedName("auto") private Long auto; + @Expose @SerializedName("last_type") private String lastType; + @Expose @SerializedName("last_timestamp") private Long lastTimestamp; + + public Long getTotal() { + return total; + } + + public GlobalProfileUserDecisions setTotal(Long total) { + this.total = total; + return this; + } + + public Long getBlocked() { + return blocked; + } + + public GlobalProfileUserDecisions setBlocked(Long blocked) { + this.blocked = blocked; + return this; + } + + public Long getWatched() { + return watched; + } + + public GlobalProfileUserDecisions setWatched(Long watched) { + this.watched = watched; + return this; + } + + public Long getAccepted() { + return accepted; + } + + public GlobalProfileUserDecisions setAccepted(Long accepted) { + this.accepted = accepted; + return this; + } + + public Long getManual() { + return manual; + } + + public GlobalProfileUserDecisions setManual(Long manual) { + this.manual = manual; + return this; + } + + public Long getAuto() { + return auto; + } + + public GlobalProfileUserDecisions setAuto(Long auto) { + this.auto = auto; + return this; + } + + public String getLastType() { + return lastType; + } + + public GlobalProfileUserDecisions setLastType(String lastType) { + this.lastType = lastType; + return this; + } + + public Long getLastTimestamp() { + return lastTimestamp; + } + + public GlobalProfileUserDecisions setLastTimestamp(Long lastTimestamp) { + this.lastTimestamp = lastTimestamp; + return this; + } +} diff --git a/src/test/java/com/siftscience/GlobalProfileLookupTest.java b/src/test/java/com/siftscience/GlobalProfileLookupTest.java new file mode 100644 index 0000000..9dec094 --- /dev/null +++ b/src/test/java/com/siftscience/GlobalProfileLookupTest.java @@ -0,0 +1,163 @@ +package com.siftscience; + +import com.siftscience.exception.MissingFieldException; +import com.siftscience.model.GlobalProfileLookupFieldSet; +import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Assert; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import static java.net.HttpURLConnection.HTTP_OK; + +public class GlobalProfileLookupTest { + + @Test + public void testGlobalProfileLookupByEmail() throws Exception { + String accountId = "YOUR_ACCOUNT_ID"; + String responseBody = "{\n" + + " \"status\": 0,\n" + + " \"error_message\": \"OK\",\n" + + " \"lookback_months\": 12,\n" + + " \"profile_summary\": {\n" + + " \"identity_found\": true,\n" + + " \"has_links\": true,\n" + + " \"link_count\": 7,\n" + + " \"linked_accounts_count_per_industry\": {\"finances\": 3, \"internet\": 4}\n" + + " }\n" + + "}"; + + MockWebServer server = new MockWebServer(); + MockResponse response = new MockResponse(); + response.setResponseCode(HTTP_OK); + response.setBody(responseBody); + server.enqueue(response); + server.start(); + + SiftClient client = new SiftClient("YOUR_API_KEY", accountId, + new OkHttpClient.Builder() + .addInterceptor(OkHttpUtils.urlRewritingInterceptor(server)) + .build()); + + GlobalProfileLookupRequest lookupRequest = client.buildRequest( + new GlobalProfileLookupFieldSet() + .setEmail("jane.doe@example.com")); + + GlobalProfileLookupResponse siftResponse = lookupRequest.send(); + + RecordedRequest request = server.takeRequest(); + Assert.assertEquals("POST", request.getMethod()); + Assert.assertEquals("/v3/accounts/" + accountId + "/global_profile/lookup", + request.getPath()); + Assert.assertEquals(request.getHeader("Authorization"), "Basic WU9VUl9BUElfS0VZOg=="); + JSONAssert.assertEquals("{\"email\": \"jane.doe@example.com\"}", + request.getBody().readUtf8(), false); + + Assert.assertEquals(HTTP_OK, siftResponse.getHttpStatusCode()); + JSONAssert.assertEquals(response.getBody().readUtf8(), + siftResponse.getBody().toJson(), true); + Assert.assertTrue(siftResponse.getBody().getProfileSummary().getIdentityFound()); + } + + @Test + public void testGlobalProfileLookupByPhone() throws Exception { + String accountId = "YOUR_ACCOUNT_ID"; + String responseBody = "{\"status\": 0, \"error_message\": \"OK\", \"error_code\": null, " + + "\"profile_summary\": {\"identity_found\": false}}"; + + MockWebServer server = new MockWebServer(); + MockResponse response = new MockResponse(); + response.setResponseCode(HTTP_OK); + response.setBody(responseBody); + server.enqueue(response); + server.start(); + + SiftClient client = new SiftClient("YOUR_API_KEY", accountId, + new OkHttpClient.Builder() + .addInterceptor(OkHttpUtils.urlRewritingInterceptor(server)) + .build()); + + GlobalProfileLookupRequest lookupRequest = client.buildRequest( + new GlobalProfileLookupFieldSet() + .setPhone("+15551234567")); + GlobalProfileLookupResponse siftResponse = lookupRequest.send(); + + RecordedRequest request = server.takeRequest(); + JSONAssert.assertEquals("{\"phone\": \"+15551234567\"}", + request.getBody().readUtf8(), false); + + Assert.assertEquals(HTTP_OK, siftResponse.getHttpStatusCode()); + Assert.assertTrue(siftResponse.isOk()); + Assert.assertFalse(siftResponse.getBody().getProfileSummary().getIdentityFound()); + } + + @Test + public void testLookupIdentityNotFound() throws Exception { + String accountId = "YOUR_ACCOUNT_ID"; + String responseBody = "{\n" + + " \"status\": 0,\n" + + " \"error_message\": \"OK\",\n" + + " \"error_code\": null,\n" + + " \"lookback_months\": null,\n" + + " \"profile_summary\": {\n" + + " \"identity_found\": false,\n" + + " \"has_links\": null,\n" + + " \"link_count\": null,\n" + + " \"linked_accounts_count_per_industry\": null\n" + + " },\n" + + " \"identity_age\": null,\n" + + " \"user_decisions\": null,\n" + + " \"chargebacks\": null,\n" + + " \"orders\": null,\n" + + " \"transactions\": null,\n" + + " \"locations\": null\n" + + "}"; + + MockWebServer server = new MockWebServer(); + MockResponse response = new MockResponse(); + response.setResponseCode(HTTP_OK); + response.setBody(responseBody); + server.enqueue(response); + server.start(); + + SiftClient client = new SiftClient("YOUR_API_KEY", accountId, + new OkHttpClient.Builder() + .addInterceptor(OkHttpUtils.urlRewritingInterceptor(server)) + .build()); + + GlobalProfileLookupRequest lookupRequest = client.buildRequest( + new GlobalProfileLookupFieldSet() + .setEmail("unknown@example.com")); + GlobalProfileLookupResponse siftResponse = lookupRequest.send(); + + Assert.assertEquals(HTTP_OK, siftResponse.getHttpStatusCode()); + Assert.assertTrue(siftResponse.isOk()); + Assert.assertFalse(siftResponse.getBody().getProfileSummary().getIdentityFound()); + Assert.assertNull(siftResponse.getBody().getLookbackMonths()); + Assert.assertNull(siftResponse.getBody().getIdentityAge()); + Assert.assertNull(siftResponse.getBody().getUserDecisions()); + Assert.assertNull(siftResponse.getBody().getChargebacks()); + Assert.assertNull(siftResponse.getBody().getOrders()); + Assert.assertNull(siftResponse.getBody().getTransactions()); + Assert.assertNull(siftResponse.getBody().getLocations()); + } + + @Test + public void testGlobalProfileLookupRequiresEmailOrPhone() { + SiftClient client = new SiftClient("YOUR_API_KEY", "YOUR_ACCOUNT_ID"); + + GlobalProfileLookupRequest lookupRequest = client.buildRequest( + new GlobalProfileLookupFieldSet()); + + try { + lookupRequest.send(); + Assert.fail("Expected a MissingFieldException to be thrown"); + } catch (MissingFieldException e) { + // expected + } catch (Exception e) { + Assert.fail("Expected a MissingFieldException, got " + e.getClass()); + } + } +} diff --git a/src/test/java/com/siftscience/GlobalProfileTest.java b/src/test/java/com/siftscience/GlobalProfileTest.java new file mode 100644 index 0000000..b7c72a9 --- /dev/null +++ b/src/test/java/com/siftscience/GlobalProfileTest.java @@ -0,0 +1,175 @@ +package com.siftscience; + +import com.siftscience.model.GlobalProfileFieldSet; +import okhttp3.OkHttpClient; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Assert; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import static java.net.HttpURLConnection.HTTP_OK; + +public class GlobalProfileTest { + + private static final String RESPONSE_BODY = "{\n" + + " \"status\": 0,\n" + + " \"error_message\": \"OK\",\n" + + " \"lookback_months\": 12,\n" + + " \"profile_summary\": {\n" + + " \"identity_found\": true,\n" + + " \"has_links\": true,\n" + + " \"link_count\": 7,\n" + + " \"linked_accounts_count_per_industry\": {\"finances\": 3, \"internet\": 4}\n" + + " },\n" + + " \"identity_age\": {\n" + + " \"oldest_account_age_timestamp\": 1681090536,\n" + + " \"newest_account_age_timestamp\": 1881090536,\n" + + " \"average_account_age_timestamp\": 1781090536\n" + + " },\n" + + " \"user_decisions\": {\n" + + " \"total\": 12, \"blocked\": 2, \"watched\": 3, \"accepted\": 6,\n" + + " \"manual\": 4, \"auto\": 8, \"last_type\": \"BLOCK\", \"last_timestamp\": 1881090536\n" + + " },\n" + + " \"chargebacks\": {\n" + + " \"total\": 3, \"fraudulent\": 2, \"other\": 1,\n" + + " \"last_timestamp\": 1881090536, \"last_fraudulent_timestamp\": 1881090536\n" + + " },\n" + + " \"orders\": {\n" + + " \"total\": 50, \"blocked\": 2, \"watched\": 5, \"accepted\": 40,\n" + + " \"last_timestamp\": 1881090536, \"last_blocked_timestamp\": 1881090536\n" + + " },\n" + + " \"transactions\": {\n" + + " \"total\": 120, \"failed_fraud\": 3, \"failed_other\": 5, \"successful\": 112,\n" + + " \"last_timestamp\": 1881090536, \"last_failed_fraud_timestamp\": 1881090536\n" + + " },\n" + + " \"locations\": {\n" + + " \"unique_billing_addresses\": 2, \"unique_shipping_addresses\": 4,\n" + + " \"distinct_countries_count\": 3, \"distinct_regions_count\": 5,\n" + + " \"location_connected_accounts\": [{\"city\": \"Kyiv\", \"country\": \"UA\"}],\n" + + " \"location_last_used_timestamp\": 1881090536\n" + + " }\n" + + "}"; + + @Test + public void testGetGlobalProfile() throws Exception { + String accountId = "YOUR_ACCOUNT_ID"; + + MockWebServer server = new MockWebServer(); + MockResponse response = new MockResponse(); + response.setResponseCode(HTTP_OK); + response.setBody(RESPONSE_BODY); + server.enqueue(response); + server.start(); + + // Create a new client and link it to the mock server. + SiftClient client = new SiftClient("YOUR_API_KEY", accountId, + new OkHttpClient.Builder() + .addInterceptor(OkHttpUtils.urlRewritingInterceptor(server)) + .build()); + + // Build and execute the request against the mock server. + GlobalProfileRequest getGlobalProfileRequest = client.buildRequest( + new GlobalProfileFieldSet() + .setUserId("some_user_id") + .setGlobalOnly(true) + .setIncludeOwnData(false)); + + GlobalProfileResponse siftResponse = getGlobalProfileRequest.send(); + + // Verify the request. + RecordedRequest request = server.takeRequest(); + Assert.assertEquals("GET", request.getMethod()); + Assert.assertEquals("/v3/accounts/" + accountId + + "/global_profile/users/some_user_id?global_only=true&include_own_data=false", + request.getPath()); + Assert.assertEquals(request.getHeader("Authorization"), "Basic WU9VUl9BUElfS0VZOg=="); + + // Verify the response was parsed correctly. + Assert.assertEquals(HTTP_OK, siftResponse.getHttpStatusCode()); + JSONAssert.assertEquals(response.getBody().readUtf8(), + siftResponse.getBody().toJson(), true); + + Assert.assertTrue(siftResponse.getBody().getProfileSummary().getIdentityFound()); + Assert.assertEquals(Long.valueOf(7), siftResponse.getBody().getProfileSummary().getLinkCount()); + Assert.assertEquals(Integer.valueOf(12), siftResponse.getBody().getLookbackMonths()); + Assert.assertEquals("UA", + siftResponse.getBody().getLocations().getLocationConnectedAccounts().get(0).getCountry()); + } + + @Test + public void testGetGlobalProfileWithoutOptionalParams() throws Exception { + String accountId = "YOUR_ACCOUNT_ID"; + + MockWebServer server = new MockWebServer(); + MockResponse response = new MockResponse(); + response.setResponseCode(HTTP_OK); + response.setBody(RESPONSE_BODY); + server.enqueue(response); + server.start(); + + SiftClient client = new SiftClient("YOUR_API_KEY", accountId, + new OkHttpClient.Builder() + .addInterceptor(OkHttpUtils.urlRewritingInterceptor(server)) + .build()); + + GlobalProfileRequest getGlobalProfileRequest = client.buildRequest( + new GlobalProfileFieldSet().setUserId("some_user_id")); + getGlobalProfileRequest.send(); + + RecordedRequest request = server.takeRequest(); + Assert.assertEquals("GET", request.getMethod()); + Assert.assertEquals("/v3/accounts/" + accountId + "/global_profile/users/some_user_id", + request.getPath()); + Assert.assertEquals(request.getHeader("Authorization"), "Basic WU9VUl9BUElfS0VZOg=="); + } + + @Test + public void testIdentityNotFound() throws Exception { + String accountId = "YOUR_ACCOUNT_ID"; + String responseBody = "{\n" + + " \"status\": 0,\n" + + " \"error_message\": \"OK\",\n" + + " \"error_code\": null,\n" + + " \"lookback_months\": null,\n" + + " \"profile_summary\": {\n" + + " \"identity_found\": false,\n" + + " \"has_links\": null,\n" + + " \"link_count\": null,\n" + + " \"linked_accounts_count_per_industry\": null\n" + + " },\n" + + " \"identity_age\": null,\n" + + " \"user_decisions\": null,\n" + + " \"chargebacks\": null,\n" + + " \"orders\": null,\n" + + " \"transactions\": null,\n" + + " \"locations\": null\n" + + "}"; + + MockWebServer server = new MockWebServer(); + MockResponse response = new MockResponse(); + response.setResponseCode(HTTP_OK); + response.setBody(responseBody); + server.enqueue(response); + server.start(); + + SiftClient client = new SiftClient("YOUR_API_KEY", accountId, + new OkHttpClient.Builder() + .addInterceptor(OkHttpUtils.urlRewritingInterceptor(server)) + .build()); + + GlobalProfileRequest getGlobalProfileRequest = client.buildRequest( + new GlobalProfileFieldSet().setUserId("unknown_user_id")); + GlobalProfileResponse siftResponse = getGlobalProfileRequest.send(); + + Assert.assertFalse(siftResponse.getBody().getProfileSummary().getIdentityFound()); + Assert.assertNull(siftResponse.getBody().getLookbackMonths()); + Assert.assertNull(siftResponse.getBody().getIdentityAge()); + Assert.assertNull(siftResponse.getBody().getUserDecisions()); + Assert.assertNull(siftResponse.getBody().getChargebacks()); + Assert.assertNull(siftResponse.getBody().getOrders()); + Assert.assertNull(siftResponse.getBody().getTransactions()); + Assert.assertNull(siftResponse.getBody().getLocations()); + } +}