From fcf51ec13cacf182782e1f4934f3f4116b97d8a7 Mon Sep 17 00:00:00 2001 From: Akhilesh Agarwal Date: Wed, 6 May 2026 04:38:09 -0400 Subject: [PATCH] fix: ESG query-string binding and EntityRisk CountryOfIncorporation casing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-repo audit of all SDK methods against the live QubitOn API surfaced two issues. Same drift surface as the SAP, Oracle, Go, and Node SDK PRs. 1. lookupESGScore (Esg.ESGRequest) country and domain are bound on the server as [FromQuery] on ESGController, not body. Java SDK was sending them in the JSON body where they were silently dropped, causing scoring to default to global / no-domain results. Fix: marked the country and domain fields with @JsonIgnore so Jackson omits them from the marshalled body, and added an esgPath helper that serialises them into the URL query string with URLEncoder + StandardCharsets.UTF_8. 2. EntityRiskRequest.countryOfIncorporation (Risk.java) The canonical API DTO pins this property as PascalCase via [JsonPropertyName("CountryOfIncorporation")] which overrides the global camelCase naming policy. The server is case-sensitive here, so the lowercase camelCase wire name was silently dropped on deserialisation -- the country was never associated with the request. Java SDK was tagging the field as @JsonProperty("countryOfIncorporation") (camelCase). Changed to @JsonProperty("CountryOfIncorporation") (PascalCase) and added a comment explaining the trap so a future "consistency cleanup" doesn't revert it. Public Java method signatures and getter/setter names are unchanged (setter is still setCountryOfIncorporation, builder method is still .countryOfIncorporation(...)). Only the wire JSON key changes. Audit findings outside this PR ------------------------------- Spot-checked the other drift candidates -- all clean: - TaxRequest: identityNumber/identityNumberType/country/entityName ✅ - TaxFormatRequest: identityNumber/identityNumberType/countryIso2 ✅ - BusinessRegistrationRequest: entityName ✅ Tests ----- mvn -B test passes (no failures). --- .../java/com/qubiton/sdk/QubitOnClient.java | 23 +++++++++++++++++-- src/main/java/com/qubiton/sdk/models/Esg.java | 16 +++++++++++-- .../java/com/qubiton/sdk/models/Risk.java | 6 ++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/qubiton/sdk/QubitOnClient.java b/src/main/java/com/qubiton/sdk/QubitOnClient.java index ed66738..539a0fa 100644 --- a/src/main/java/com/qubiton/sdk/QubitOnClient.java +++ b/src/main/java/com/qubiton/sdk/QubitOnClient.java @@ -1008,10 +1008,29 @@ private static String requireCountryIso2(Tax.TaxRequest r) { } @Override public Esg.ESGResponse lookupESGScore(Esg.ESGRequest r) { - return executeSync("POST", "/api/esg/Scores", r, Esg.ESGResponse.class); + return executeSync("POST", esgPath(r), r, Esg.ESGResponse.class); } @Override public CompletableFuture lookupESGScoreAsync(Esg.ESGRequest r) { - return executeAsync("POST", "/api/esg/Scores", r, Esg.ESGResponse.class); + return executeAsync("POST", esgPath(r), r, Esg.ESGResponse.class); + } + + /** + * Build the ESG endpoint path with country and domain as query-string + * parameters. Server binds them as {@code [FromQuery]} on ESGController, + * not body — they are {@code @JsonIgnore} on {@link Esg.ESGRequest} so + * Jackson naturally omits them from the marshalled body. + */ + private static String esgPath(Esg.ESGRequest r) { + if (r == null) return "/api/esg/Scores"; + StringBuilder qs = new StringBuilder(); + if (r.getCountry() != null && !r.getCountry().isEmpty()) { + qs.append("country=").append(java.net.URLEncoder.encode(r.getCountry(), java.nio.charset.StandardCharsets.UTF_8)); + } + if (r.getDomain() != null && !r.getDomain().isEmpty()) { + if (qs.length() > 0) qs.append('&'); + qs.append("domain=").append(java.net.URLEncoder.encode(r.getDomain(), java.nio.charset.StandardCharsets.UTF_8)); + } + return qs.length() == 0 ? "/api/esg/Scores" : "/api/esg/Scores?" + qs; } @Override public Security.DomainSecurityResponse domainSecurityReport(Security.DomainSecurityRequest r) { diff --git a/src/main/java/com/qubiton/sdk/models/Esg.java b/src/main/java/com/qubiton/sdk/models/Esg.java index 9f976d0..fadabc5 100644 --- a/src/main/java/com/qubiton/sdk/models/Esg.java +++ b/src/main/java/com/qubiton/sdk/models/Esg.java @@ -1,5 +1,6 @@ package com.qubiton.sdk.models; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -10,11 +11,22 @@ public final class Esg { private Esg() {} + /** + * Request shape for {@code lookupESGScore}. + * + *

The body sent on the wire contains only {@code companyName}. {@code country} and + * {@code domain} are bound on the server as {@code [FromQuery]} parameters on + * ESGController — the SDK serialises them into the URL query string and excludes + * them from the JSON body via {@code @JsonIgnore}. Sending them in the body would + * silently no-op. + */ @JsonInclude(JsonInclude.Include.NON_NULL) public static final class ESGRequest { @JsonProperty("companyName") private String companyName; - @JsonProperty("country") private String country; - @JsonProperty("domain") private String domain; + /** Sent as URL query parameter {@code ?country=…}, NOT body. */ + @JsonIgnore private String country; + /** Sent as URL query parameter {@code &domain=…}, NOT body. */ + @JsonIgnore private String domain; public String getCompanyName() { return companyName; } public ESGRequest setCompanyName(String v) { this.companyName = v; return this; } diff --git a/src/main/java/com/qubiton/sdk/models/Risk.java b/src/main/java/com/qubiton/sdk/models/Risk.java index a669d24..5167944 100644 --- a/src/main/java/com/qubiton/sdk/models/Risk.java +++ b/src/main/java/com/qubiton/sdk/models/Risk.java @@ -344,7 +344,11 @@ public static final class FailRateResponse { @JsonInclude(JsonInclude.Include.NON_NULL) public static final class EntityRiskRequest { @JsonProperty("companyName") private String companyName; - @JsonProperty("countryOfIncorporation") private String countryOfIncorporation; + // Wire field is "CountryOfIncorporation" (PascalCase) — pinned by + // [JsonPropertyName("CountryOfIncorporation")] on the canonical API DTO, + // which overrides the global camelCase naming policy. The server is + // case-sensitive here, so the lowercase alias is silently dropped. + @JsonProperty("CountryOfIncorporation") private String countryOfIncorporation; @JsonProperty("category") private String category; @JsonProperty("url") private String url; @JsonProperty("businessEntityType") private String businessEntityType;