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

feat: 쿠폰 발행 기능 구현 및 테스트 #57

Merged
merged 10 commits into from
Nov 9, 2023
35 changes: 35 additions & 0 deletions src/docs/asciidoc/coupon.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
== 쿠폰(Coupon)

쿠폰에 대해 생성/삭제/조회/발급/사용 기능을 제공합니다.

=== 쿠폰 생성

관리자가 쿠폰을 생성합니다.

[discrete]
==== 요청

include::{snippets}/coupons/http-request.adoc[]

[discrete]
==== 응답

include::{snippets}/coupons/http-response.adoc[]

=== 쿠폰 삭제 (진행 중)

관리자가 쿠폰을 삭제합니다.

=== 쿠폰 조회 (진행 중)

관리자 혹은 사용자가 쿠폰들을 조회합니다.

사용자가 자신의 보관함에 있는 쿠폰들을 조회합니다.

=== 쿠폰 발급 (진행 중)

사용자가 발급 가능한 쿠폰을 선착순으로 발급 받습니다.

=== 쿠폰 사용 (진행 중)

사용자가 자신의 보관함에 있는 쿠폰들을 사용합니다.
45 changes: 45 additions & 0 deletions src/main/java/com/moabam/api/application/CouponService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.moabam.api.application;

import java.time.LocalDateTime;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.moabam.api.domain.entity.Coupon;
import com.moabam.api.domain.repository.CouponRepository;
import com.moabam.api.dto.CouponMapper;
import com.moabam.api.dto.CreateCouponRequest;
import com.moabam.global.error.exception.BadRequestException;
import com.moabam.global.error.exception.ConflictException;
import com.moabam.global.error.model.ErrorMessage;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CouponService {

private final CouponRepository couponRepository;

@Transactional
public void createCoupon(Long adminId, CreateCouponRequest request) {
validateConflictCouponName(request.name());
validateCouponPeriod(request.startAt(), request.endAt());

Coupon coupon = CouponMapper.toEntity(adminId, request);
couponRepository.save(coupon);
}

private void validateConflictCouponName(String name) {
if (couponRepository.existsByName(name)) {
throw new ConflictException(ErrorMessage.CONFLICT_COUPON_NAME);
}
}

private void validateCouponPeriod(LocalDateTime startAt, LocalDateTime endAt) {
if (startAt.isAfter(endAt)) {
throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD);
}
}
}
93 changes: 93 additions & 0 deletions src/main/java/com/moabam/api/domain/entity/Coupon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.moabam.api.domain.entity;

import static com.moabam.global.error.model.ErrorMessage.*;
import static java.util.Objects.*;

import java.time.LocalDateTime;

import org.hibernate.annotations.ColumnDefault;

import com.moabam.api.domain.entity.enums.CouponType;
import com.moabam.global.common.entity.BaseTimeEntity;
import com.moabam.global.error.exception.BadRequestException;

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 lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

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

@Column(name = "name", nullable = false, unique = true, length = 20)
private String name;

@ColumnDefault("1")
@Column(name = "point", nullable = false)
private int point;

@Column(name = "description", length = 50)
private String description;

@Enumerated(value = EnumType.STRING)
@Column(name = "type", nullable = false)
private CouponType type;

@ColumnDefault("1")
@Column(name = "stock", nullable = false)
private int stock;

@Column(name = "start_at", nullable = false)
private LocalDateTime startAt;

@Column(name = "end_at", nullable = false)
private LocalDateTime endAt;

@Column(name = "admin_id", updatable = false, nullable = false)
private Long adminId;

@Builder
private Coupon(String name, int point, String description, CouponType type, int stock, LocalDateTime startAt,
LocalDateTime endAt, Long adminId) {
this.name = requireNonNull(name);
this.point = validatePoint(point);
this.description = description;
this.type = requireNonNull(type);
this.stock = validateStock(stock);
this.startAt = requireNonNull(startAt);
this.endAt = requireNonNull(endAt);
this.adminId = requireNonNull(adminId);
}

private int validatePoint(int point) {
if (point < 1) {
throw new BadRequestException(INVALID_COUPON_POINT);
}

return point;
}

private int validateStock(int stock) {
if (stock < 1) {
throw new BadRequestException(INVALID_COUPON_STOCK);
}

return stock;
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/moabam/api/domain/entity/enums/CouponType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.moabam.api.domain.entity.enums;

import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.moabam.global.error.exception.NotFoundException;
import com.moabam.global.error.model.ErrorMessage;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum CouponType {

MORNING_COUPON("아침"),
NIGHT_COUPON("저녁"),
GOLDEN_COUPON("황금"),
DISCOUNT_COUPON("할인");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: 아침 저녁 황금과 할인의 차이가 뭔가요?
할인이 지난번 얘기가 나왔던 결제 시 할인인가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다! 할인은 결제 시 할인 입니다!


private final String typeName;
private static final Map<String, CouponType> COUPON_TYPE_MAP;

static {
COUPON_TYPE_MAP = Collections.unmodifiableMap(Arrays.stream(values())
.collect(Collectors.toMap(CouponType::getTypeName, Function.identity())));
}

public static CouponType from(String typeName) {
return Optional.ofNullable(COUPON_TYPE_MAP.get(typeName))
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_TYPE));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.moabam.api.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.moabam.api.domain.entity.Coupon;

public interface CouponRepository extends JpaRepository<Coupon, Long> {

boolean existsByName(String name);
}
24 changes: 24 additions & 0 deletions src/main/java/com/moabam/api/dto/CouponMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.moabam.api.dto;

import com.moabam.api.domain.entity.Coupon;
import com.moabam.api.domain.entity.enums.CouponType;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CouponMapper {

public static Coupon toEntity(Long adminId, CreateCouponRequest request) {
return Coupon.builder()
.name(request.name())
.description(request.description())
.type(CouponType.from(request.type()))
.point(request.point())
.stock(request.stock())
.startAt(request.startAt())
.endAt(request.endAt())
.adminId(adminId)
.build();
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/moabam/api/dto/CreateCouponRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.moabam.api.dto;

import java.time.LocalDateTime;

import org.hibernate.validator.constraints.Length;

import com.fasterxml.jackson.annotation.JsonFormat;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;

@Builder
public record CreateCouponRequest(
@NotBlank(message = "쿠폰명이 입력되지 않았거나 20자를 넘었습니다.") @Length(max = 20) String name,
@Length(max = 50, message = "쿠폰 간단 소개는 최대 50자까지 가능합니다.") String description,
@NotBlank(message = "쿠폰 종류를 입력해주세요.") String type,
@Min(value = 1, message = "벌레 수 혹은 할인 금액은 1 이상이어야 합니다.") int point,
@Min(value = 1, message = "쿠폰 재고는 1 이상이어야 합니다.") int stock,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm")
@NotNull(message = "쿠폰 발급 시작 시각을 입력해주세요.") LocalDateTime startAt,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm")
@NotNull(message = "쿠폰 발급 종료 시각을 입력해주세요.") LocalDateTime endAt
) {

}
27 changes: 27 additions & 0 deletions src/main/java/com/moabam/api/presentation/CouponController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.moabam.api.presentation;

import org.springframework.http.HttpStatus;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.moabam.api.application.CouponService;
import com.moabam.api.dto.CreateCouponRequest;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/admins/coupons")
public class CouponController {

private final CouponService couponService;

@PostMapping
@ResponseStatus(HttpStatus.OK)
public void createCoupon(@RequestBody CreateCouponRequest request) {
couponService.createCoupon(1L, request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ public enum ErrorMessage {

FAILED_FCM_INIT("파이어베이스 설정을 실패했습니다."),
NOT_FOUND_FCM_TOKEN("해당 유저는 접속 중이 아닙니다."),
CONFLICT_KNOCK("이미 콕 알림을 보낸 대상입니다.");
CONFLICT_KNOCK("이미 콕 알림을 보낸 대상입니다."),

INVALID_COUPON_POINT("쿠폰의 보너스 포인트는 0 이상이어야 합니다."),
INVALID_COUPON_STOCK("쿠폰의 재고는 0 이상이어야 합니다."),
CONFLICT_COUPON_NAME("쿠폰의 이름이 중복되었습니다."),
NOT_FOUND_COUPON_TYPE("존재하지 않는 쿠폰 종류입니다."),
INVALID_COUPON_PERIOD("쿠폰 발급 종료 시각은 시작 시각보다 이후여야 합니다.");

private final String message;
}
Loading