@@ -0,0 +1,92 @@
+package store.model.order;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import java.util.Map;
+import java.util.stream.Collectors;
+import store.model.store.StoreRoom;
+import store.model.store.product.Product;
+
+public class Payment {
+    private final PurchaseOrder purchaseOrder;
+    private final StoreRoom storeRoom;
+    private final boolean hasMembership;
+
+    private Payment(PurchaseOrder purchaseOrder, StoreRoom storeRoom, boolean hasMembership) {
+        this.purchaseOrder = purchaseOrder;
+        this.storeRoom = storeRoom;
+        this.hasMembership = hasMembership;
+    }
+
+    public static Payment from(PurchaseOrder purchaseOrder, StoreRoom storeRoom, boolean hasMembership) {
+        return new Payment(purchaseOrder, storeRoom, hasMembership);
+    }
+
+    public long calculateActualPrice() {
+        long promotionalDiscount = calculatePromotionalDiscount();
+        long membershipDiscount = calculateMembershipDiscount();
+        long totalPrice = calculateTotalPrice();
+        return totalPrice - (promotionalDiscount + membershipDiscount);
+    }
+
+    public long calculatePromotionalDiscount() {
+        return extractPromotionalProducts().entrySet().stream()
+                .mapToLong(entry -> (long) storeRoom.getProductPrice(entry.getKey()) * entry.getValue())
+                .sum();
+    }
+
+    public long calculateMembershipDiscount() {
+        if (!hasMembership) {
+            return 0;
+        }
+        long promotionAppliedPrice = extractPromotionalProducts().entrySet().stream()
+                .mapToLong(entry -> {
+                    Product product = storeRoom.getPromotionProducts().findNullableProductByName(entry.getKey());
+                    return (long) product.getPrice() * entry.getValue() * product.getPromotionUnit();
+                }).sum();
+        long membershipDiscount = (long) ((calculateTotalPrice() - promotionAppliedPrice) * 0.3);
+        return Math.min(8000L, membershipDiscount);
+    }
+
+    public long calculateTotalPrice() {
+        return purchaseOrder.getPurchaseOrder().entrySet().stream()
+                .mapToLong(entry -> (long) storeRoom.getProductPrice(entry.getKey()) * entry.getValue())
+                .sum();
+    }
+
+    public long calculateProductPrice(String productName) {
+        int buyAmount = purchaseOrder.getPurchaseOrder().get(productName);
+        int productPrice = storeRoom.getProductPrice(productName);
+        return (long) buyAmount * productPrice;
+    }
+
+    public long calculateTotalBuyAmount() {
+        return purchaseOrder.getPurchaseOrder().values().stream()
+                .mapToLong(Long::valueOf)
+                .sum();
+    }
+
+    private int calculateGiveawayAmount(String productName, int quantity) {
+        Product product = storeRoom.getPromotionProducts().findNullableProductByName(productName);
+        if (product != null
+                && product.isPromotionPeriod(DateTimes.now())
+                && product.getStock() >= product.getPromotionUnit()) {
+            int promotionalQuantity = quantity - storeRoom.getNonPromotionalQuantity(productName, quantity);
+            return promotionalQuantity / product.getPromotionUnit();
+        }
+        return 0;
+    }
+
+    public Map<String, Integer> extractPromotionalProducts() {
+        return purchaseOrder.getPurchaseOrder().entrySet().stream()
+                .map(entry -> Map.entry(
+                        entry.getKey(),
+                        calculateGiveawayAmount(entry.getKey(), entry.getValue())
+                ))
+                .filter(entry -> entry.getValue() > 0)
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    }
+
+    public PurchaseOrder getPurchaseOrder() {
+        return purchaseOrder;
+    }
+}

리뷰: 저도 `of`, `from`메서드 네이밍에 대해서 고민을 많이했는데 어떤 블로그에는 파라미터가 2개 이상이면 `of` 하나면 `from`으로 한다는 글을 봤습니다.

다른 글에서는 정보를 조합해서 객체를 만드는 경우는 `of`라 하고 파라미터로 들어오는 정보를 이용해서 객체를 만드는 경우는 `from`이라고 네이밍한다는 글도 있었는데 저는 이 글에 더 공감을 느껴서 위 경우는 `from`이라는 메서드 네임이 잘 맞다는 생각을 했습니다.