Created
December 13, 2020 04:55
-
-
Save todokr/db153eeba6c908fb415d4e7294cfa8eb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.Date | |
def main(args: Array[String]): Unit = { | |
implicit val discountPolicy: DiscountPolicy = new ComplexDiscountPolicy | |
val price = Price(1000) | |
val quantity = Quantity(25) | |
println(s"price: $price") | |
println(s"quantity: $quantity") | |
val sales = SalesOrder( | |
itemId = ItemId("special"), | |
customer = Customer( | |
customerName = "todokr", | |
customerRank = CustomerRank.Loyal | |
), | |
unitPrice = price, | |
quantity = quantity, | |
purchaseType = PurchaseType.Onetime | |
) | |
val subtotal = sales.subtotal | |
val discountInfo = sales.subtotal.discounts.map(_.toString).mkString(" ", "\n ", "") | |
println(s"applied discounts:") | |
println(discountInfo) | |
println(s"subtotal: ${subtotal.amount}") | |
} | |
opaque type ItemId = String | |
object ItemId: | |
def apply(value: String): ItemId = value | |
opaque type Price = Int | |
object Price: | |
def apply(value: Int): Price = value | |
opaque type Quantity = Int | |
object Quantity: | |
def apply(value: Int): Quantity = value | |
extension (q: Quantity): | |
def > (other: Quantity): Boolean = q > other | |
def * (price: Price): Amount = q * price | |
def withFixedDiscount(fixed: Fixed): Fixed = Fixed(q * fixed.value) | |
opaque type Amount = Int | |
extension (a: Amount): | |
def applyDiscount(discounts: Seq[Discount]): Amount = { | |
val (p, f) = | |
discounts.foldLeft((Percentage.Zero, Fixed.Zero)){ | |
case ((accP, accF), discount) => | |
discount.discountType match { | |
case p: Percentage => (accP + p, accF) | |
case f: Fixed => (accP, accF + f) | |
} | |
} | |
val fixedDiscounted = a - f.value | |
(fixedDiscounted - (fixedDiscounted * (p.value / 100))).round.toInt | |
} | |
final case class DiscountAppliedSubtotal(amount: Amount, discounts: Seq[Discount]) | |
enum Discount( | |
val name: String, | |
val discountType: DiscountType, | |
val priority: Int, | |
): | |
case CampaignItemDiscount(fixedDiscount: Fixed, quantity: Quantity) extends Discount( | |
name = "Camplaign Item Discount", | |
discountType = quantity.withFixedDiscount(fixedDiscount), | |
priority = 100, | |
) | |
case LoyalCustomerDiscount() extends Discount( | |
name = "Loyal Customer Discount", | |
discountType = Percentage(15), | |
priority = 90, | |
) | |
case AmountDiscount(discountPercentage: Percentage) extends Discount( | |
name = "Amount Discount", | |
discountType = discountPercentage, | |
priority = 80, | |
) | |
case SubscriptionDiscount() extends Discount( | |
name = "Subscription Discount", | |
discountType = Percentage(10), | |
priority = 70, | |
) | |
def mergeable(other: Discount): Boolean = (this, other) match { | |
case (_: AmountDiscount, _: CampaignItemDiscount) => false | |
case _ => true | |
} | |
override def toString(): String = s"$name: $discountType" | |
sealed trait DiscountType { | |
override def toString: String | |
} | |
case class Percentage(value: Double) extends DiscountType: | |
def +(other: Percentage): Percentage = Percentage(value + other.value) | |
override def toString: String = s"-$value%" | |
object Percentage: | |
val Zero: Percentage = Percentage(0.toDouble) | |
case class Fixed(value: Int) extends DiscountType: | |
def +(other: Fixed): Fixed = Fixed(value + other.value) | |
override def toString: String = s"-$value" | |
object Fixed: | |
val Zero: Fixed = Fixed(0) | |
enum CustomerRank: | |
case Member | |
case Loyal | |
case class Customer( | |
customerName: String, | |
customerRank: CustomerRank | |
) | |
enum PurchaseType: | |
case Subscription(endDate: Date) | |
case Onetime | |
trait DiscountPolicy: | |
def appliableDiscounts(order: SalesOrder): Seq[Discount] | |
class ComplexDiscountPolicy extends DiscountPolicy: | |
import Discount._ | |
import scala.util.chaining._ | |
// encupcelize logic | |
def appliableDiscounts(order: SalesOrder): Seq[Discount] = { | |
val campaignItemDiscount = | |
if(order.itemId == ItemId("special")) CampaignItemDiscount(Fixed(200), order.quantity).pipe(Some(_)) | |
else None | |
val loyalCustomerDiscount = | |
order.customer.customerRank match { | |
case CustomerRank.Loyal => LoyalCustomerDiscount().pipe(Some(_)) | |
case _ => None | |
} | |
val amountDiscount = | |
if (order.quantity > 10) AmountDiscount(Percentage(5)).pipe(Some(_)) | |
else if (order.quantity > 5) AmountDiscount(Percentage(3)).pipe(Some(_)) | |
else None | |
val subscriptionDiscount = | |
order.purchaseType match { | |
case PurchaseType.Subscription => SubscriptionDiscount().pipe(Some(_)) | |
case _ => None | |
} | |
Seq( | |
campaignItemDiscount, | |
loyalCustomerDiscount, | |
amountDiscount, | |
subscriptionDiscount) | |
.collect { case Some(d) => d } | |
.sortBy(_.priority).reverse | |
.foldLeft(Seq.empty){ (acc: Seq[Discount], d: Discount) => | |
if(acc.forall(d.mergeable)) acc :+ d | |
else acc | |
} | |
} | |
class QuantityBasedDiscountPolicy extends DiscountPolicy: | |
import Discount._ | |
def appliableDiscounts(order: SalesOrder): Seq[Discount] = | |
if(order.quantity > 5) Seq(AmountDiscount(Percentage(3))) else Seq.empty | |
case class SalesOrder( | |
itemId: ItemId, | |
customer: Customer, | |
unitPrice: Price, | |
quantity: Quantity, | |
purchaseType: PurchaseType | |
): | |
def subtotal(implicit policy: DiscountPolicy): DiscountAppliedSubtotal = { | |
val discounts = policy.appliableDiscounts(this) | |
val amount = (quantity * unitPrice).applyDiscount(discounts) | |
DiscountAppliedSubtotal(amount, discounts) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment