@@ -0,0 +1,81 @@
+package store.service;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import java.util.List;
+import store.model.Item;
+import store.model.Product;
+import store.model.Products;
+import store.model.Receipt;
+import store.util.constants.ServiceConstants;
+
+public class ReceiptService {
+    public ReceiptService() {
+    }
+
+    public Receipt createReceipt(Products products, List<Item> items, boolean applyMembership) {
+        int totalPrice = getTotalPrice(products, items);
+        int discountByPromotion = getPromotionDiscount(products, items);
+        int discountByMembership = 0;
+        if (applyMembership) {
+            discountByMembership = getMembershipDiscount(products, items);
+        }
+        int payment = totalPrice - discountByPromotion - discountByMembership;
+        return new Receipt(items, totalPrice, discountByPromotion, discountByMembership, payment);
+    }
+
+    private int getTotalPrice(Products products, List<Item> items) {
+        int totalPrice = 0;
+        for (Item item : items) {
+            totalPrice += getPrice(products, item);
+        }
+        return totalPrice;
+    }
+
+    private int getPrice(Products products, Item item) {
+        Product product = products.findProductByName(item.getName());
+        return item.getQuantity() * product.getPrice();
+    }
+
+
+    public int getPromotionDiscount(Products products, List<Item> items) {
+        int discount = 0;
+        for (Item item : items) {
+            discount += getPriceForFreeByPromotion(products, item);
+        }
+        return discount;
+    }
+
+    private int getPriceForFreeByPromotion(Products products, Item item) {
+        Product product = products.findProductByNameAndPromotionIsNotNull(item.getName());
+        if (product == null || !product.isApplyPromotion(DateTimes.now())) {
+            return 0;
+        }
+        int price = product.getPrice();
+        int getFreeCount = product.getMaxCanGetForFreeByPromotion(item.getQuantity());
+        return getFreeCount * price;
+    }
+
+    public int getMembershipDiscount(Products products, List<Item> items) {
+        int totalPriceNotApplyPromotion = 0;
+        for (Item item : items) {
+            totalPriceNotApplyPromotion += getPriceNotApplyPromotion(products, item);
+        }
+        int discount = (int) (totalPriceNotApplyPromotion * ServiceConstants.MEMBERSHIP_DISCOUNT_RATE);
+        if (discount > ServiceConstants.MEMBERSHIP_DISCOUNT_LIMIT) {
+            discount = ServiceConstants.MEMBERSHIP_DISCOUNT_LIMIT;
+        }
+        return discount;
+    }
+
+    private int getPriceNotApplyPromotion(Products products, Item item) {
+        Product product = products.findProductByNameAndPromotionIsNotNull(item.getName());
+        if (product == null || !product.isApplyPromotion(DateTimes.now())) {
+            product = products.findProductByNameAndPromotionIsNull(item.getName());
+            return item.getQuantity() * product.getPrice();
+        }
+        int price = product.getPrice();
+        int applyPromotion = product.getMaxCanApplyPromotion(item.getQuantity());
+        int notApplyPromotionCount = item.getQuantity() - applyPromotion;
+        return notApplyPromotionCount * price;
+    }
+}

리뷰: 먼저 ByPromotion 메서드가 많은 이유는 타임어택 이슈로 설계를 좀 더 고민하지 못한 문제도 있지만, 제 네이밍의 문제도 있는 것 같습니다!
ByPromotion이 붙은 메서드의 경우 프로모션과 관련된 로직이지만, 해당 로직의 주체가 Promotion 자체가 아니라고 생각하여 Promotion에서 처리하지 않았습니다. 그럼에도 ByPromotion을 붙인 이유는 의미를 쉽게 파악할 수 있도록 한 것이었는데, 결과적으로 혼란이 생겼다면 좋지 못한 네이밍인 것 같습니다.
(네이밍 고민을 많이 해도 너무 어렵네요 🥲🥲)

위 메서드의 경우는 영수증에 관한 내용이라 생각해 ReceiptService에서 처리하였지만, 다시 보니 Product에 포함해도 괜찮을 것이라 생각됩니다! 좋은 피드백 감사합니다!