Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,29 +130,29 @@ extension StoreProductDiscount {
case .day:
switch subscriptionPeriod.unit {
case .day: return 1
case .week: return 7
case .month: return 30
case .week: return Decimal(365) / Decimal(52)
case .month: return Decimal(365) / Decimal(12)
case .year: return 365
}
case .week:
switch subscriptionPeriod.unit {
case .day: return 1 / 7
case .day: return Decimal(52) / Decimal(365)
case .week: return 1
case .month: return 4
case .month: return Decimal(52) / Decimal(12)
case .year: return 52
}
case .month:
switch subscriptionPeriod.unit {
case .day: return 1 / 30
case .week: return 1 / 4
case .day: return Decimal(12) / Decimal(365)
case .week: return Decimal(12) / Decimal(52)
case .month: return 1
case .year: return 12
}
case .year:
switch subscriptionPeriod.unit {
case .day: return 1 / 365
case .week: return 1 / 52
case .month: return 1 / 12
case .day: return Decimal(1) / Decimal(365)
case .week: return Decimal(1) / Decimal(52)
case .month: return Decimal(1) / Decimal(12)
case .year: return 1
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ struct SK2StoreProduct: StoreProductType {
return "\(periodDays)"
}

private func roundedPrice(_ amount: Decimal) -> Decimal {
(amount as NSDecimalNumber)
.rounding(accordingToBehavior: SubscriptionPeriod.roundingBehavior) as Decimal
}

var dailyPrice: String {
guard let subscriptionPeriod = underlyingSK2Product.subscription?.subscriptionPeriod else {
return "n/a"
Expand All @@ -264,16 +269,16 @@ struct SK2StoreProduct: StoreProductType {
case .year:
periods = Decimal(365 * numberOfUnits)
case .month:
periods = Decimal(30 * numberOfUnits)
periods = Decimal(365) / Decimal(12) * Decimal(numberOfUnits)
case .week:
periods = Decimal(7 * numberOfUnits)
periods = Decimal(365) / Decimal(52) * Decimal(numberOfUnits)
case .day:
periods = Decimal(numberOfUnits)
@unknown default:
periods = Decimal(numberOfUnits)
}

return (inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle)
return roundedPrice(inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle)
}

var weeklyPrice: String {
Expand All @@ -289,16 +294,16 @@ struct SK2StoreProduct: StoreProductType {
case .year:
periods = Decimal(52 * numberOfUnits)
case .month:
periods = Decimal(4 * numberOfUnits)
periods = Decimal(52) / Decimal(12) * Decimal(numberOfUnits)
case .week:
periods = Decimal(numberOfUnits)
case .day:
periods = Decimal(numberOfUnits) / Decimal(7)
periods = Decimal(numberOfUnits) * Decimal(52) / Decimal(365)
@unknown default:
periods = Decimal(numberOfUnits)
}

return (inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle)
return roundedPrice(inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle)
}

var monthlyPrice: String {
Expand All @@ -316,14 +321,14 @@ struct SK2StoreProduct: StoreProductType {
case .month:
periods = Decimal(1 * numberOfUnits)
case .week:
periods = Decimal(numberOfUnits) / Decimal(4)
periods = Decimal(numberOfUnits) * Decimal(12) / Decimal(52)
case .day:
periods = Decimal(numberOfUnits) / Decimal(30)
periods = Decimal(numberOfUnits) * Decimal(12) / Decimal(365)
@unknown default:
periods = Decimal(numberOfUnits)
}

return (inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle)
return roundedPrice(inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle)
}

var yearlyPrice: String {
Expand All @@ -348,7 +353,7 @@ struct SK2StoreProduct: StoreProductType {
periods = Decimal(numberOfUnits)
}

return (inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle)
return roundedPrice(inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle)
}

var hasFreeTrial: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ struct StripeProductType: StoreProductType {
return "\(periodDays)"
}

private func roundedPrice(_ amount: Decimal) -> Decimal {
(amount as NSDecimalNumber)
.rounding(accordingToBehavior: SubscriptionPeriod.roundingBehavior) as Decimal
}

var dailyPrice: String {
if price == 0.00 {
return "$0.00"
Expand All @@ -295,18 +300,19 @@ struct StripeProductType: StoreProductType {
}

if subscriptionPeriod.unit == .month {
periods = Decimal(30 * numberOfUnits)
periods = Decimal(365) / Decimal(12) * Decimal(numberOfUnits)
}

if subscriptionPeriod.unit == .week {
periods = Decimal(numberOfUnits) / Decimal(7)
periods = Decimal(365) / Decimal(52) * Decimal(numberOfUnits)
}

if subscriptionPeriod.unit == .day {
periods = Decimal(numberOfUnits) / Decimal(1)
}

return numberFormatter.string(from: NSDecimalNumber(decimal: inputPrice / periods)) ?? "n/a"
let rounded = roundedPrice(inputPrice / periods)
return numberFormatter.string(from: NSDecimalNumber(decimal: rounded)) ?? "n/a"
}

var weeklyPrice: String {
Expand All @@ -329,18 +335,19 @@ struct StripeProductType: StoreProductType {
}

if subscriptionPeriod.unit == .month {
periods = Decimal(4 * numberOfUnits)
periods = Decimal(52) / Decimal(12) * Decimal(numberOfUnits)
}

if subscriptionPeriod.unit == .week {
periods = Decimal(numberOfUnits) / Decimal(1)
}

if subscriptionPeriod.unit == .day {
periods = Decimal(numberOfUnits) / Decimal(7)
periods = Decimal(numberOfUnits) * Decimal(52) / Decimal(365)
}

return numberFormatter.string(from: NSDecimalNumber(decimal: price / periods)) ?? "n/a"
let rounded = roundedPrice(price / periods)
return numberFormatter.string(from: NSDecimalNumber(decimal: rounded)) ?? "n/a"
}

var monthlyPrice: String {
Expand Down Expand Up @@ -368,14 +375,15 @@ struct StripeProductType: StoreProductType {
}

if subscriptionPeriod.unit == .week {
periods = Decimal(numberOfUnits) / Decimal(4)
periods = Decimal(numberOfUnits) * Decimal(12) / Decimal(52)
}

if subscriptionPeriod.unit == .day {
periods = Decimal(numberOfUnits) / Decimal(30)
periods = Decimal(numberOfUnits) * Decimal(12) / Decimal(365)
}

return numberFormatter.string(from: NSDecimalNumber(decimal: price / periods)) ?? "n/a"
let rounded = roundedPrice(price / periods)
return numberFormatter.string(from: NSDecimalNumber(decimal: rounded)) ?? "n/a"
}

var yearlyPrice: String {
Expand Down Expand Up @@ -410,7 +418,8 @@ struct StripeProductType: StoreProductType {
periods = Decimal(numberOfUnits) / Decimal(365)
}

return numberFormatter.string(from: NSDecimalNumber(decimal: price / periods)) ?? "n/a"
let rounded = roundedPrice(price / periods)
return numberFormatter.string(from: NSDecimalNumber(decimal: rounded)) ?? "n/a"
}

var hasFreeTrial: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ extension SubscriptionPeriod {
let periodsPerDay: Decimal = {
switch self.unit {
case .day: return 1
case .week: return 7
case .month: return 30
case .week: return Decimal(365) / Decimal(52)
case .month: return Decimal(365) / Decimal(12)
case .year: return 365
}
}() * Decimal(value)
Expand All @@ -143,25 +143,25 @@ extension SubscriptionPeriod {
}

func pricePerWeek(withTotalPrice price: Decimal) -> Decimal {
let periodsPerDay: Decimal = {
let periodsPerWeek: Decimal = {
switch self.unit {
case .day: return 1 / 7
case .day: return Decimal(52) / Decimal(365)
case .week: return 1
case .month: return 4
case .month: return Decimal(52) / Decimal(12)
case .year: return 52
}
}() * Decimal(value)

return (price as NSDecimalNumber)
.dividing(by: periodsPerDay as NSDecimalNumber,
.dividing(by: periodsPerWeek as NSDecimalNumber,
withBehavior: Self.roundingBehavior) as Decimal
}

func pricePerMonth(withTotalPrice price: Decimal) -> Decimal {
let periodsPerMonth: Decimal = {
switch self.unit {
case .day: return 1 / 30
case .week: return 1 / 4
case .day: return Decimal(12) / Decimal(365)
case .week: return Decimal(12) / Decimal(52)
case .month: return 1
case .year: return 12
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,19 +174,25 @@ struct TestStoreProduct: StoreProductType {
return formatter
}

private func roundedPrice(_ amount: Decimal) -> Decimal {
(amount as NSDecimalNumber)
.rounding(accordingToBehavior: SubscriptionPeriod.roundingBehavior) as Decimal
}

var dailyPrice: String {
guard price != 0, let unit = subscriptionUnit else {
return priceFormatter.string(from: 0) ?? "$0.00"
}
let days: Decimal
switch unit {
case .day: days = Decimal(subscriptionValue)
case .week: days = Decimal(7 * subscriptionValue)
case .month: days = Decimal(30 * subscriptionValue)
case .week: days = Decimal(365) / Decimal(52) * Decimal(subscriptionValue)
case .month: days = Decimal(365) / Decimal(12) * Decimal(subscriptionValue)
case .year: days = Decimal(365 * subscriptionValue)
@unknown default: days = 1
}
return priceFormatter.string(from: NSDecimalNumber(decimal: price / days)) ?? "n/a"
let rounded = roundedPrice(price / days)
return priceFormatter.string(from: NSDecimalNumber(decimal: rounded)) ?? "n/a"
}

var weeklyPrice: String {
Expand All @@ -195,13 +201,14 @@ struct TestStoreProduct: StoreProductType {
}
let weeks: Decimal
switch unit {
case .day: weeks = Decimal(subscriptionValue) / 7
case .day: weeks = Decimal(subscriptionValue) * Decimal(52) / Decimal(365)
case .week: weeks = Decimal(subscriptionValue)
case .month: weeks = Decimal(4 * subscriptionValue)
case .month: weeks = Decimal(52) / Decimal(12) * Decimal(subscriptionValue)
case .year: weeks = Decimal(52 * subscriptionValue)
@unknown default: weeks = 1
}
return priceFormatter.string(from: NSDecimalNumber(decimal: price / weeks)) ?? "n/a"
let rounded = roundedPrice(price / weeks)
return priceFormatter.string(from: NSDecimalNumber(decimal: rounded)) ?? "n/a"
}

var monthlyPrice: String {
Expand All @@ -210,13 +217,14 @@ struct TestStoreProduct: StoreProductType {
}
let months: Decimal
switch unit {
case .day: months = Decimal(subscriptionValue) / 30
case .week: months = Decimal(subscriptionValue) / 4
case .day: months = Decimal(subscriptionValue) * Decimal(12) / Decimal(365)
case .week: months = Decimal(subscriptionValue) * Decimal(12) / Decimal(52)
case .month: months = Decimal(subscriptionValue)
case .year: months = Decimal(12 * subscriptionValue)
@unknown default: months = 1
}
return priceFormatter.string(from: NSDecimalNumber(decimal: price / months)) ?? "n/a"
let rounded = roundedPrice(price / months)
return priceFormatter.string(from: NSDecimalNumber(decimal: rounded)) ?? "n/a"
}

var yearlyPrice: String {
Expand All @@ -231,7 +239,8 @@ struct TestStoreProduct: StoreProductType {
case .year: years = Decimal(subscriptionValue)
@unknown default: years = 1
}
return priceFormatter.string(from: NSDecimalNumber(decimal: price / years)) ?? "n/a"
let rounded = roundedPrice(price / years)
return priceFormatter.string(from: NSDecimalNumber(decimal: rounded)) ?? "n/a"
}

// MARK: - Trial
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ struct SubscriptionPeriodPriceTests {

@Test("Daily price for monthly subscription")
func testDailyPriceForMonthlySubscription() {
// $9.99/month should be ~$0.33/day (9.99 / 30)
// $9.99/month should be ~$0.32/day ((9.99 * 12) / 365)
let period = SubscriptionPeriod(value: 1, unit: .month)
let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(9.99))

// 9.99 / 30 = 0.333 rounds down to 0.33
#expect(dailyPrice == Decimal(string: "0.33"))
// (9.99 * 12) / 365 = 0.328... rounds down to 0.32
#expect(dailyPrice == Decimal(string: "0.32"))
}

@Test("Daily price for weekly subscription")
Expand All @@ -44,7 +44,7 @@ struct SubscriptionPeriodPriceTests {
let period = SubscriptionPeriod(value: 1, unit: .week)
let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(4.99))

// 4.99 / 7 = 0.712... rounds down to 0.71
// 4.99 / (365/52) = 0.710... rounds down to 0.71
#expect(dailyPrice == Decimal(string: "0.71"))
}

Expand All @@ -63,7 +63,7 @@ struct SubscriptionPeriodPriceTests {
let period = SubscriptionPeriod(value: 3, unit: .month)
let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(24.99))

// 24.99 / 90 = 0.2776... rounds down to 0.27
// 24.99 / 91.25 = 0.2738... rounds down to 0.27
#expect(dailyPrice == Decimal(string: "0.27"))
}

Expand All @@ -79,14 +79,35 @@ struct SubscriptionPeriodPriceTests {
#expect(weeklyPrice == Decimal(string: "1.92"))
}

@Test("Yearly plan weekly and monthly prices use annualized conversion")
func testYearlyPlanAnnualizedConversion() {
let period = SubscriptionPeriod(value: 1, unit: .year)

let weeklyPrice = period.pricePerWeek(withTotalPrice: Decimal(89.99))
let monthlyPrice = period.pricePerMonth(withTotalPrice: Decimal(89.99))

#expect(weeklyPrice == Decimal(string: "1.73"))
#expect(monthlyPrice == Decimal(string: "7.49"))
}

@Test("Weekly price for monthly subscription")
func testWeeklyPriceForMonthlySubscription() {
// $9.99/month should be ~$2.49/week (9.99 / 4)
// $9.99/month should be ~$2.30/week ((9.99 * 12) / 52)
let period = SubscriptionPeriod(value: 1, unit: .month)
let weeklyPrice = period.pricePerWeek(withTotalPrice: Decimal(9.99))

// 9.99 / 4 = 2.4975 rounds down to 2.49
#expect(weeklyPrice == Decimal(string: "2.49"))
// (9.99 * 12) / 52 = 2.305... rounds down to 2.30
#expect(weeklyPrice == Decimal(string: "2.30"))
}

@Test("Weekly price annualizes monthly products before dividing by 52")
func testWeeklyPriceAnnualizedFromMonthlySubscription() {
// $29.99/month should be ~$6.92/week ((29.99 * 12) / 52)
let period = SubscriptionPeriod(value: 1, unit: .month)
let weeklyPrice = period.pricePerWeek(withTotalPrice: Decimal(29.99))

// (29.99 * 12) / 52 = 6.920... rounds down to 6.92
#expect(weeklyPrice == Decimal(string: "6.92"))
}

@Test("Weekly price for weekly subscription")
Expand Down
Loading