Skip to content

Commit

Permalink
Enable account locale resolving and add account preference
Browse files Browse the repository at this point in the history
  • Loading branch information
semotpan committed Aug 4, 2024
1 parent ac89bec commit 74ffd1e
Show file tree
Hide file tree
Showing 20 changed files with 344 additions and 100 deletions.
10 changes: 9 additions & 1 deletion server/src/main/java/io/myfinbox/account/AccountCreated.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import io.myfinbox.shared.DomainEvent;
import lombok.Builder;

import java.time.ZoneId;
import java.util.Currency;
import java.util.UUID;

import static io.myfinbox.shared.Guards.notBlank;
Expand All @@ -18,7 +20,9 @@
public record AccountCreated(UUID accountId,
String emailAddress,
String firstName,
String lastName) implements DomainEvent {
String lastName,
Currency currency,
ZoneId zoneId) implements DomainEvent {

/**
* Constructor for the AccountCreated record.
Expand All @@ -27,9 +31,13 @@ public record AccountCreated(UUID accountId,
* @param emailAddress The email address associated with the account.
* @param firstName The first name of the account holder.
* @param lastName The last name of the account holder.
* @param currency The currency of the account holder.
* @param zoneId The zoneId of the account holder.
*/
public AccountCreated {
notNull(accountId, "accountIdentifier cannot be null");
notBlank(emailAddress, "emailAddress cannot be blank");
notNull(currency, "currency cannot be null");
notNull(zoneId, "zoneId cannot be null");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import io.myfinbox.shared.ApiFailureHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.Currency;
import java.util.Locale;

import static java.util.Objects.isNull;
import static org.springframework.http.HttpHeaders.ACCEPT_LANGUAGE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.ResponseEntity.created;
import static org.springframework.web.servlet.support.ServletUriComponentsBuilder.fromCurrentRequest;
Expand All @@ -20,19 +22,39 @@
@RequiredArgsConstructor
final class AccountController implements AccountsApi {

static final Locale defaultLocale = Locale.of("en", "MD");

private final CreateAccountUseCase createAccountUseCase;
private final ApiFailureHandler apiFailureHandler;

@PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> create(@RequestBody AccountCreateResource resource) {
public ResponseEntity<?> create(@RequestHeader(name = ACCEPT_LANGUAGE, required = false) Locale locale,
@RequestBody AccountCreateResource resource) {
var command = CreateAccountCommand.builder()
.firstName(resource.getFirstName())
.lastName(resource.getLastName())
.emailAddress(resource.getEmailAddress())
.currency(Currency.getInstance(resolve(locale)).getCurrencyCode())
.zoneId(resource.getZoneId())
.build();

return createAccountUseCase.create(command).fold(apiFailureHandler::handle,
account -> created(fromCurrentRequest().path("/{id}").build(account.getId().toString()))
.body(resource.accountId(account.getId().id())));
.body(resource.accountId(account.getId().id())
.currency(command.currency())
.zoneId(resource.getZoneId())
));
}

private Locale resolve(Locale locale) {
try {
if (isNull(Currency.getInstance(locale).getCurrencyCode())) {
return defaultLocale;
}
} catch (NullPointerException | IllegalArgumentException e) {
return defaultLocale;
}

return locale;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.myfinbox.rest.AccountCreateResource;
import io.myfinbox.shared.ApiErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
Expand All @@ -13,7 +14,9 @@
import org.springframework.http.ResponseEntity;

import java.net.URI;
import java.util.Locale;

import static io.swagger.v3.oas.annotations.enums.ParameterIn.HEADER;
import static org.springframework.http.HttpHeaders.LOCATION;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

Expand All @@ -37,6 +40,7 @@ public interface AccountsApi {
@ApiResponse(responseCode = "500", description = "Internal Server Error",
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class)))
})
ResponseEntity<?> create(@RequestBody(description = "AccountResource to be created", required = true) AccountCreateResource resource);
ResponseEntity<?> create(@Parameter(in = HEADER) Locale locale,
@RequestBody(description = "AccountResource to be created", required = true) AccountCreateResource resource);

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.myfinbox.account.application;

import io.myfinbox.account.domain.Account;
import io.myfinbox.account.domain.Accounts;
import io.myfinbox.account.domain.*;
import io.myfinbox.shared.Failure;
import io.myfinbox.shared.Failure.FieldViolation;
import io.vavr.collection.Seq;
Expand All @@ -13,6 +12,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.DateTimeException;
import java.time.ZoneId;
import java.util.Currency;
import java.util.regex.Pattern;

import static io.myfinbox.account.application.CreateAccountUseCase.CreateAccountCommand.*;
Expand All @@ -37,14 +39,14 @@ public Either<Failure, Account> create(CreateAccountCommand cmd) {
return Either.left(Failure.ofValidation(ERROR_MESSAGE, validation.getError().toJavaList()));
}

if (accounts.existsByEmailAddress(new Account.EmailAddress(cmd.emailAddress()))) {
if (accounts.existsByEmailAddress(new EmailAddress(cmd.emailAddress()))) {
return Either.left(Failure.ofConflict("Email address '%s' already exists.".formatted(cmd.emailAddress())));
}

var account = Account.builder()
.firstName(cmd.firstName())
.lastName(cmd.lastName())
.emailAddress(new Account.EmailAddress(cmd.emailAddress()))
.accountDetails(new AccountDetails(cmd.firstName(), cmd.lastName()))
.emailAddress(new EmailAddress(cmd.emailAddress()))
.preference(new Preference(Currency.getInstance(cmd.currency()), ZoneId.of(cmd.zoneId())))
.build();

accounts.save(account);
Expand All @@ -58,8 +60,10 @@ Validation<Seq<FieldViolation>, CreateAccountCommand> validate(CreateAccountComm
return Validation.combine(
validateFirstName(cmd.firstName()),
validateLastName(cmd.lastName()),
validateEmailAddress(cmd.emailAddress())
).ap((firstName, lastName, emailAddress) -> cmd);
validateEmailAddress(cmd.emailAddress()),
validateCurrency(cmd.currency()),
validateZoneId(cmd.zoneId())
).ap((firstName, lastName, emailAddress, currency, zoneId) -> cmd);
}

private Validation<FieldViolation, String> validateFirstName(String firstName) {
Expand Down Expand Up @@ -114,5 +118,33 @@ private Validation<FieldViolation, String> validateEmailAddress(String emailAddr

return Valid(emailAddress);
}

private Validation<FieldViolation, String> validateCurrency(String currencyCode) {
try {
Currency.getInstance(currencyCode);
} catch (NullPointerException | IllegalArgumentException e) {
return Invalid(FieldViolation.builder()
.field(FIELD_CURRENCY)
.message("Currency '%s' is invalid.".formatted(currencyCode))
.rejectedValue(currencyCode)
.build());
}

return Valid(currencyCode);
}

private Validation<FieldViolation, String> validateZoneId(String zoneId) {
try {
ZoneId.of(zoneId);
} catch (DateTimeException | NullPointerException e) {
return Invalid(FieldViolation.builder()
.field(FIELD_ZONE_ID)
.message("ZoneId '%s' is invalid.".formatted(zoneId))
.rejectedValue(zoneId)
.build());
}

return Valid(zoneId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ public interface CreateAccountUseCase {
@Builder
record CreateAccountCommand(String firstName,
String lastName,
String emailAddress) {
String emailAddress,
String currency,
String zoneId) {

public static final String FIELD_FIRST_NAME = "firstName";
public static final String FIELD_LAST_NAME = "lastName";
public static final String FIELD_EMAIL_ADDRESS = "emailAddress";
public static final String FIELD_CURRENCY = "currency";
public static final String FIELD_ZONE_ID = "zoneId";

}
}
58 changes: 26 additions & 32 deletions server/src/main/java/io/myfinbox/account/domain/Account.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import io.myfinbox.account.AccountCreated;
import jakarta.persistence.*;
import lombok.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.AbstractAggregateRoot;

import java.io.Serializable;
import java.time.Instant;
import java.util.UUID;
import java.util.regex.Pattern;

import static io.myfinbox.shared.Guards.*;
import static io.myfinbox.shared.Guards.notNull;
import static lombok.AccessLevel.PRIVATE;

@Entity
Expand All @@ -27,28 +26,38 @@ public class Account extends AbstractAggregateRoot<Account> {
public static final String patternRFC5322 = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$";
static final Pattern pattern = Pattern.compile(patternRFC5322);

private @EmbeddedId AccountIdentifier id;
private @Embedded EmailAddress emailAddress;
private String firstName;
private String lastName;
private Instant creationDate;
@EmbeddedId
private final AccountIdentifier id;
private final Instant creationDate;

@Builder
public Account(String firstName, String lastName, EmailAddress emailAddress) {
this.emailAddress = notNull(emailAddress, "emailAddress cannot be null");
@Embedded
private EmailAddress emailAddress;

if (!StringUtils.isBlank(firstName)) {
this.firstName = doesNotOverflow(firstName.trim(), MAX_LENGTH, "firstName overflow, max length allowed '%d'".formatted(MAX_LENGTH));
}
@Embedded
private AccountDetails accountDetails;

if (!StringUtils.isBlank(lastName)) {
this.lastName = doesNotOverflow(lastName.trim(), MAX_LENGTH, "lastName overflow, max length allowed '%d'".formatted(MAX_LENGTH));
}
@Embedded
private Preference preference;

@Builder
public Account(AccountDetails accountDetails,
EmailAddress emailAddress,
Preference preference) {
this.accountDetails = notNull(accountDetails, "accountDetails cannot be null");
this.emailAddress = notNull(emailAddress, "emailAddress cannot be null");
this.preference = notNull(preference, "preference cannot be null");

this.id = new AccountIdentifier(UUID.randomUUID());
this.creationDate = Instant.now();

registerEvent(new AccountCreated(this.id.id(), this.emailAddress.emailAddress(), firstName, lastName));
registerEvent(AccountCreated.builder()
.accountId(this.id.id())
.emailAddress(this.emailAddress.emailAddress())
.firstName(this.accountDetails.firstName())
.lastName(this.accountDetails.lastName())
.currency(this.preference.currency())
.zoneId(this.preference.zoneId())
.build());
}

@Embeddable
Expand All @@ -63,19 +72,4 @@ public String toString() {
return id.toString();
}
}

@Embeddable
public record EmailAddress(String emailAddress) implements Serializable {

public EmailAddress {
notBlank(emailAddress, "emailAddress cannot be blank");
doesNotOverflow(emailAddress.trim(), MAX_LENGTH, "emailAddress max length must be '%d'".formatted(MAX_LENGTH));
matches(emailAddress, pattern, "emailAddress must match '%s'".formatted(patternRFC5322));
}

@Override
public String toString() {
return emailAddress;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.myfinbox.account.domain;

import jakarta.persistence.Embeddable;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;

import java.io.Serializable;

import static io.myfinbox.shared.Guards.doesNotOverflow;
import static lombok.AccessLevel.PACKAGE;

@ToString
@Embeddable
@EqualsAndHashCode
@NoArgsConstructor(access = PACKAGE, force = true)
public final class AccountDetails implements Serializable {

public static final int MAX_LENGTH = 255;

private String firstName;
private String lastName;

public AccountDetails(String firstName, String lastName) {
if (!StringUtils.isBlank(firstName)) {
this.firstName = doesNotOverflow(firstName.trim(), MAX_LENGTH, "firstName overflow, max length allowed '%d'".formatted(MAX_LENGTH));
}

if (!StringUtils.isBlank(lastName)) {
this.lastName = doesNotOverflow(lastName.trim(), MAX_LENGTH, "lastName overflow, max length allowed '%d'".formatted(MAX_LENGTH));
}
}

public String firstName() {
return firstName;
}

public String lastName() {
return lastName;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.myfinbox.account.domain;

import io.myfinbox.account.domain.Account.AccountIdentifier;
import io.myfinbox.account.domain.Account.EmailAddress;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

Expand Down
27 changes: 27 additions & 0 deletions server/src/main/java/io/myfinbox/account/domain/EmailAddress.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.myfinbox.account.domain;

import jakarta.persistence.Embeddable;

import java.io.Serializable;
import java.util.regex.Pattern;

import static io.myfinbox.shared.Guards.*;

@Embeddable
public record EmailAddress(String emailAddress) implements Serializable {

static final String patternRFC5322 = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$";
static final Pattern pattern = Pattern.compile(patternRFC5322);
static final int MAX_LENGTH = 255;

public EmailAddress {
notBlank(emailAddress, "emailAddress cannot be blank");
doesNotOverflow(emailAddress.trim(), MAX_LENGTH, "emailAddress max length must be '%d'".formatted(MAX_LENGTH));
matches(emailAddress, pattern, "emailAddress must match '%s'".formatted(patternRFC5322));
}

@Override
public String toString() {
return emailAddress;
}
}
Loading

0 comments on commit 74ffd1e

Please sign in to comment.