diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index fe01391..3977683 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,6 +12,7 @@ This version adds more utilities. ### ✨ Features * `BasicProduct` is a new, lightweight product struct. +* `Product` has new ways of calculating yearly savings. diff --git a/Sources/StoreKitPlus/Products/Product+YearlySaving.swift b/Sources/StoreKitPlus/Products/Product+YearlySaving.swift new file mode 100644 index 0000000..2c9c11b --- /dev/null +++ b/Sources/StoreKitPlus/Products/Product+YearlySaving.swift @@ -0,0 +1,77 @@ +// +// Product+YearlySaving.swift +// StoreKitPlus +// +// Created by Daniel Saidi on 2024-12-04. +// Copyright © 2024 Daniel Saidi. All rights reserved. +// + +import StoreKit + +public extension Product { + + /// Get a yearly savings in percentages when comparing a + /// yearly product with a monthy one. + /// + /// - Returns: A raw 0-1 percentage. + static func yearlySavingsPercentage( + forYearlyProduct yearly: Product, + comparedToMonthlyProduct monthly: Product + ) -> Decimal? { + yearlySavingsPercentage( + forYearlyPrice: yearly.price, + comparedToMonthlyPrice: monthly.price + ) + } + + /// Get a yearly savings in percentages when comparing a + /// yearly price with a monthy one. + /// + /// - Returns: A raw 0-1 percentage. + static func yearlySavingsPercentage( + forYearlyPrice yearly: Decimal, + comparedToMonthlyPrice monthly: Decimal + ) -> Decimal? { + guard yearly > 0 else { return nil } + guard monthly > 0 else { return nil } + let percentage = 1 - (yearly / (12 * monthly)) + return percentage + } + + /// Get a yearly savings in percentages when comparing a + /// yearly product with a monthy product variant. + /// + /// - Returns: A 0-100 (not 0-1) display percentage. + static func yearlySavingsDisplayPercentage( + forYearlyProduct yearly: Product, + comparedToMonthlyProduct monthly: Product + ) -> Int? { + yearlySavingsDisplayPercentage( + forYearlyPrice: yearly.price, + comparedToMonthlyPrice: monthly.price + ) + } + + /// Get a yearly savings in percentages when comparing a + /// yearly product with a monthy product variant. + /// + /// - Returns: A 0-100 (not 0-1) display percentage. + static func yearlySavingsDisplayPercentage( + forYearlyPrice yearly: Decimal, + comparedToMonthlyPrice monthly: Decimal + ) -> Int? { + yearlySavingsPercentage( + forYearlyPrice: yearly, + comparedToMonthlyPrice: monthly + )?.asDisplayPercentageValue() + } +} + +private extension Decimal { + + func asDisplayPercentageValue() -> Int { + let number = self as NSNumber + let result = 100 * Double(truncating: number) + return Int(result.rounded()) + } +} diff --git a/Tests/StoreKitPlusTests/Products/Product+YearlySavingTests.swift b/Tests/StoreKitPlusTests/Products/Product+YearlySavingTests.swift new file mode 100644 index 0000000..f30b377 --- /dev/null +++ b/Tests/StoreKitPlusTests/Products/Product+YearlySavingTests.swift @@ -0,0 +1,41 @@ +import XCTest +import StoreKit +import StoreKitPlus + +final class Product_YearlySavingTests: XCTestCase { + + func testCalculatingYearlySavingsPercentageRequiresPositivePrices() throws { + let result1 = Product.yearlySavingsPercentage( + forYearlyPrice: -1, + comparedToMonthlyPrice: 10 + ) + let result2 = Product.yearlySavingsPercentage( + forYearlyPrice: 100, + comparedToMonthlyPrice: -1 + ) + XCTAssertNil(result1) + XCTAssertNil(result2) + } + + func testCanCalculateYearlySavingsPercentage() throws { + let percentage = Product.yearlySavingsPercentage( + forYearlyPrice: 60, + comparedToMonthlyPrice: 10 // 120 + ) + XCTAssertEqual(percentage, 0.5) + } + + func testCanCalculateYearlySavingsDisplayPercentage() throws { + let percentage = Product.yearlySavingsDisplayPercentage( + forYearlyPrice: 100, + comparedToMonthlyPrice: 10 // 120 + ) + XCTAssertEqual(percentage, 17) + } +} + +private struct TestTransaction: ValidatableTransaction { + + let expirationDate: Date? + let revocationDate: Date? +}