Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package dev.amg.payguard.identity.customer.domain.value_object;

import domain.ValueObject;
import org.jspecify.annotations.Nullable;
import org.springframework.modulith.NamedInterface;

@NamedInterface
public final class CustomerId implements ValueObject {
private static final String CUSTOMER_ID_PREFIX = "cus_";
private final String value;

private CustomerId(String value) {
this.value = PrefixedIdValidator.validateAndReturn(value, CUSTOMER_ID_PREFIX);
}

public static CustomerId parse(String value) {
return new CustomerId(value);
}

@Override
public boolean equals(@Nullable Object o) {
if (!(o instanceof CustomerId that)) return false;

return getValue().equals(that.getValue());
}

@Override
public int hashCode() {
return getValue().hashCode();
}

public String getValue() {
return value;
}

@Override
public boolean sameValueAs(@Nullable ValueObject otherCustomerId) {
return equals(otherCustomerId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.amg.payguard.identity.customer.domain.value_object;

import domain.ValueObject;
import java.util.Objects;
import org.jspecify.annotations.Nullable;

/** Immutable identifier. Parsing validateAndReturns syntax, not existence or authorization. */
public final class OtpId implements ValueObject {
private static final String OTP_ID_PREFIX = "otp_";
private final String value;

private OtpId(String value) {
this.value = PrefixedIdValidator.validateAndReturn(value, OTP_ID_PREFIX);
}

public static OtpId parse(String value) {
return new OtpId(value);
}

public String value() {
return value;
}

@Override
public boolean equals(@Nullable Object other) {
return this == other || (other instanceof OtpId that && value.equals(that.value));
}

@Override
public int hashCode() {
return Objects.hashCode(value);
}

@Override
public String toString() {
return value;
}

@Override
public boolean sameValueAs(@Nullable ValueObject otherOtpId) {
return equals(otherOtpId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.amg.payguard.identity.customer.domain.value_object;

import java.util.Objects;

final class PrefixedIdValidator {
private static final int RANDOM_SUFFIX_LENGTH = 22;
private static final String BASE62_ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

private PrefixedIdValidator() {}

static String validateAndReturn(String identifier, String expectedPrefix) {
Objects.requireNonNull(identifier);
Objects.requireNonNull(expectedPrefix);

if (identifier.length() != (expectedPrefix.length() + RANDOM_SUFFIX_LENGTH)
|| !identifier.startsWith(expectedPrefix)) {
throw new IllegalArgumentException(
"invalid identifier format: expected "
+ expectedPrefix
+ " followed by 22 Base62 characters");
}

for (int index = expectedPrefix.length(); index < identifier.length(); index++) {
char character = identifier.charAt(index);
boolean isBase62Character = BASE62_ALPHABET.indexOf(character) >= 0;
if (!isBase62Character) {
throw new IllegalArgumentException("invalid identifier suffix: expected Base62 characters");
}
}
return identifier;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package dev.amg.payguard.identity.customer.domain.value_object;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dev.amg.payguard.identity.verification.domain.enums;

public enum OtpChannel {
SMS,
EMAIL,
PUSH,
AUTHENTICATOR_APP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.amg.payguard.identity.verification.domain.enums;

public enum OtpPurpose {
LOGIN,
TRANSFER_CONFIRM,
LOAN_DISBURSE,
COLLATERAL_RELEASE,
CARD_ISSUANCE,
PROFILE_CHANGE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.amg.payguard.identity.verification.domain.enums;

public enum OtpStatus {
ACTIVE,
USED,
EXPIRED,
LOCKED,
INVALIDATED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.amg.payguard.identity.verification.domain.enums;

public enum OtpVerificationMode {
RANDOM_NUMERIC,
TOTP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.amg.payguard.identity.verification.domain.enums;

public enum OtpVerificationResult {
VERIFIED,
INVALID_CODE,
EXPIRED,
ALREADY_USED,
LOCKED,
INVALIDATED,
PURPOSE_MISMATCH,
DEVICE_MISMATCH
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.amg.payguard.identity.verification.domain.event;

import dev.amg.payguard.identity.customer.domain.value_object.CustomerId;
import dev.amg.payguard.identity.customer.domain.value_object.OtpId;
import dev.amg.payguard.identity.verification.domain.enums.OtpChannel;
import dev.amg.payguard.identity.verification.domain.enums.OtpPurpose;
import dev.amg.payguard.identity.verification.domain.enums.OtpVerificationResult;
import java.time.Instant;

public record OtpEvent(
String type,
OtpId challengeId,
CustomerId customerId,
OtpPurpose purpose,
OtpChannel channel,
OtpVerificationResult result,
Instant occurredAt,
String reason) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.amg.payguard.identity.verification.domain.event;

@FunctionalInterface
public interface OtpEventPublisher {
void publish(OtpEvent event);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.amg.payguard.identity.verification.domain.exception;

import java.io.Serial;

public final class InvalidOtpAttemptLimitException extends RuntimeException {

@Serial private static final long serialVersionUID = 1L;
private static final String MESSAGE =
"OTP max attempts must be greater than or equal to %d, but was %d.";

public InvalidOtpAttemptLimitException(int maxAttempts, int minAttempts) {
super(MESSAGE.formatted(maxAttempts, minAttempts));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.amg.payguard.identity.verification.domain.exception;

import java.io.Serial;

public final class OtpAttemptCountOutOfRangeException extends RuntimeException {

@Serial private static final long serialVersionUID = 1L;
public static final String MESSAGE = "OTP attempts must be between 0 and %d, but was %d.";

public OtpAttemptCountOutOfRangeException(int attempts, int maxAttempts) {
super(MESSAGE.formatted(maxAttempts, attempts));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.amg.payguard.identity.verification.domain.exception;

import java.io.Serial;

public final class OtpCodeHashMustNotBeBlankException extends RuntimeException {

@Serial private static final long serialVersionUID = 1L;
public static final String MESSAGE = "OTP code hash must not be blank.";

public OtpCodeHashMustNotBeBlankException() {
super(MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.amg.payguard.identity.verification.domain.exception;

import dev.amg.payguard.identity.verification.domain.enums.OtpVerificationMode;
import java.io.Serial;

public final class OtpCodeHashRequiredForRandomNumericVerificationException
extends RuntimeException {
@Serial private static final long serialVersionUID = 1L;

public static final String MESSAGE = "OTP code hash is required for %s verification mode.";

public OtpCodeHashRequiredForRandomNumericVerificationException(
OtpVerificationMode verificationMode) {
super(MESSAGE.formatted(verificationMode));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.amg.payguard.identity.verification.domain.exception;

import java.io.Serial;
import java.time.Instant;

public final class OtpConsumedTimeMustMatchUsedStatusException extends RuntimeException {

@Serial private static final long serialVersionUID = 1L;
public static final String MESSAGE =
"""
OTP consumption time must be within the challenge validity period. \
consumedAt=%s, createdAt=%s, expiresAt=%s.""";

public OtpConsumedTimeMustMatchUsedStatusException(
Instant consumedAt, Instant createdAt, Instant expiresAt) {
super(MESSAGE.formatted(consumedAt, createdAt, expiresAt));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.amg.payguard.identity.verification.domain.exception;

import java.io.Serial;
import java.time.Instant;

public final class OtpConsumedTimeOutsideValidityPeriodException extends RuntimeException {

@Serial private static final long serialVersionUID = 1L;
public static final String MESSAGE =
"""
OTP consumption time must be within the validity period. \
consumedAt=%s, createdAt=%s, expiresAt=%s""";

public OtpConsumedTimeOutsideValidityPeriodException(
Instant consumedAt, Instant createdAt, Instant expiresAt) {
super(MESSAGE.formatted(consumedAt, createdAt, expiresAt));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.amg.payguard.identity.verification.domain.exception;

import java.io.Serial;

public final class OtpDeviceBindingHashMustNotBeBlankException extends RuntimeException {

@Serial private static final long serialVersionUID = 1L;
public static final String MESSAGE = "OTP device binding hash must not be blank.";

public OtpDeviceBindingHashMustNotBeBlankException() {
super(MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.amg.payguard.identity.verification.domain.exception;

import java.io.Serial;

public final class OtpExpirationTimeMustBeAfterCreationTimeException extends RuntimeException {

@Serial private static final long serialVersionUID = 1L;
private static final String MESSAGE = "OTP expiration time must be after the creation time.";

public OtpExpirationTimeMustBeAfterCreationTimeException() {
super(MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.amg.payguard.identity.verification.domain.exception;

import java.io.Serial;
import java.time.Instant;

public class OtpRetentionPeriodMustNotPrecedeExpirationException extends RuntimeException {

@Serial private static final long serialVersionUID = 1L;

public static final String MESSAGE =
"OTP retention period is invalid: retentionUntil must be after expiresAt. "
+ "The OTP record cannot be scheduled for removal before or at the same time as its expiration. "
+ "retentionUntil=%s, expiresAt=%s";

public OtpRetentionPeriodMustNotPrecedeExpirationException(
Instant retentionUntil, Instant expiresAt) {
super(MESSAGE.formatted(retentionUntil, expiresAt));
}
}
Loading
Loading