Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable account locale resolving and add account preference #37

Merged
merged 1 commit into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading