From f0bdf572cdb0b322f2fba0bcb532910a76568426 Mon Sep 17 00:00:00 2001 From: minbros Date: Fri, 19 Sep 2025 00:32:23 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Fix:=20Test=20=EC=9D=B8=EC=8A=A4=ED=84=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/workflow-test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index d0ab3ac..da4396f 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -6,9 +6,6 @@ on: - develop - develop/** - feature/** - pull_request: - branches: - - main jobs: build-and-deploy: From 36681c5f9f16600d763c61bd5ecd87456cfb7350 Mon Sep 17 00:00:00 2001 From: minbros Date: Tue, 23 Sep 2025 22:35:17 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Feat:=20CheckoutOrder=EC=97=90=20Ticket=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gorae/backend/entity/BaseEntity.java | 21 ++++++++++++++++--- .../gorae/backend/entity/CheckoutOrder.java | 14 ++++++++++++- .../java/gorae/backend/entity/Student.java | 9 ++------ .../java/gorae/backend/entity/Ticket.java | 6 ++++++ .../backend/entity/instructor/Instructor.java | 7 ------- .../checkout/CheckoutCompletionService.java | 12 +++++++---- 6 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/main/java/gorae/backend/entity/BaseEntity.java b/src/main/java/gorae/backend/entity/BaseEntity.java index d97b446..94abab3 100644 --- a/src/main/java/gorae/backend/entity/BaseEntity.java +++ b/src/main/java/gorae/backend/entity/BaseEntity.java @@ -1,8 +1,9 @@ package gorae.backend.entity; +import gorae.backend.constant.MemberRole; +import gorae.backend.entity.instructor.Instructor; import jakarta.persistence.*; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; @@ -24,8 +25,7 @@ public abstract class BaseEntity { private Long id; @Column(unique = true, updatable = false, nullable = false, columnDefinition = "uuid") - @Builder.Default - private UUID publicId = UUID.randomUUID(); + private UUID publicId; @CreatedDate @Column(updatable = false) @@ -34,4 +34,19 @@ public abstract class BaseEntity { @LastModifiedDate @Column private Instant updatedAt; + + @PrePersist + protected void onCreate() { + if (publicId == null) { + publicId = UUID.randomUUID(); + } + + if (this instanceof Member member) { + if (member instanceof Student) { + member.setRole(MemberRole.STUDENT); + } else if (member instanceof Instructor) { + member.setRole(MemberRole.INSTRUCTOR); + } + } + } } diff --git a/src/main/java/gorae/backend/entity/CheckoutOrder.java b/src/main/java/gorae/backend/entity/CheckoutOrder.java index 5d96f8a..118d221 100644 --- a/src/main/java/gorae/backend/entity/CheckoutOrder.java +++ b/src/main/java/gorae/backend/entity/CheckoutOrder.java @@ -3,11 +3,14 @@ import gorae.backend.constant.CheckoutOrderStatus; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -29,11 +32,20 @@ public class CheckoutOrder extends BaseEntity { @JoinColumn(name = "product_id", nullable = false) private Product product; + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) + @Builder.Default + private List tickets = new ArrayList<>(); + private Instant paymentDate; - public void completeOrder() { + public void completeOrder(List tickets) { status = CheckoutOrderStatus.COMPLETED; paymentDate = Instant.now(); + this.tickets = tickets; + + for (Ticket ticket : tickets) { + ticket.setOrder(this); + } } public void updateStatus(CheckoutOrderStatus status) { diff --git a/src/main/java/gorae/backend/entity/Student.java b/src/main/java/gorae/backend/entity/Student.java index 9ece160..242a054 100644 --- a/src/main/java/gorae/backend/entity/Student.java +++ b/src/main/java/gorae/backend/entity/Student.java @@ -1,6 +1,5 @@ package gorae.backend.entity; -import gorae.backend.constant.MemberRole; import gorae.backend.constant.TicketStatus; import gorae.backend.dto.member.StudentDto; import jakarta.persistence.*; @@ -42,12 +41,7 @@ public class Student extends Member { @Builder.Default private List lectures = new ArrayList<>(); - @PrePersist - public void prePersist() { - super.setRole(MemberRole.STUDENT); - } - - public void addFirstTicket() { + public Ticket addFirstTicket() { Instant now = Instant.now(); ZoneId thailandZone = ZoneId.of("Asia/Bangkok"); LocalDateTime endOfDay = LocalDateTime.of(2025, 9, 28, 23, 59, 59); @@ -63,6 +57,7 @@ public void addFirstTicket() { tickets.add(ticket); isFirst = false; + return ticket; } public Ticket addMonthlyTicket(Product product, Instant now) { diff --git a/src/main/java/gorae/backend/entity/Ticket.java b/src/main/java/gorae/backend/entity/Ticket.java index 5ee9e5a..262f99f 100644 --- a/src/main/java/gorae/backend/entity/Ticket.java +++ b/src/main/java/gorae/backend/entity/Ticket.java @@ -6,6 +6,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.experimental.SuperBuilder; import java.time.Instant; @@ -34,6 +35,11 @@ public class Ticket extends BaseEntity { @Enumerated(EnumType.STRING) private TicketStatus status; + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id") + private CheckoutOrder order; + public void useTicket() { status = TicketStatus.USED; } diff --git a/src/main/java/gorae/backend/entity/instructor/Instructor.java b/src/main/java/gorae/backend/entity/instructor/Instructor.java index 5cad18a..993f3ed 100644 --- a/src/main/java/gorae/backend/entity/instructor/Instructor.java +++ b/src/main/java/gorae/backend/entity/instructor/Instructor.java @@ -1,6 +1,5 @@ package gorae.backend.entity.instructor; -import gorae.backend.constant.MemberRole; import gorae.backend.dto.instructor.InstructorDto; import gorae.backend.entity.Course; import gorae.backend.entity.Lecture; @@ -9,7 +8,6 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.OneToMany; -import jakarta.persistence.PrePersist; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -49,11 +47,6 @@ public class Instructor extends Member { @OneToMany(mappedBy = "instructor", orphanRemoval = true) private List lectures; - @PrePersist - public void prePersist() { - super.setRole(MemberRole.INSTRUCTOR); - } - public InstructorDto toDto() { return new InstructorDto(getName(), profileImageUrl); } diff --git a/src/main/java/gorae/backend/service/checkout/CheckoutCompletionService.java b/src/main/java/gorae/backend/service/checkout/CheckoutCompletionService.java index cbb121a..ddf75b0 100644 --- a/src/main/java/gorae/backend/service/checkout/CheckoutCompletionService.java +++ b/src/main/java/gorae/backend/service/checkout/CheckoutCompletionService.java @@ -14,6 +14,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; @Slf4j @@ -26,18 +28,20 @@ public class CheckoutCompletionService { @Transactional public void handleCompletedOrder(CheckoutOrder order) { - order.completeOrder(); - Product product = order.getProduct(); + List tickets; if (product.getName() == ProductName.FIRST_CLASS) { Student student = order.getStudent(); - student.addFirstTicket(); + Ticket firstTicket = student.addFirstTicket(); + + tickets = new ArrayList<>(Collections.singletonList(firstTicket)); studentRepository.save(student); } else { - List tickets = order.getStudent().addMonthlyTickets(product); + tickets = order.getStudent().addMonthlyTickets(product); ticketRepository.saveAll(tickets); } + order.completeOrder(tickets); checkoutOrderRepository.save(order); log.info("[Service] Order {} completed in Paypal, syncing locally", order.getOrderId()); } From 0be67c7bcb68a4d46d02616b0ab599a24fbbb6f0 Mon Sep 17 00:00:00 2001 From: minbros Date: Wed, 24 Sep 2025 22:17:55 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Fix:=20=ED=92=88=EB=AA=A9=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gorae/backend/constant/ProductName.java | 9 ++--- src/main/resources/init/01-product-data.sql | 39 ++++++------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/main/java/gorae/backend/constant/ProductName.java b/src/main/java/gorae/backend/constant/ProductName.java index dec2b9b..95d558b 100644 --- a/src/main/java/gorae/backend/constant/ProductName.java +++ b/src/main/java/gorae/backend/constant/ProductName.java @@ -7,12 +7,9 @@ @AllArgsConstructor public enum ProductName { FIRST_CLASS("First Class"), - ONE_CLASS_ONE_TO_TWO("1 Class"), - ONE_CLASS_ONE_TO_FOUR("1 Class"), - FOUR_CLASSES_ONE_TO_TWO("4 Classes"), - FOUR_CLASSES_ONE_TO_FOUR("4 Classes"), - EIGHT_CLASSES_ONE_TO_TWO("8 Classes"), - EIGHT_CLASSES_ONE_TO_FOUR("8 Classes"); + ONE_CLASS("1 Class"), + FOUR_CLASSES("4 Classes"), + EIGHT_CLASSES("8 Classes"); private final String key; } diff --git a/src/main/resources/init/01-product-data.sql b/src/main/resources/init/01-product-data.sql index 1400463..d2f8b48 100644 --- a/src/main/resources/init/01-product-data.sql +++ b/src/main/resources/init/01-product-data.sql @@ -3,39 +3,24 @@ INSERT INTO product (id, public_id, created_at, updated_at, name, price, discoun duration, lesson_ratio) VALUES (1, 'b7e9c8a4-3f2d-4de2-9a07-5f1a6b2c8f52', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'FIRST_CLASS', - '300', '90', 'THB', 1, 7, 5), - (2, '7b1f0e3d-2e49-4c53-bf10-8a1e7e1d3c45', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'FOUR_CLASSES_ONE_TO_FOUR', - '1200', '1080', 'THB', 4, 28, 4), - (3, '5e6d8c7a-1f23-4b8b-9c12-7d6e2e4f5b67', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'FOUR_CLASSES_ONE_TO_TWO', - '1720', '1540', 'THB', 4, 28, 2), - (4, 'a2d4c8e1-6f4b-4e2e-8c91-2e3f7b9d1a23', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'EIGHT_CLASSES_ONE_TO_FOUR', - '2400', '1900', 'THB', 8, 56, 4), - (5, 'f4e7c5b2-3d8e-4a1b-8e6c-9d2b5a7f3c18', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'EIGHT_CLASSES_ONE_TO_TWO', - '3440', '2700', 'THB', 8, 56, 2), - (6, 'e8f3b9d2-7c4a-4c8b-9a5e-0b3f9e7b6a2f', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'ONE_CLASS_ONE_TO_FOUR', - '300', '300', 'THB', 1, 7, 4), - (7, 'e7c9f6a7-3d45-4b0f-9c18-5a6e2b1d8c71', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'ONE_CLASS_ONE_TO_TWO', - '430', '430', 'THB', 1, 7, 2); + '3.60', '2.00', 'USD', 1, 7, 7), + (2, '7b1f0e3d-2e49-4c53-bf10-8a1e7e1d3c45', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'ONE_CLASS', + '3.60', '3.60', 'USD', 1, 7, 7), + (3, '5e6d8c7a-1f23-4b8b-9c12-7d6e2e4f5b67', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'FOUR_CLASSES', + '14.40', '13.00', 'USD', 4, 28, 7), + (4, 'a2d4c8e1-6f4b-4e2e-8c91-2e3f7b9d1a23', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'EIGHT_CLASSES', + '28.80', '23.00', 'USD', 8, 56, 7); INSERT INTO product_benefit (product_id, benefits) -VALUES (1, '1:5 group lesson'), +VALUES (1, '1:7 group lesson'), (1, '1-hour Google Meet class'), (1, 'Only one-time purchase'), - (2, '1:4 group lesson'), + (2, '1:7 group lesson'), (2, '1-hour Google Meet class'), (2, 'Post-class feedback'), - (3, '1:2 group lesson'), + (3, '1:7 group lesson'), (3, '1-hour Google Meet class'), (3, 'Post-class feedback'), - (4, '1:4 group lesson'), + (4, '1:7 group lesson'), (4, '1-hour Google Meet class'), - (4, 'Post-class feedback'), - (5, '1:2 group lesson'), - (5, '1-hour Google Meet class'), - (5, 'Post-class feedback'), - (6, '1:4 group lesson'), - (6, '1-hour Google Meet class'), - (6, 'Post-class feedback'), - (7, '1:2 group lesson'), - (7, '1-hour Google Meet class'), - (7, 'Post-class feedback'); + (4, 'Post-class feedback'); From 597c86b433678153bae7b97d9f7adcfd22528af1 Mon Sep 17 00:00:00 2001 From: minbros Date: Thu, 25 Sep 2025 11:09:50 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Feat:=20=EC=A3=BC=EB=AC=B8=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CheckoutController.java | 18 ++++++++++++++++ .../dto/checkout/CheckoutOrderDto.java | 19 +++++++++++++++++ .../dto/product/ProductSummaryDto.java | 21 +++++++++++++++++++ .../gorae/backend/entity/CheckoutOrder.java | 18 ++++++++++++++++ .../java/gorae/backend/entity/Product.java | 16 ++++++++++++++ .../repository/CheckoutOrderRepository.java | 5 +++++ .../service/checkout/CheckoutService.java | 8 +++++++ src/main/resources/init/05-other-data.sql | 12 +++++------ 8 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 src/main/java/gorae/backend/dto/checkout/CheckoutOrderDto.java create mode 100644 src/main/java/gorae/backend/dto/product/ProductSummaryDto.java diff --git a/src/main/java/gorae/backend/controller/CheckoutController.java b/src/main/java/gorae/backend/controller/CheckoutController.java index b6bb198..af59a5c 100644 --- a/src/main/java/gorae/backend/controller/CheckoutController.java +++ b/src/main/java/gorae/backend/controller/CheckoutController.java @@ -3,11 +3,13 @@ import gorae.backend.common.JwtUtils; import gorae.backend.dto.ResponseDto; import gorae.backend.dto.checkout.ApproveRequestDto; +import gorae.backend.dto.checkout.CheckoutOrderDto; import gorae.backend.dto.checkout.CheckoutRequestDto; import gorae.backend.exception.CustomException; import gorae.backend.exception.ErrorStatus; import gorae.backend.service.checkout.CheckoutService; import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -20,6 +22,8 @@ import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static gorae.backend.common.JwtUtils.getId; import static gorae.backend.common.JwtUtils.getPublicId; @@ -98,4 +102,18 @@ public ResponseEntity> approveCheckout( log.info("[API] ApproveCheckout responded to user: {}, order: {}", publicId, orderId); return ResponseEntity.ok(ResponseDto.createSuccess()); } + + @Operation(summary = "결제 내역 조회") + @ApiResponse(responseCode = "200", description = "결제 내역 조회 성공") + @GetMapping + @PreAuthorize("hasRole('STUDENT')") + public ResponseEntity>> getOrders(Authentication authentication) { + String userId = getId(authentication); + String publicId = getPublicId(authentication); + log.info("[API] GetOrders requested from {}", publicId); + + List orderDtoList = checkoutService.getOrders(userId); + log.info("[API] GetOrders responded to {}", publicId); + return ResponseEntity.ok(ResponseDto.createSuccess(orderDtoList)); + } } diff --git a/src/main/java/gorae/backend/dto/checkout/CheckoutOrderDto.java b/src/main/java/gorae/backend/dto/checkout/CheckoutOrderDto.java new file mode 100644 index 0000000..8a9378b --- /dev/null +++ b/src/main/java/gorae/backend/dto/checkout/CheckoutOrderDto.java @@ -0,0 +1,19 @@ +package gorae.backend.dto.checkout; + +import gorae.backend.constant.CheckoutOrderStatus; +import gorae.backend.dto.product.ProductSummaryDto; +import lombok.Builder; + +import java.time.Instant; +import java.util.UUID; + +@Builder +public record CheckoutOrderDto( + UUID id, + CheckoutOrderStatus status, + ProductSummaryDto product, + int currentCount, + Instant paymentDate, + Instant endDate +) { +} diff --git a/src/main/java/gorae/backend/dto/product/ProductSummaryDto.java b/src/main/java/gorae/backend/dto/product/ProductSummaryDto.java new file mode 100644 index 0000000..488638a --- /dev/null +++ b/src/main/java/gorae/backend/dto/product/ProductSummaryDto.java @@ -0,0 +1,21 @@ +package gorae.backend.dto.product; + +import lombok.Builder; + +import java.math.BigDecimal; +import java.util.UUID; + +@Builder +public record ProductSummaryDto( + UUID id, + String name, + BigDecimal price, + BigDecimal discountedPrice, + BigDecimal discountAmount, + BigDecimal discountPercentage, + String currencyCode, + boolean hasDiscount, + int lessonRatio, + int count +) { +} diff --git a/src/main/java/gorae/backend/entity/CheckoutOrder.java b/src/main/java/gorae/backend/entity/CheckoutOrder.java index 118d221..39f7e6e 100644 --- a/src/main/java/gorae/backend/entity/CheckoutOrder.java +++ b/src/main/java/gorae/backend/entity/CheckoutOrder.java @@ -1,6 +1,8 @@ package gorae.backend.entity; import gorae.backend.constant.CheckoutOrderStatus; +import gorae.backend.constant.TicketStatus; +import gorae.backend.dto.checkout.CheckoutOrderDto; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -38,6 +40,17 @@ public class CheckoutOrder extends BaseEntity { private Instant paymentDate; + public CheckoutOrderDto toDto() { + return CheckoutOrderDto.builder() + .id(getPublicId()) + .product(product.toSummaryDto()) + .currentCount(getCurrentTicketCount()) + .paymentDate(paymentDate) + .endDate(tickets.isEmpty() ? null : tickets.getFirst().getEndTime()) + .status(status) + .build(); + } + public void completeOrder(List tickets) { status = CheckoutOrderStatus.COMPLETED; paymentDate = Instant.now(); @@ -51,4 +64,9 @@ public void completeOrder(List tickets) { public void updateStatus(CheckoutOrderStatus status) { this.status = status; } + + public int getCurrentTicketCount() { + return Math.toIntExact(tickets.stream().filter( + ticket -> ticket.getStatus() == TicketStatus.ACTIVE).count()); + } } diff --git a/src/main/java/gorae/backend/entity/Product.java b/src/main/java/gorae/backend/entity/Product.java index a9b88f8..db0d479 100644 --- a/src/main/java/gorae/backend/entity/Product.java +++ b/src/main/java/gorae/backend/entity/Product.java @@ -3,6 +3,7 @@ import gorae.backend.common.DiscountCalculator; import gorae.backend.constant.ProductName; import gorae.backend.dto.product.ProductDto; +import gorae.backend.dto.product.ProductSummaryDto; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -60,4 +61,19 @@ public ProductDto toDto() { .lessonRatio(lessonRatio) .build(); } + + public ProductSummaryDto toSummaryDto() { + return ProductSummaryDto.builder() + .id(getPublicId()) + .name(name.getKey()) + .price(price) + .discountedPrice(discountedPrice) + .discountPercentage(DiscountCalculator.getDiscountPercentage(price, discountedPrice)) + .discountAmount(DiscountCalculator.getDiscountAmount(price, discountedPrice)) + .currencyCode(currencyCode) + .count(count) + .hasDiscount(DiscountCalculator.getHasDiscount(price, discountedPrice)) + .lessonRatio(lessonRatio) + .build(); + } } diff --git a/src/main/java/gorae/backend/repository/CheckoutOrderRepository.java b/src/main/java/gorae/backend/repository/CheckoutOrderRepository.java index e1eacac..d365dcc 100644 --- a/src/main/java/gorae/backend/repository/CheckoutOrderRepository.java +++ b/src/main/java/gorae/backend/repository/CheckoutOrderRepository.java @@ -5,7 +5,9 @@ import gorae.backend.entity.Product; import gorae.backend.entity.Student; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import java.util.List; import java.util.Optional; public interface CheckoutOrderRepository extends JpaRepository { @@ -14,4 +16,7 @@ public interface CheckoutOrderRepository extends JpaRepository findByOrderIdAndStudent_Id(String orderId, Long studentId); Optional findByStudentAndProductAndStatus(Student student, Product product, CheckoutOrderStatus status); + + @Query("select c from CheckoutOrder c where c.student.id = :studentId and c.status in ('COMPLETED', 'FAILED')") + List findByStudent_Id(Long studentId); } diff --git a/src/main/java/gorae/backend/service/checkout/CheckoutService.java b/src/main/java/gorae/backend/service/checkout/CheckoutService.java index d9a5e10..7f3166b 100644 --- a/src/main/java/gorae/backend/service/checkout/CheckoutService.java +++ b/src/main/java/gorae/backend/service/checkout/CheckoutService.java @@ -5,6 +5,7 @@ import gorae.backend.constant.paypal.PaypalOrderStatus; import gorae.backend.constant.paypal.PaypalOrderIntent; import gorae.backend.constant.ProductName; +import gorae.backend.dto.checkout.CheckoutOrderDto; import gorae.backend.dto.checkout.CheckoutRequestDto; import gorae.backend.dto.client.paypal.CreateOrderRequestDto; import gorae.backend.dto.client.paypal.OrderDto; @@ -180,4 +181,11 @@ public void approveCheckout(String userId, String orderId) throws Exception { log.info("[Service] ApproveCheckout succeeded for member: {}, order: {}", studentPublicId, orderId); } + + @Transactional(readOnly = true) + public List getOrders(String userId) { + long studentId = Long.parseLong(userId); + return checkoutOrderRepository.findByStudent_Id(studentId) + .stream().map(CheckoutOrder::toDto).toList(); + } } diff --git a/src/main/resources/init/05-other-data.sql b/src/main/resources/init/05-other-data.sql index 7bbb13a..e938cd0 100644 --- a/src/main/resources/init/05-other-data.sql +++ b/src/main/resources/init/05-other-data.sql @@ -4,24 +4,24 @@ VALUES -- 지난 달 티켓 (USED) ( gen_random_uuid(), 8900003, date_trunc('month', NOW() - interval '1 month') , date_trunc('month', NOW()) - interval '1 second', 'USED' -, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 4), +, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 7), ( gen_random_uuid(), 8900003, date_trunc('month', NOW() - interval '1 month') , date_trunc('month', NOW()) - interval '1 second', 'USED' -, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 4), +, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 7), -- -- 이번 달 티켓 (ACTIVE) ( gen_random_uuid(), 8900003, date_trunc('month', NOW()) , date_trunc('month', NOW() + interval '1 month') - interval '1 second', 'ACTIVE' -, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 2), +, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 7), ( gen_random_uuid(), 8900003, date_trunc('month', NOW()) , date_trunc('month', NOW() + interval '1 month') - interval '1 second', 'ACTIVE' -, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 2), +, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 7), ( gen_random_uuid(), 8900003, date_trunc('month', NOW()) , date_trunc('month', NOW() + interval '1 month') - interval '1 second', 'ACTIVE' -, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 4), +, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 7), ( gen_random_uuid(), 8900003, date_trunc('month', NOW()) , date_trunc('month', NOW() + interval '1 month') - interval '1 second', 'ACTIVE' -, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 4); +, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 7); INSERT INTO enrollment (public_id, student_id, course_id, ticket_id, enrolled_at, status) VALUES (gen_random_uuid(), 8900003, 1, 1, NOW() - interval '7 day', 'ENROLLED'), From 7978f07b1551a0a7267b537503e65d88bbf3ff36 Mon Sep 17 00:00:00 2001 From: minbros Date: Thu, 25 Sep 2025 13:18:34 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Feat:=20=EC=A3=BC=EB=AC=B8=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gorae/backend/repository/CheckoutOrderRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/gorae/backend/repository/CheckoutOrderRepository.java b/src/main/java/gorae/backend/repository/CheckoutOrderRepository.java index d365dcc..9968682 100644 --- a/src/main/java/gorae/backend/repository/CheckoutOrderRepository.java +++ b/src/main/java/gorae/backend/repository/CheckoutOrderRepository.java @@ -17,6 +17,6 @@ public interface CheckoutOrderRepository extends JpaRepository findByStudentAndProductAndStatus(Student student, Product product, CheckoutOrderStatus status); - @Query("select c from CheckoutOrder c where c.student.id = :studentId and c.status in ('COMPLETED', 'FAILED')") + @Query("select c from CheckoutOrder c where c.student.id = :studentId and c.status = 'COMPLETED'") List findByStudent_Id(Long studentId); } From 8f0f8a356b2a5471686c75a3ba37ea61d0539195 Mon Sep 17 00:00:00 2001 From: minbros Date: Thu, 25 Sep 2025 19:12:17 +0900 Subject: [PATCH 6/6] =?UTF-8?q?Fix:=20=EA=B2=B0=EC=A0=9C=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/client/paypal/PaymentSource.java | 2 +- .../dto/client/paypal/PurchaseUnit.java | 37 ++++++++++++++++++- .../java/gorae/backend/entity/Student.java | 6 +-- .../service/checkout/CheckoutService.java | 6 +-- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/main/java/gorae/backend/dto/client/paypal/PaymentSource.java b/src/main/java/gorae/backend/dto/client/paypal/PaymentSource.java index 8ab5f0b..78a9fd3 100644 --- a/src/main/java/gorae/backend/dto/client/paypal/PaymentSource.java +++ b/src/main/java/gorae/backend/dto/client/paypal/PaymentSource.java @@ -20,7 +20,7 @@ public record ExperienceContext( public enum ShippingPreference { GET_FROM_FILE, NO_SHIPPING, - SET_PROVIDED, ADDRESS + SET_PROVIDED_ADDRESS } public enum LandingPage { diff --git a/src/main/java/gorae/backend/dto/client/paypal/PurchaseUnit.java b/src/main/java/gorae/backend/dto/client/paypal/PurchaseUnit.java index 5cf92fb..b6da280 100644 --- a/src/main/java/gorae/backend/dto/client/paypal/PurchaseUnit.java +++ b/src/main/java/gorae/backend/dto/client/paypal/PurchaseUnit.java @@ -1,8 +1,41 @@ package gorae.backend.dto.client.paypal; import com.fasterxml.jackson.annotation.JsonProperty; +import gorae.backend.constant.ProductName; +import gorae.backend.entity.Product; -public record PurchaseUnit(Amount amount) { - public record Amount(@JsonProperty("currency_code") String currencyCode, String value) { +import java.util.List; + +public record PurchaseUnit(List items, Amount amount) { + public static PurchaseUnit createPurchaseUnit(Product product) { + String currencyCode = product.getCurrencyCode(); + String value = String.valueOf(product.getDiscountedPrice()); + Amount unitAmount = new Amount(currencyCode, value, null); + + ProductName productName = product.getName(); + String name = productName.getKey() + " Voucher"; + String description; + if (productName == ProductName.FIRST_CLASS) { + description = "First Class - valid until 10/09"; + } else { + description = String.format("%s - %d days validity", productName.getKey(), product.getDuration()); + } + + return new PurchaseUnit( + List.of(new Item(name, description, 1, unitAmount)), + Amount.createTotalAmount(currencyCode, value) + ); + } + + public record Item(String name, String description, int quantity, @JsonProperty("unit_amount") Amount unitAmount) { + } + + public record Amount(@JsonProperty("currency_code") String currencyCode, String value, Breakdown breakdown) { + public static Amount createTotalAmount(String currencyCode, String value) { + return new Amount(currencyCode, value, new Breakdown(new Amount(currencyCode, value, null))); + } + + public record Breakdown(@JsonProperty("item_total") Amount total) { + } } } \ No newline at end of file diff --git a/src/main/java/gorae/backend/entity/Student.java b/src/main/java/gorae/backend/entity/Student.java index 242a054..4fb0336 100644 --- a/src/main/java/gorae/backend/entity/Student.java +++ b/src/main/java/gorae/backend/entity/Student.java @@ -43,9 +43,9 @@ public class Student extends Member { public Ticket addFirstTicket() { Instant now = Instant.now(); - ZoneId thailandZone = ZoneId.of("Asia/Bangkok"); - LocalDateTime endOfDay = LocalDateTime.of(2025, 9, 28, 23, 59, 59); - Instant endTime = endOfDay.atZone(thailandZone).toInstant(); + ZoneId indonesiaZone = ZoneId.of("Asia/Jakarta"); + LocalDateTime endOfDay = LocalDateTime.of(2025, 10, 9, 23, 59, 59); + Instant endTime = endOfDay.atZone(indonesiaZone).toInstant(); Ticket ticket = Ticket.builder() .student(this) diff --git a/src/main/java/gorae/backend/service/checkout/CheckoutService.java b/src/main/java/gorae/backend/service/checkout/CheckoutService.java index 7f3166b..6f3fbfe 100644 --- a/src/main/java/gorae/backend/service/checkout/CheckoutService.java +++ b/src/main/java/gorae/backend/service/checkout/CheckoutService.java @@ -110,13 +110,13 @@ private CreateOrderRequestDto getCreateOrderRequestDto(Product product) { PaymentSource.PaypalPaymentSource.ExperienceContext.builder() .returnUrl(frontendUrl + "/approve") .cancelUrl(frontendUrl + "/pricing") + .shippingPreference(PaymentSource.PaypalPaymentSource.ExperienceContext.ShippingPreference.NO_SHIPPING) .userAction(PaymentSource.PaypalPaymentSource.ExperienceContext.UserAction.PAY_NOW) - .landingPage(PaymentSource.PaypalPaymentSource.ExperienceContext.LandingPage.LOGIN) + .landingPage(PaymentSource.PaypalPaymentSource.ExperienceContext.LandingPage.NO_PREFERENCE) .build() ); - PurchaseUnit.Amount amount = new PurchaseUnit.Amount(product.getCurrencyCode(), product.getDiscountedPrice().toString()); - PurchaseUnit purchaseUnit = new PurchaseUnit(amount); + PurchaseUnit purchaseUnit = PurchaseUnit.createPurchaseUnit(product); return CreateOrderRequestDto.builder() .intent(PaypalOrderIntent.CAPTURE) .purchaseUnits(List.of(purchaseUnit))