1
0

add i18n support for CoopShareTx (#168)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/168
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-04-01 12:18:36 +02:00
parent 413ca0917e
commit 4f00d1b920
6 changed files with 64 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
package net.hostsharing.hsadminng.hs.hosting.asset; package net.hostsharing.hsadminng.hs.office.coopassets;
import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.config.RetroactiveTranslator; import net.hostsharing.hsadminng.config.RetroactiveTranslator;
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
// HOWTO translate messages which got created without i18n support, in this case in a PostgreSQL constraint trigger // HOWTO translate messages which got created without i18n support, in this case in a PostgreSQL constraint trigger
@Service @Service
public class HsHostingAssetTranslations implements RetroactiveTranslator { public class HsCoopAssetTranslations implements RetroactiveTranslator {
public static final String ERROR_400_PREFIX = "ERROR: [400] "; public static final String ERROR_400_PREFIX = "ERROR: [400] ";

View File

@@ -0,0 +1,26 @@
package net.hostsharing.hsadminng.hs.office.coopshares;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.config.RetroactiveTranslator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
// HOWTO translate messages which got created without i18n support, in this case in a PostgreSQL constraint trigger
@Service
public class HsCoopShareTranslations implements RetroactiveTranslator {
public static final String ERROR_400_PREFIX = "ERROR: [400] ";
@Autowired
private MessageTranslator messageTranslator;
@Override
public boolean canTranslate(final String message) {
return message.equals("ERROR: [400] coop shares transaction would result in a negative number of shares");
}
@Override
public String translate(final String message) {
return ERROR_400_PREFIX + messageTranslator.translate(message.substring(ERROR_400_PREFIX.length()));
}
}

View File

@@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
@@ -37,6 +38,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Autowired @Autowired
private StrictMapper mapper; private StrictMapper mapper;
@Autowired
private MessageTranslator messageTranslator;
@Autowired @Autowired
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
@@ -118,32 +122,31 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
MultiValidationException.throwIfNotEmpty(violations); MultiValidationException.throwIfNotEmpty(violations);
} }
private static void validateSubscriptionTransaction( private void validateSubscriptionTransaction(
final HsOfficeCoopSharesTransactionInsertResource requestBody, final HsOfficeCoopSharesTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getTransactionType() == SUBSCRIPTION if (requestBody.getTransactionType() == SUBSCRIPTION
&& requestBody.getShareCount() < 0) { && requestBody.getShareCount() < 0) {
violations.add("for %s, shareCount must be positive but is \"%d\"".formatted( violations.add(messageTranslator.translate("for transactionType={0}, shareCount must be positive but is {1}",
requestBody.getTransactionType(), requestBody.getShareCount())); requestBody.getTransactionType(), requestBody.getShareCount()));
} }
} }
private static void validateCancellationTransaction( private void validateCancellationTransaction(
final HsOfficeCoopSharesTransactionInsertResource requestBody, final HsOfficeCoopSharesTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getTransactionType() == CANCELLATION if (requestBody.getTransactionType() == CANCELLATION
&& requestBody.getShareCount() > 0) { && requestBody.getShareCount() > 0) {
violations.add("for %s, shareCount must be negative but is \"%d\"".formatted( violations.add(messageTranslator.translate("for transactionType={0}, shareCount must be negative but is {1}",
requestBody.getTransactionType(), requestBody.getShareCount())); requestBody.getTransactionType(), requestBody.getShareCount()));
} }
} }
private static void validateshareCount( private void validateshareCount(
final HsOfficeCoopSharesTransactionInsertResource requestBody, final HsOfficeCoopSharesTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getShareCount() == 0) { if (requestBody.getShareCount() == 0) {
violations.add("shareCount must not be 0 but is \"%d\"".formatted( violations.add(messageTranslator.translate("shareCount must not be 0"));
requestBody.getShareCount()));
} }
} }

View File

@@ -12,6 +12,12 @@ unknown\ authorization\ ticket=unbekanntes Autorisierungs-Ticket
{0}\ "{1}"\ not\ found\ or\ not\ accessible={0} "{1}" nicht gefunden oder nicht zugänglich {0}\ "{1}"\ not\ found\ or\ not\ accessible={0} "{1}" nicht gefunden oder nicht zugänglich
but\ is=ist aber but\ is=ist aber
# office.coop-shares
for\ transactionType\={0},\ shareCount\ must\ be\ positive\ but\ is\ {1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1}
for\ transactionType\={0},\ shareCount\ must\ be\ negative\ but\ is\ {1}=für transactionType={0}, muss shareCount negativ sein, ist aber {1}
shareCount\ must\ not\ be\ 0=shareCount darf nicht 0 sein
coop\ shares\ transaction\ would\ result\ in\ a\ negative\ number\ of\ shares=Geschäftsanteile-Transaktion würde zu negativen Geschäftsanteilen führen
# office.coop-assets # office.coop-assets
either\ {0}\ or\ {1}\ must\ be\ given=entweder {0} oder {1} muss angegeben werden either\ {0}\ or\ {1}\ must\ be\ given=entweder {0} oder {1} muss angegeben werden
either\ {0}\ or\ {1}\ must\ be\ given,\ not\ both=entweder {0} oder {1} muss angegeben werden, nicht beide either\ {0}\ or\ {1}\ must\ be\ given,\ not\ both=entweder {0} oder {1} muss angegeben werden, nicht beide

View File

@@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
@@ -32,7 +33,7 @@ import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = {HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class}) classes = {HsadminNgApplication.class, DisableSecurityConfig.class, MessageTranslator.class, JpaAttempt.class})
@ActiveProfiles("test") @ActiveProfiles("test")
@Transactional @Transactional
@Tag("officeIntegrationTest") @Tag("officeIntegrationTest")
@@ -180,7 +181,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
} }
@Nested @Nested
class AddCoopSharesTransaction { class PostNewCoopSharesTransaction {
@Test @Test
void globalAdmin_canAddCoopSharesTransaction() { void globalAdmin_canAddCoopSharesTransaction() {
@@ -191,6 +192,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
final var location = RestAssured // @formatter:off final var location = RestAssured // @formatter:off
.given() .given()
.header("Authorization", "Bearer superuser-alex@hostsharing.net") .header("Authorization", "Bearer superuser-alex@hostsharing.net")
.header("Accept-Language", "de")
.contentType(ContentType.JSON).body(""" .contentType(ContentType.JSON).body("""
{ {
"membership.uuid": "%s", "membership.uuid": "%s",
@@ -306,6 +308,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
.header("Authorization", "Bearer superuser-alex@hostsharing.net") .header("Authorization", "Bearer superuser-alex@hostsharing.net")
.header("Accept-Language", "de")
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body(""" .body("""
{ {
@@ -329,7 +332,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
{ {
"statusCode": 400, "statusCode": 400,
"statusPhrase": "Bad Request", "statusPhrase": "Bad Request",
"message": "ERROR: [400] coop shares transaction would result in a negative number of shares" "message": "ERROR: [400] Geschäftsanteile-Transaktion würde zu negativen Geschäftsanteilen führen"
} }
""")); // @formatter:on """)); // @formatter:on
} }

View File

@@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.coopshares; package net.hostsharing.hsadminng.hs.office.coopshares;
import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.config.MessagesResourceConfig;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
@@ -27,7 +28,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficeCoopSharesTransactionController.class) @WebMvcTest(HsOfficeCoopSharesTransactionController.class)
@Import({DisableSecurityConfig.class, MessageTranslator.class}) @Import({DisableSecurityConfig.class,
MessagesResourceConfig.class,
MessageTranslator.class})
@ActiveProfiles("test") @ActiveProfiles("test")
class HsOfficeCoopSharesTransactionControllerRestTest { class HsOfficeCoopSharesTransactionControllerRestTest {
@@ -61,45 +64,45 @@ class HsOfficeCoopSharesTransactionControllerRestTest {
enum BadRequestTestCases { enum BadRequestTestCases {
MEMBERSHIP_UUID_MISSING( MEMBERSHIP_UUID_MISSING(
requestBody -> requestBody.without("membership.uuid"), requestBody -> requestBody.without("membership.uuid"),
"membershipUuid must not be null"), "membershipUuid darf nicht null sein ist aber null"),
TRANSACTION_TYPE_MISSING( TRANSACTION_TYPE_MISSING(
requestBody -> requestBody.without("transactionType"), requestBody -> requestBody.without("transactionType"),
"transactionType must not be null"), "transactionType darf nicht null sein ist aber null"),
VALUE_DATE_MISSING( VALUE_DATE_MISSING(
requestBody -> requestBody.without("valueDate"), requestBody -> requestBody.without("valueDate"),
"valueDate must not be null"), "valueDate darf nicht null sein ist aber null"),
SHARES_COUNT_FOR_SUBSCRIPTION_MUST_BE_POSITIVE( SHARES_COUNT_FOR_SUBSCRIPTION_MUST_BE_POSITIVE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "SUBSCRIPTION") .with("transactionType", "SUBSCRIPTION")
.with("shareCount", -1), .with("shareCount", -1),
"for SUBSCRIPTION, shareCount must be positive but is \"-1\""), "für transactionType=SUBSCRIPTION, muss shareCount positiv sein, ist aber -1"),
SHARES_COUNT_FOR_CANCELLATION_MUST_BE_NEGATIVE( SHARES_COUNT_FOR_CANCELLATION_MUST_BE_NEGATIVE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "CANCELLATION") .with("transactionType", "CANCELLATION")
.with("shareCount", 1), .with("shareCount", 1),
"for CANCELLATION, shareCount must be negative but is \"1\""), "für transactionType=CANCELLATION, muss shareCount negativ sein, ist aber 1"),
SHARES_COUNT_MUST_NOT_BE_NULL( SHARES_COUNT_MUST_NOT_BE_NULL(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "REVERSAL") .with("transactionType", "REVERSAL")
.with("shareCount", 0), .with("shareCount", 0),
"shareCount must not be 0 but is \"0\""), "shareCount darf nicht 0 sein"),
REFERENCE_MISSING( REFERENCE_MISSING(
requestBody -> requestBody.without("reference"), requestBody -> requestBody.without("reference"),
"reference must not be null"), "reference darf nicht null sein ist aber null"),
REFERENCE_TOO_SHORT( REFERENCE_TOO_SHORT(
requestBody -> requestBody.with("reference", "12345"), requestBody -> requestBody.with("reference", "12345"),
"reference size must be between 6 and 48 but is \"12345\""), "reference Größe muss zwischen 6 und 48 sein ist aber \"12345\""),
REFERENCE_TOO_LONG( REFERENCE_TOO_LONG(
requestBody -> requestBody.with("reference", "0123456789012345678901234567890123456789012345678"), requestBody -> requestBody.with("reference", "0123456789012345678901234567890123456789012345678"),
"reference size must be between 6 and 48 but is \"0123456789012345678901234567890123456789012345678\""); "reference Größe muss zwischen 6 und 48 sein ist aber \"0123456789012345678901234567890123456789012345678\"");
private final Function<JsonBuilder, JsonBuilder> givenBodyTransformation; private final Function<JsonBuilder, JsonBuilder> givenBodyTransformation;
private final String expectedErrorMessage; private final String expectedErrorMessage;
@@ -124,6 +127,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest {
mockMvc.perform(MockMvcRequestBuilders mockMvc.perform(MockMvcRequestBuilders
.post("/api/hs/office/coopsharestransactions") .post("/api/hs/office/coopsharestransactions")
.header("Authorization", "Bearer superuser-alex@hostsharing.net") .header("Authorization", "Bearer superuser-alex@hostsharing.net")
.header("Accept-Language", "de")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(testCase.givenRequestBody()) .content(testCase.givenRequestBody())
.accept(MediaType.APPLICATION_JSON)) .accept(MediaType.APPLICATION_JSON))