Skip to content

Instantly share code, notes, and snippets.

@todokr
Created December 13, 2020 04:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save todokr/db153eeba6c908fb415d4e7294cfa8eb to your computer and use it in GitHub Desktop.
Save todokr/db153eeba6c908fb415d4e7294cfa8eb to your computer and use it in GitHub Desktop.
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