From 1306a528da1b7183c87b874efb15bc2933942008 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Tue, 27 Oct 2026 20:49:22 -0700 Subject: [PATCH 1/2] fix(reports): support arbitrary matchType ranges in net rate schemes The API documents `matchType` in netRateSchemes as a free string (`perfect`, `100` or any percentage range such as `100-82`), but Match only had the closed MatchType enum, so ranges without a constant were read as null and could not be sent. Match now stores the raw value in `matchTypeValue` (JSON key `matchType`). The existing getMatchType()/setMatchType(MatchType) keep their signatures and work on top of it; getMatchType() returns null only for values without a predefined constant, while the original value stays available. No overload was added to setMatchType, so existing calls stay unambiguous. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../crowdin/client/reports/model/Match.java | 29 ++++++- .../client/reports/ReportsApiTest.java | 54 +++++++++++++ .../client/reports/model/MatchTest.java | 79 +++++++++++++++++++ ...ingReportSettingsTemplateCustomRanges.json | 57 +++++++++++++ ...ingReportSettingsTemplateCustomRanges.json | 63 +++++++++++++++ 5 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/crowdin/client/reports/model/MatchTest.java create mode 100644 src/test/resources/api/reports/addPostEditingReportSettingsTemplateCustomRanges.json create mode 100644 src/test/resources/api/reports/postEditingReportSettingsTemplateCustomRanges.json diff --git a/src/main/java/com/crowdin/client/reports/model/Match.java b/src/main/java/com/crowdin/client/reports/model/Match.java index a19c06111..f6438a79d 100644 --- a/src/main/java/com/crowdin/client/reports/model/Match.java +++ b/src/main/java/com/crowdin/client/reports/model/Match.java @@ -1,9 +1,36 @@ package com.crowdin.client.reports.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @Data public class Match { - private MatchType matchType; + + /** + * Match type as sent to and returned by the API (JSON key {@code matchType}): {@code perfect}, {@code 100} + * or any percentage range, e.g. {@code 99-82}, {@code 100-74}. + */ + @JsonProperty("matchType") + private String matchTypeValue; private Float price; + + /** + * Predefined view of {@link #matchTypeValue}. + * + * @return the matching {@link MatchType} constant, or {@code null} if the value is not set or is a range that has + * no predefined constant (e.g. {@code 100-74}); the original value is always available in {@link #matchTypeValue} + */ + public MatchType getMatchType() { + return this.matchTypeValue == null ? null : MatchType.from(this.matchTypeValue); + } + + /** + * Sets {@link #matchTypeValue} from a predefined constant. Use {@code setMatchTypeValue(String)} for ranges that + * have no constant (e.g. {@code 100-82}). + * + * @param matchType predefined match type + */ + public void setMatchType(MatchType matchType) { + this.matchTypeValue = matchType == null ? null : (String) matchType.to(matchType); + } } diff --git a/src/test/java/com/crowdin/client/reports/ReportsApiTest.java b/src/test/java/com/crowdin/client/reports/ReportsApiTest.java index 955acb3fc..553fe57c9 100644 --- a/src/test/java/com/crowdin/client/reports/ReportsApiTest.java +++ b/src/test/java/com/crowdin/client/reports/ReportsApiTest.java @@ -25,6 +25,7 @@ import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -76,6 +77,7 @@ public List getMocks() { RequestMock.build(this.url + "/projects/" + projectId + "/reports", HttpPost.METHOD_NAME, "api/reports/generateTranslationActivityReport.json", "api/reports/reportGenerationStatus.json"), RequestMock.build(this.url + "/projects/" + projectId + "/reports/settings-templates", HttpPost.METHOD_NAME, "api/reports/addHourlyReportSettingsTemplate.json", "api/reports/hourlyReportSettingsTemplate.json"), RequestMock.build(this.url + "/projects/" + projectId + "/reports/settings-templates", HttpPost.METHOD_NAME, "api/reports/addPostEditingReportSettingsTemplate.json", "api/reports/postEditingReportSettingsTemplate.json"), + RequestMock.build(this.url + "/projects/" + projectId + "/reports/settings-templates", HttpPost.METHOD_NAME, "api/reports/addPostEditingReportSettingsTemplateCustomRanges.json", "api/reports/postEditingReportSettingsTemplateCustomRanges.json"), RequestMock.build(this.url + "/projects/" + projectId + "/reports/" + id, HttpGet.METHOD_NAME, "api/reports/reportGenerationStatus.json"), RequestMock.build(this.url + "/projects/" + projectId + "/reports/" + id + "/download", HttpGet.METHOD_NAME, "api/reports/downloadLink.json"), RequestMock.build(this.url + "/reports/settings-templates", HttpGet.METHOD_NAME, "api/reports/listOrganizationReportSettingsTemplate.json"), @@ -718,6 +720,58 @@ public void addPostEditingReportSettingsTemplateTest() { assertFalse(responseConfig.getUseTmEditDistance()); } + @Test + public void addPostEditingReportSettingsTemplateWithCustomMatchTypeRangesTest() { + BaseRatesForm baseRates = new BaseRatesForm(); + baseRates.setFullTranslation(fullTranslationRate); + baseRates.setProofread(proofreadRate); + IndividualRate individualRate = new IndividualRate(); + individualRate.setLanguageIds(singletonList(engLanguageId)); + individualRate.setUserIds(singletonList(20L)); + individualRate.setFullTranslation(fullTranslationRate); + individualRate.setProofread(proofreadRate); + Match perfect = new Match(); + perfect.setMatchType(MatchType.PERFECT); + perfect.setPrice(0.1f); + NetRateSchemes netRateSchemes = new NetRateSchemes(); + netRateSchemes.setTmMatch(Arrays.asList(perfect, match("100-82", 0.08f))); + netRateSchemes.setMtMatch(singletonList(match("100-74", 0.05f))); + netRateSchemes.setAiMatch(singletonList(match("100-74", 0.05f))); + netRateSchemes.setSuggestionMatch(singletonList(match("99-60", 0.07f))); + ReportSettingsTemplate.Config config = new ReportSettingsTemplate.Config(); + config.setBaseRates(baseRates); + config.setIndividualRates(singletonList(individualRate)); + config.setNetRateSchemes(netRateSchemes); + config.setCalculateInternalMatches(true); + config.setIncludePreTranslatedStrings(false); + config.setUseCategoryBasedProofreadRates(true); + config.setUseTmEditDistance(false); + + ReportSettingsTemplate request = new ReportSettingsTemplate(); + request.setName("post-editing template with ranges"); + request.setCurrency(Currency.DDK); + request.setUnit(Unit.WORDS); + request.setConfig(config); + + ReportSettingsTemplate response = this.getReportsApi().addReportSettingsTemplate(projectId, request).getData(); + NetRateSchemes responseSchemes = response.getConfig().getNetRateSchemes(); + assertEquals(netRateSchemes, responseSchemes); + assertEquals(MatchType.PERFECT, responseSchemes.getTmMatch().get(0).getMatchType()); + assertEquals("perfect", responseSchemes.getTmMatch().get(0).getMatchTypeValue()); + assertEquals("100-82", responseSchemes.getTmMatch().get(1).getMatchTypeValue()); + assertNull(responseSchemes.getTmMatch().get(1).getMatchType()); + assertEquals("100-74", responseSchemes.getMtMatch().get(0).getMatchTypeValue()); + assertEquals("100-74", responseSchemes.getAiMatch().get(0).getMatchTypeValue()); + assertEquals("99-60", responseSchemes.getSuggestionMatch().get(0).getMatchTypeValue()); + } + + private static Match match(String matchType, float price) { + Match match = new Match(); + match.setMatchTypeValue(matchType); + match.setPrice(price); + return match; + } + @Test public void legacyGroupTaskUsageTaskTypeIsSerializedAsTypeTasks() { GroupTaskUsageGenerateReportRequest.Schema schema = new GroupTaskUsageGenerateReportRequest.Schema(); diff --git a/src/test/java/com/crowdin/client/reports/model/MatchTest.java b/src/test/java/com/crowdin/client/reports/model/MatchTest.java new file mode 100644 index 000000000..a74dca3a1 --- /dev/null +++ b/src/test/java/com/crowdin/client/reports/model/MatchTest.java @@ -0,0 +1,79 @@ +package com.crowdin.client.reports.model; + +import com.crowdin.client.core.http.impl.json.JacksonJsonTransformer; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class MatchTest { + + private final JacksonJsonTransformer jsonTransformer = new JacksonJsonTransformer(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void knownMatchTypesRoundTripThroughEnumAccessorsTest() { + for (MatchType matchType : MatchType.values()) { + Match match = new Match(); + match.setMatchType(matchType); + + assertEquals(matchType, match.getMatchType()); + assertEquals(matchType.to(matchType), match.getMatchTypeValue()); + assertEquals("{\"matchType\":\"" + matchType.to(matchType) + "\"}", jsonTransformer.convert(match)); + + Match parsed = jsonTransformer.parse(jsonTransformer.convert(match), Match.class); + assertEquals(matchType, parsed.getMatchType()); + assertEquals(match, parsed); + } + } + + @Test + void knownMatchTypesDeserializeIntoEnumTest() { + Match match = jsonTransformer.parse("{\"matchType\":\"99-82\",\"price\":1.5}", Match.class); + + assertEquals(MatchType.OPTION_99_82, match.getMatchType()); + assertEquals("99-82", match.getMatchTypeValue()); + assertEquals(Float.valueOf(1.5f), match.getPrice()); + } + + @Test + void customRangesAreSerializedTest() { + Match match = new Match(); + match.setMatchTypeValue("100-82"); + match.setPrice(0.5f); + + assertJsonEquals("{\"matchType\":\"100-82\",\"price\":0.5}", jsonTransformer.convert(match)); + assertNull(match.getMatchType()); + } + + @Test + void customRangesAreDeserializedWithoutDataLossTest() { + for (String range : new String[]{"100-74", "100-82", "73-50"}) { + Match match = jsonTransformer.parse("{\"matchType\":\"" + range + "\",\"price\":0.25}", Match.class); + + assertEquals(range, match.getMatchTypeValue()); + assertNull(match.getMatchType(), "no predefined constant for " + range); + assertJsonEquals("{\"matchType\":\"" + range + "\",\"price\":0.25}", jsonTransformer.convert(match)); + } + } + + @Test + void enumSetterReplacesCustomValueAndNullClearsItTest() { + Match match = new Match(); + match.setMatchTypeValue("100-74"); + match.setMatchType(MatchType.OPTION_100); + assertEquals("100", match.getMatchTypeValue()); + + match.setMatchType(null); + assertNull(match.getMatchTypeValue()); + assertNull(match.getMatchType()); + assertEquals("{}", jsonTransformer.convert(match)); + } + + @SneakyThrows + private void assertJsonEquals(String expected, String actual) { + assertEquals(objectMapper.readTree(expected), objectMapper.readTree(actual), actual); + } +} diff --git a/src/test/resources/api/reports/addPostEditingReportSettingsTemplateCustomRanges.json b/src/test/resources/api/reports/addPostEditingReportSettingsTemplateCustomRanges.json new file mode 100644 index 000000000..6170f7c6d --- /dev/null +++ b/src/test/resources/api/reports/addPostEditingReportSettingsTemplateCustomRanges.json @@ -0,0 +1,57 @@ +{ + "name": "post-editing template with ranges", + "currency": "DDK", + "unit": "words", + "config": { + "baseRates": { + "fullTranslation": 0.1, + "proofread": 0.12 + }, + "individualRates": [ + { + "languageIds": [ + "uk" + ], + "userIds": [ + 20 + ], + "fullTranslation": 0.1, + "proofread": 0.12 + } + ], + "netRateSchemes": { + "tmMatch": [ + { + "matchType": "perfect", + "price": 0.1 + }, + { + "matchType": "100-82", + "price": 0.08 + } + ], + "mtMatch": [ + { + "matchType": "100-74", + "price": 0.05 + } + ], + "aiMatch": [ + { + "matchType": "100-74", + "price": 0.05 + } + ], + "suggestionMatch": [ + { + "matchType": "99-60", + "price": 0.07 + } + ] + }, + "calculateInternalMatches": true, + "includePreTranslatedStrings": false, + "useCategoryBasedProofreadRates": true, + "useTmEditDistance": false + } +} diff --git a/src/test/resources/api/reports/postEditingReportSettingsTemplateCustomRanges.json b/src/test/resources/api/reports/postEditingReportSettingsTemplateCustomRanges.json new file mode 100644 index 000000000..cbc3759a6 --- /dev/null +++ b/src/test/resources/api/reports/postEditingReportSettingsTemplateCustomRanges.json @@ -0,0 +1,63 @@ +{ + "data": { + "id": 4, + "name": "post-editing template with ranges", + "currency": "DDK", + "unit": "words", + "config": { + "baseRates": { + "fullTranslation": 0.1, + "proofread": 0.12 + }, + "individualRates": [ + { + "languageIds": [ + "uk" + ], + "userIds": [ + 20 + ], + "fullTranslation": 0.1, + "proofread": 0.12 + } + ], + "netRateSchemes": { + "tmMatch": [ + { + "matchType": "perfect", + "price": 0.1 + }, + { + "matchType": "100-82", + "price": 0.08 + } + ], + "mtMatch": [ + { + "matchType": "100-74", + "price": 0.05 + } + ], + "aiMatch": [ + { + "matchType": "100-74", + "price": 0.05 + } + ], + "suggestionMatch": [ + { + "matchType": "99-60", + "price": 0.07 + } + ] + }, + "calculateInternalMatches": true, + "includePreTranslatedStrings": false, + "useCategoryBasedProofreadRates": true, + "useTmEditDistance": false + }, + "createdAt": "2023-09-23T11:26:54+00:00", + "updatedAt": "2023-09-23T11:26:54+00:00", + "isPublic": false + } +} From e3404025be562a48ef3de2c6d1d0543a8e657362 Mon Sep 17 00:00:00 2001 From: Kateryna Oblakevych Date: Tue, 27 Oct 2026 20:49:22 -0700 Subject: [PATCH 2/2] fix(reports): drop unreleased deprecated report fields Remove fields that were introduced only in #423 and are marked deprecated in the official API: - postEditingCategories from GroupPreTranslateAccuracySchema and GroupTranslatorAccuracySchema (use matchScoreCategories) - excludeApprovalsForEditedTranslations and preTranslatedStringsCategorizationAdjustment from GroupTranslationCostsPostEditingByTaskSchema and ReportSettingsTemplate.Config They were never part of a release. Pre-existing deprecated fields stay in place with @Deprecated. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../model/GenerateGroupReportRequest.java | 20 ------------------- .../reports/model/ReportSettingsTemplate.java | 10 ---------- .../postEditingReportSettingsTemplate.json | 2 -- 3 files changed, 32 deletions(-) diff --git a/src/main/java/com/crowdin/client/reports/model/GenerateGroupReportRequest.java b/src/main/java/com/crowdin/client/reports/model/GenerateGroupReportRequest.java index 7ce425a28..49f5db185 100644 --- a/src/main/java/com/crowdin/client/reports/model/GenerateGroupReportRequest.java +++ b/src/main/java/com/crowdin/client/reports/model/GenerateGroupReportRequest.java @@ -73,16 +73,6 @@ public static class GroupTranslationCostsPostEditingByTaskSchema extends Schema private List taskIds; private Boolean useCategoryBasedProofreadRates; private Boolean useTmEditDistance; - /** - * @deprecated deprecated in the API - */ - @Deprecated - private Boolean excludeApprovalsForEditedTranslations; - /** - * @deprecated deprecated in the API - */ - @Deprecated - private Boolean preTranslatedStringsCategorizationAdjustment; private Boolean skipArchiving; } @@ -251,11 +241,6 @@ public static class GroupPreTranslateAccuracySchema extends Schema { private ReportsFormat format; private Date dateFrom; private Date dateTo; - /** - * @deprecated deprecated in the API, use {@link #matchScoreCategories} instead - */ - @Deprecated - private List postEditingCategories; /** * Split into categories by match score. Ranges should be in descending order (e.g. 100-90, 89-80) */ @@ -284,11 +269,6 @@ public static class GroupTranslatorAccuracySchema extends Schema { private ReportsFormat format; private Date dateFrom; private Date dateTo; - /** - * @deprecated deprecated in the API, use {@link #matchScoreCategories} instead - */ - @Deprecated - private List postEditingCategories; /** * Split into categories by match score. Ranges should be in descending order (e.g. 100-90, 89-80) */ diff --git a/src/main/java/com/crowdin/client/reports/model/ReportSettingsTemplate.java b/src/main/java/com/crowdin/client/reports/model/ReportSettingsTemplate.java index 1f04dc1a4..569732ebc 100644 --- a/src/main/java/com/crowdin/client/reports/model/ReportSettingsTemplate.java +++ b/src/main/java/com/crowdin/client/reports/model/ReportSettingsTemplate.java @@ -59,16 +59,6 @@ public static class Config { private Boolean includePreTranslatedStrings; private Boolean useCategoryBasedProofreadRates; private Boolean useTmEditDistance; - /** - * @deprecated deprecated in the API - */ - @Deprecated - private Boolean excludeApprovalsForEditedTranslations; - /** - * @deprecated deprecated in the API - */ - @Deprecated - private Boolean preTranslatedStringsCategorizationAdjustment; } /** diff --git a/src/test/resources/api/reports/postEditingReportSettingsTemplate.json b/src/test/resources/api/reports/postEditingReportSettingsTemplate.json index 41edd2eaa..1fb011495 100644 --- a/src/test/resources/api/reports/postEditingReportSettingsTemplate.json +++ b/src/test/resources/api/reports/postEditingReportSettingsTemplate.json @@ -49,8 +49,6 @@ }, "calculateInternalMatches": true, "includePreTranslatedStrings": false, - "excludeApprovalsForEditedTranslations": false, - "preTranslatedStringsCategorizationAdjustment": false, "useCategoryBasedProofreadRates": true, "useTmEditDistance": false },