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
162 changes: 162 additions & 0 deletions src/main/java/org/ject/support/admin/mail/domain/MailDispatchJob.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package org.ject.support.admin.mail.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.ject.support.admin.mail.exception.MailErrorCode;
import org.ject.support.admin.mail.exception.MailException;
import org.ject.support.domain.base.BaseTimeEntity;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "mail_dispatch_job", uniqueConstraints = @UniqueConstraint(
name = "uk_mail_dispatch_job_requester_key",
columnNames = {"requested_by_admin_id", "idempotency_key"}))
public class MailDispatchJob extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "scenario_id", nullable = false)
private Long scenarioId;

@Column(name = "recruit_id", nullable = false)
private Long recruitId;

@Column(name = "requested_by_admin_id", nullable = false)
private Long requestedByAdminId;

@Column(name = "idempotency_key", nullable = false, length = 255)
private String idempotencyKey;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private MailDispatchJobStatus status;

@Column(name = "target_count", nullable = false)
private int targetCount;

@Column(name = "processing_count", nullable = false)
private int processingCount;

@Column(name = "success_count", nullable = false)
private int successCount;

@Column(name = "failed_count", nullable = false)
private int failedCount;

@Column(name = "requested_at", nullable = false)
private LocalDateTime requestedAt;

@Column(name = "subject_template", nullable = false, columnDefinition = "TEXT")
private String subjectTemplate;

@Column(name = "body_template", nullable = false, columnDefinition = "TEXT")
private String bodyTemplate;

@Column(name = "input_variables_json", columnDefinition = "TEXT")
private String inputVariablesJson;

@Column(name = "started_at")
private LocalDateTime startedAt;

@Column(name = "finished_at")
private LocalDateTime finishedAt;

@Version
private Long version;

private MailDispatchJob(Long scenarioId,
Long recruitId,
Long requestedByAdminId,
String idempotencyKey,
String subjectTemplate,
String bodyTemplate,
String inputVariablesJson,
int targetCount) {
this.scenarioId = scenarioId;
this.recruitId = recruitId;
this.requestedByAdminId = requestedByAdminId;
this.idempotencyKey = idempotencyKey;
this.subjectTemplate = subjectTemplate;
this.bodyTemplate = bodyTemplate;
this.inputVariablesJson = inputVariablesJson;
this.targetCount = targetCount;
Comment thread
023-dev marked this conversation as resolved.
this.status = MailDispatchJobStatus.REQUESTED;
this.requestedAt = LocalDateTime.now();
}

public static MailDispatchJob create(Long scenarioId,
Long recruitId,
Long requestedByAdminId,
String idempotencyKey,
String subjectTemplate,
String bodyTemplate,
String inputVariablesJson,
int targetCount) {
if (targetCount <= 0) {
throw new MailException(MailErrorCode.INVALID_DISPATCH_TARGET_COUNT);
}
return new MailDispatchJob(
scenarioId,
recruitId,
requestedByAdminId,
idempotencyKey,
subjectTemplate,
bodyTemplate,
inputVariablesJson,
targetCount
);
}

public void startProcessing() {
validateStatus(MailDispatchJobStatus.REQUESTED);
status = MailDispatchJobStatus.PROCESSING;
processingCount = targetCount;
startedAt = LocalDateTime.now();
}

public void recordSuccess() {
validateStatus(MailDispatchJobStatus.PROCESSING);
processingCount--;
successCount++;
finishIfCompleted();
}

public void recordFailure() {
validateStatus(MailDispatchJobStatus.PROCESSING);
processingCount--;
failedCount++;
finishIfCompleted();
}

private void finishIfCompleted() {
if (processingCount > 0) {
return;
}
// 모든 대상이 실패한 경우에만 작업을 실패로 마무리한다.
status = failedCount == targetCount
? MailDispatchJobStatus.FAILED
: MailDispatchJobStatus.COMPLETED;
finishedAt = LocalDateTime.now();
}

private void validateStatus(MailDispatchJobStatus expectedStatus) {
if (status != expectedStatus) {
throw new MailException(MailErrorCode.INVALID_DISPATCH_JOB_STATUS);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.ject.support.admin.mail.domain;

public enum MailDispatchJobStatus {
REQUESTED,
PROCESSING,
COMPLETED,
FAILED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.ject.support.admin.mail.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.ject.support.admin.mail.exception.MailErrorCode;
import org.ject.support.admin.mail.exception.MailException;
import org.ject.support.domain.base.BaseTimeEntity;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "mail_dispatch_target")
public class MailDispatchTarget extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "dispatch_job_id", nullable = false)
private MailDispatchJob dispatchJob;

@Column(name = "apply_id", nullable = false)
private Long applyId;

@Column(nullable = false, length = 255)
private String email;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private MailDispatchTargetStatus status;

@Column(name = "failure_reason", columnDefinition = "TEXT")
private String failureReason;

@Column(name = "sent_at")
private LocalDateTime sentAt;

@Version
private Long version;

private MailDispatchTarget(MailDispatchJob dispatchJob, Long applyId, String email) {
this.dispatchJob = dispatchJob;
this.applyId = applyId;
this.email = email;
this.status = MailDispatchTargetStatus.PENDING;
}

public static MailDispatchTarget pending(MailDispatchJob dispatchJob, Long applyId, String email) {
return new MailDispatchTarget(dispatchJob, applyId, email);
}

public void markSent() {
validatePending();
status = MailDispatchTargetStatus.SENT;
sentAt = LocalDateTime.now();
failureReason = null;
}

public void markFailed(String failureReason) {
validatePending();
status = MailDispatchTargetStatus.FAILED;
this.failureReason = failureReason;
sentAt = null;
}

private void validatePending() {
if (status != MailDispatchTargetStatus.PENDING) {
throw new MailException(MailErrorCode.INVALID_DISPATCH_TARGET_STATUS);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.ject.support.admin.mail.domain;

public enum MailDispatchTargetStatus {
PENDING,
SENT,
FAILED
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public enum MailErrorCode implements ErrorCode {
TEST_MAIL_SEND_FAILURE(SERVICE_UNAVAILABLE, "MAIL-11", "테스트 메일 발송에 실패했습니다."),
INVALID_VARIABLE_VALUE(BAD_REQUEST, "MAIL-12", "메일 입력 변수 값이 올바르지 않습니다."),
UNRESOLVED_TEMPLATE_VARIABLE(BAD_REQUEST, "MAIL-13", "렌더링되지 않은 템플릿 변수가 있습니다."),
INVALID_DISPATCH_TARGET_STATUS(BAD_REQUEST, "MAIL-14", "현재 상태에서는 발송 대상 처리를 수행할 수 없습니다."),
INVALID_DISPATCH_TARGET_COUNT(BAD_REQUEST, "MAIL-15", "메일 발송 대상은 1~500명이어야 합니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.ject.support.admin.mail.repository;

import java.util.Optional;
import org.ject.support.admin.mail.domain.MailDispatchJob;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MailDispatchJobRepository extends JpaRepository<MailDispatchJob, Long> {

Optional<MailDispatchJob> findByRequestedByAdminIdAndIdempotencyKey(
Long requestedByAdminId, String idempotencyKey);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.ject.support.admin.mail.repository;

import java.util.List;
import java.util.Optional;
import org.ject.support.admin.mail.domain.MailDispatchTarget;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MailDispatchTargetRepository extends JpaRepository<MailDispatchTarget, Long> {

Optional<MailDispatchTarget> findByDispatchJobIdAndApplyId(Long dispatchJobId, Long applyId);

List<MailDispatchTarget> findAllByDispatchJobIdOrderByIdAsc(Long dispatchJobId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
CREATE TABLE mail_dispatch_job
(
id BIGINT AUTO_INCREMENT NOT NULL,
scenario_id BIGINT NOT NULL,
recruit_id BIGINT NOT NULL,
requested_by_admin_id BIGINT NOT NULL,
idempotency_key VARCHAR(255) NOT NULL,
status VARCHAR(30) NOT NULL,
target_count INT NOT NULL,
processing_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
failed_count INT NOT NULL DEFAULT 0,
requested_at datetime(6) NOT NULL,
subject_template TEXT NOT NULL,
body_template TEXT NOT NULL,
input_variables_json TEXT,
started_at datetime(6),
finished_at datetime(6),
version BIGINT NOT NULL DEFAULT 0,
created_at datetime(6),
updated_at datetime(6),
CONSTRAINT `PRIMARY` PRIMARY KEY (id),
CONSTRAINT uk_mail_dispatch_job_requester_key UNIQUE (requested_by_admin_id, idempotency_key)
) ENGINE = InnoDB;

CREATE TABLE mail_dispatch_target
(
id BIGINT AUTO_INCREMENT NOT NULL,
dispatch_job_id BIGINT NOT NULL,
apply_id BIGINT NOT NULL,
email VARCHAR(255) NOT NULL,
status VARCHAR(30) NOT NULL,
failure_reason TEXT,
sent_at datetime(6),
version BIGINT NOT NULL DEFAULT 0,
created_at datetime(6),
updated_at datetime(6),
CONSTRAINT `PRIMARY` PRIMARY KEY (id),
CONSTRAINT fk_mail_dispatch_target_job
FOREIGN KEY (dispatch_job_id) REFERENCES mail_dispatch_job (id) ON DELETE CASCADE,
CONSTRAINT uk_mail_dispatch_target_job_apply UNIQUE (dispatch_job_id, apply_id)
) ENGINE = InnoDB;

CREATE INDEX idx_mail_dispatch_target_job_status
ON mail_dispatch_target (dispatch_job_id, status);
Loading
Loading