diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d29c842..0d031f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,18 +19,30 @@ jobs: if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/') uses: oversizedev/GithubWorkflows/.github/workflows/test.yml@main secrets: inherit - - build-swiftpm: - name: Build SwiftPM - needs: tests + + build-app-store-services: + name: Build OversizeAppStoreServices + needs: + - tests uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main with: package: OversizeAppStoreServices secrets: inherit + + build-metric-services: + name: Build OversizeMetricServices + needs: + - tests + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm-all-platforms.yml@main + with: + package: OversizeMetricServices + secrets: inherit bump: name: Bump version - needs: build-swiftpm + needs: + - build-app-store-services + - build-metric-services if: github.ref == 'refs/heads/main' uses: oversizedev/GithubWorkflows/.github/workflows/bump.yml@main secrets: inherit diff --git a/Package.swift b/Package.swift index 577d907..bae8424 100644 --- a/Package.swift +++ b/Package.swift @@ -38,11 +38,25 @@ let package = Package( name: "OversizeAppStoreServices", targets: ["OversizeAppStoreServices"] ), + .library( + name: "OversizeMetricServices", + targets: ["OversizeMetricServices"] + ), ], dependencies: dependencies, targets: [ .target( name: "OversizeAppStoreServices", + dependencies: [ + .product(name: "AppStoreConnect", package: "asc-swift"), + .product(name: "Factory", package: "Factory"), + .product(name: "OversizeCore", package: "OversizeCore"), + .product(name: "OversizeModels", package: "OversizeModels"), + .product(name: "OversizeServices", package: "OversizeServices"), + ] + ), + .target( + name: "OversizeMetricServices", dependencies: [ .product(name: "AppStoreConnect", package: "asc-swift"), .product(name: "Factory", package: "Factory"), @@ -51,11 +65,12 @@ let package = Package( .product(name: "OversizeServices", package: "OversizeServices"), .product(name: "Gzip", package: "GzipSwift"), .product(name: "CodableCSV", package: "CodableCSV"), + "OversizeAppStoreServices", ] ), .testTarget( name: "OversizeAppStoreServicesTests", - dependencies: ["OversizeAppStoreServices"] + dependencies: ["OversizeAppStoreServices", "OversizeMetricServices"] ), ] ) diff --git a/Sources/OversizeAppStoreServices/Models/AgeRatingDeclaration.swift b/Sources/OversizeAppStoreServices/Models/AgeRatingDeclaration.swift index f36be39..0366099 100644 --- a/Sources/OversizeAppStoreServices/Models/AgeRatingDeclaration.swift +++ b/Sources/OversizeAppStoreServices/Models/AgeRatingDeclaration.swift @@ -4,7 +4,6 @@ // import AppStoreAPI -import AppStoreConnect import OversizeCore public struct AgeRatingDeclaration: Sendable { diff --git a/Sources/OversizeAppStoreServices/Models/App.swift b/Sources/OversizeAppStoreServices/Models/App.swift index 42393a1..69e6015 100644 --- a/Sources/OversizeAppStoreServices/Models/App.swift +++ b/Sources/OversizeAppStoreServices/Models/App.swift @@ -4,7 +4,6 @@ // import AppStoreAPI -import AppStoreConnect import Foundation public struct App: Identifiable, Sendable { @@ -83,6 +82,8 @@ public struct App: Identifiable, Sendable { appStoreVersions.append(includedAppStoreVersion) case let .build(includedBuild): builds.append(includedBuild) + case let .prereleaseVersion(prereleaseVersion): + prereleaseVersions.append(prereleaseVersion) default: continue } @@ -135,6 +136,8 @@ public struct App: Identifiable, Sendable { appStoreVersions.append(includedAppStoreVersion) case let .build(includedBuild): builds.append(includedBuild) + case let .prereleaseVersion(prereleaseVersion): + prereleaseVersions.append(prereleaseVersion) default: continue } diff --git a/Sources/OversizeAppStoreServices/Models/AppCategory.swift b/Sources/OversizeAppStoreServices/Models/AppCategory.swift index 6f14c9b..00d257c 100644 --- a/Sources/OversizeAppStoreServices/Models/AppCategory.swift +++ b/Sources/OversizeAppStoreServices/Models/AppCategory.swift @@ -4,7 +4,7 @@ // import AppStoreAPI -import AppStoreConnect + import Foundation import OversizeCore import SwiftUI @@ -33,10 +33,10 @@ public struct AppCategory: Identifiable, Sendable { } self.included = .init(subCategories: subcategoryIds?.compactMap { subcategoryId in - if let subcategory = included.first { $0.id == subcategoryId } { - return .init(schema: subcategory) + if let subcategory = included.first(where: { $0.id == subcategoryId }) { + .init(schema: subcategory) } else { - return nil + nil } }) } diff --git a/Sources/OversizeAppStoreServices/Models/AppInfo.swift b/Sources/OversizeAppStoreServices/Models/AppInfo.swift index 3bb83b4..5b25150 100644 --- a/Sources/OversizeAppStoreServices/Models/AppInfo.swift +++ b/Sources/OversizeAppStoreServices/Models/AppInfo.swift @@ -4,7 +4,7 @@ // import AppStoreAPI -import AppStoreConnect + import OversizeCore public struct AppInfo: Sendable { diff --git a/Sources/OversizeAppStoreServices/Models/AppInfoLocalization.swift b/Sources/OversizeAppStoreServices/Models/AppInfoLocalization.swift index e6b57df..5c2aa14 100644 --- a/Sources/OversizeAppStoreServices/Models/AppInfoLocalization.swift +++ b/Sources/OversizeAppStoreServices/Models/AppInfoLocalization.swift @@ -4,7 +4,6 @@ // import AppStoreAPI -import AppStoreConnect import Foundation public struct AppInfoLocalization: Sendable, Hashable, Identifiable { diff --git a/Sources/OversizeAppStoreServices/Models/AppStoreReviewDetail.swift b/Sources/OversizeAppStoreServices/Models/AppStoreReviewDetail.swift index b4afeeb..f0971ff 100644 --- a/Sources/OversizeAppStoreServices/Models/AppStoreReviewDetail.swift +++ b/Sources/OversizeAppStoreServices/Models/AppStoreReviewDetail.swift @@ -4,7 +4,6 @@ // import AppStoreAPI -import AppStoreConnect import Foundation import OversizeCore diff --git a/Sources/OversizeAppStoreServices/Models/AppStoreVersion.swift b/Sources/OversizeAppStoreServices/Models/AppStoreVersion.swift index 033cba5..21893ae 100644 --- a/Sources/OversizeAppStoreServices/Models/AppStoreVersion.swift +++ b/Sources/OversizeAppStoreServices/Models/AppStoreVersion.swift @@ -4,7 +4,6 @@ // import AppStoreAPI -import AppStoreConnect import Foundation import OversizeCore @@ -74,10 +73,10 @@ public struct AppStoreVersion: Sendable, Identifiable { build: includedBuild.flatMap { .init(schema: $0) }, ageRatingDeclaration: includedAgeRatingDeclaration.flatMap { .init(schema: $0) }, appStoreVersionLocalizations: includedAppStoreVersionLocalizations.flatMap { localizations in - localizations.flatMap(AppStoreVersionLocalization.init) + localizations.compactMap(AppStoreVersionLocalization.init) }, appStoreReviewDetail: includedAppStoreReviewDetail.flatMap { reviewDetails in - reviewDetails.flatMap(AppStoreReviewDetail.init) + reviewDetails.compactMap(AppStoreReviewDetail.init) } ) diff --git a/Sources/OversizeAppStoreServices/Models/AppStoreVersionLocalization.swift b/Sources/OversizeAppStoreServices/Models/AppStoreVersionLocalization.swift index 7c18ea6..cce77ea 100644 --- a/Sources/OversizeAppStoreServices/Models/AppStoreVersionLocalization.swift +++ b/Sources/OversizeAppStoreServices/Models/AppStoreVersionLocalization.swift @@ -4,7 +4,7 @@ // import AppStoreAPI -import AppStoreConnect + import Foundation import OversizeCore diff --git a/Sources/OversizeAppStoreServices/Models/Build.swift b/Sources/OversizeAppStoreServices/Models/Build.swift index 5f0677a..cadab9e 100644 --- a/Sources/OversizeAppStoreServices/Models/Build.swift +++ b/Sources/OversizeAppStoreServices/Models/Build.swift @@ -4,7 +4,6 @@ // import AppStoreAPI -import AppStoreConnect import Foundation public struct Build: Sendable, Identifiable { @@ -50,7 +49,6 @@ public struct Build: Sendable, Identifiable { } else { buildAudienceType = .none } - let templateUrl = schema.attributes?.iconAssetToken?.templateURL relationships = .init( buildBundlesIds: schema.relationships?.buildBundles?.data?.compactMap { $0.id } ) diff --git a/Sources/OversizeAppStoreServices/Models/BuildBundleFileSize.swift b/Sources/OversizeAppStoreServices/Models/BuildBundleFileSize.swift index 7e663b3..c959f58 100644 --- a/Sources/OversizeAppStoreServices/Models/BuildBundleFileSize.swift +++ b/Sources/OversizeAppStoreServices/Models/BuildBundleFileSize.swift @@ -4,7 +4,7 @@ // swift-format-ignore-file import AppStoreAPI -import AppStoreConnect + import Foundation public struct BuildBundleFileSize: Sendable, Identifiable { diff --git a/Sources/OversizeAppStoreServices/Models/Certificate.swift b/Sources/OversizeAppStoreServices/Models/Certificate.swift index 781230c..05038b6 100644 --- a/Sources/OversizeAppStoreServices/Models/Certificate.swift +++ b/Sources/OversizeAppStoreServices/Models/Certificate.swift @@ -4,13 +4,13 @@ // import AppStoreAPI -import AppStoreConnect + import Foundation public struct Certificate: Sendable { public let id: String public let name: String - public let platform: BundleID.Platform + public let platform: BundleIDPlatform public let type: CertificateType public let content: String public var expirationDate: Date @@ -19,12 +19,13 @@ public struct Certificate: Sendable { guard let name = schema.attributes?.name, let type = CertificateType(rawValue: schema.attributes?.certificateType?.rawValue ?? ""), let content = schema.attributes?.certificateContent, - let platform = schema.attributes?.platform, + let platformRawValue = schema.attributes?.platform?.rawValue, + let platform: BundleIDPlatform = .init(rawValue: platformRawValue), let expirationDate = schema.attributes?.expirationDate else { return nil } id = schema.id self.name = name - self.platform = BundleID.Platform(schema: platform) + self.platform = platform self.type = type self.content = content self.expirationDate = expirationDate diff --git a/Sources/OversizeAppStoreServices/Models/CustomerReview.swift b/Sources/OversizeAppStoreServices/Models/CustomerReview.swift index 576bc28..34b8fe9 100644 --- a/Sources/OversizeAppStoreServices/Models/CustomerReview.swift +++ b/Sources/OversizeAppStoreServices/Models/CustomerReview.swift @@ -4,7 +4,7 @@ // import AppStoreAPI -import AppStoreConnect + import Foundation public struct CustomerReview: Sendable, Identifiable { diff --git a/Sources/OversizeAppStoreServices/Models/CustomerReviewResponseV1.swift b/Sources/OversizeAppStoreServices/Models/CustomerReviewResponseV1.swift index 71dc9f3..7d394e0 100644 --- a/Sources/OversizeAppStoreServices/Models/CustomerReviewResponseV1.swift +++ b/Sources/OversizeAppStoreServices/Models/CustomerReviewResponseV1.swift @@ -4,7 +4,6 @@ // import AppStoreAPI -import AppStoreConnect import Foundation public struct CustomerReviewResponseV1: Sendable, Identifiable { diff --git a/Sources/OversizeAppStoreServices/Models/ImageAsset.swift b/Sources/OversizeAppStoreServices/Models/ImageAsset.swift index 68db3dd..a81a6aa 100644 --- a/Sources/OversizeAppStoreServices/Models/ImageAsset.swift +++ b/Sources/OversizeAppStoreServices/Models/ImageAsset.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // ImageAsset.swift, created on 17.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/InAppPurchaseAvailability.swift b/Sources/OversizeAppStoreServices/Models/InAppPurchaseAvailability.swift index 07377da..17bc913 100644 --- a/Sources/OversizeAppStoreServices/Models/InAppPurchaseAvailability.swift +++ b/Sources/OversizeAppStoreServices/Models/InAppPurchaseAvailability.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchaseAvailability.swift, created on 24.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/InAppPurchaseContent.swift b/Sources/OversizeAppStoreServices/Models/InAppPurchaseContent.swift index 893916f..fa73e23 100644 --- a/Sources/OversizeAppStoreServices/Models/InAppPurchaseContent.swift +++ b/Sources/OversizeAppStoreServices/Models/InAppPurchaseContent.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchaseContent.swift, created on 17.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/InAppPurchaseImage.swift b/Sources/OversizeAppStoreServices/Models/InAppPurchaseImage.swift index e20c711..5611d61 100644 --- a/Sources/OversizeAppStoreServices/Models/InAppPurchaseImage.swift +++ b/Sources/OversizeAppStoreServices/Models/InAppPurchaseImage.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchaseImage.swift, created on 17.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/InAppPurchaseLocalization.swift b/Sources/OversizeAppStoreServices/Models/InAppPurchaseLocalization.swift index c96d51e..5f19c39 100644 --- a/Sources/OversizeAppStoreServices/Models/InAppPurchaseLocalization.swift +++ b/Sources/OversizeAppStoreServices/Models/InAppPurchaseLocalization.swift @@ -1,10 +1,10 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchaseLocalization.swift, created on 12.01.2025 // import AppStoreAPI -import AppStoreConnect + import Foundation import SwiftUI diff --git a/Sources/OversizeAppStoreServices/Models/InAppPurchasePrice.swift b/Sources/OversizeAppStoreServices/Models/InAppPurchasePrice.swift index 670111f..693edaf 100644 --- a/Sources/OversizeAppStoreServices/Models/InAppPurchasePrice.swift +++ b/Sources/OversizeAppStoreServices/Models/InAppPurchasePrice.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchasePrice.swift, created on 29.01.2025 // @@ -27,20 +27,29 @@ public struct InAppPurchasePrice: Sendable, Identifiable { territoryId: schema.relationships?.territory?.data?.id ) - self.included = .init( - inAppPurchasePricePoint: included?.compactMap { (item: InAppPurchasePricesResponse.IncludedItem) -> InAppPurchasePricePoint? in - if case let .inAppPurchasePricePoint(value) = item { - return .init(schema: value) - } - return nil - }.first, - territory: included?.compactMap { (item: InAppPurchasePricesResponse.IncludedItem) -> Territory? in - if case let .territory(value) = item { - return .init(schema: value) + if let includedItems = included { + var inAppPurchasePricePoint: InAppPurchasePricePoint? + var territory: Territory? + + for includedItem in includedItems { + switch includedItem { + case let .inAppPurchasePricePoint(value): + if schema.relationships?.inAppPurchasePricePoint?.data?.id == value.id { + inAppPurchasePricePoint = .init(schema: value) + } + case let .territory(value): + if schema.relationships?.territory?.data?.id == value.id { + territory = .init(schema: value) + } } - return nil - }.first - ) + } + self.included = .init( + inAppPurchasePricePoint: inAppPurchasePricePoint, + territory: territory + ) + } else { + self.included = nil + } } public struct Relationships: Sendable { diff --git a/Sources/OversizeAppStoreServices/Models/InAppPurchasePricePoint.swift b/Sources/OversizeAppStoreServices/Models/InAppPurchasePricePoint.swift index c9ec1df..303cdb9 100644 --- a/Sources/OversizeAppStoreServices/Models/InAppPurchasePricePoint.swift +++ b/Sources/OversizeAppStoreServices/Models/InAppPurchasePricePoint.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchasePricePoint.swift, created on 17.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/InAppPurchasePriceSchedule.swift b/Sources/OversizeAppStoreServices/Models/InAppPurchasePriceSchedule.swift index 7b1ad7c..f9a1836 100644 --- a/Sources/OversizeAppStoreServices/Models/InAppPurchasePriceSchedule.swift +++ b/Sources/OversizeAppStoreServices/Models/InAppPurchasePriceSchedule.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchasePriceSchedule.swift, created on 17.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/InAppPurchaseV2.swift b/Sources/OversizeAppStoreServices/Models/InAppPurchaseV2.swift index ffeceb4..ae3ee82 100644 --- a/Sources/OversizeAppStoreServices/Models/InAppPurchaseV2.swift +++ b/Sources/OversizeAppStoreServices/Models/InAppPurchaseV2.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchaseV2.swift, created on 02.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/IntegerRange.swift b/Sources/OversizeAppStoreServices/Models/IntegerRange.swift new file mode 100644 index 0000000..249698e --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/IntegerRange.swift @@ -0,0 +1,51 @@ +// +// Copyright © 2025 Alexander Romanov +// IntegerRange.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation +import OversizeCore + +public struct IntegerRange: Sendable { + public let minimum: Int? + public let maximum: Int? + + public init(schema: AppStoreAPI.IntegerRange) { + minimum = schema.minimum + maximum = schema.maximum + } + + public init( + minimum: Int? = nil, + maximum: Int? = nil + ) { + self.minimum = minimum + self.maximum = maximum + } +} + +// MARK: - CustomStringConvertible + +extension IntegerRange: CustomStringConvertible { + public var description: String { + switch (minimum, maximum) { + case let (min?, max?): + "\(min)-\(max)" + case let (min?, nil): + "≥\(min)" + case let (nil, max?): + "≤\(max)" + case (nil, nil): + "undefined" + } + } +} + +// MARK: - Equatable + +extension IntegerRange: Equatable { + public static func == (lhs: IntegerRange, rhs: IntegerRange) -> Bool { + lhs.minimum == rhs.minimum && lhs.maximum == rhs.maximum + } +} diff --git a/Sources/OversizeAppStoreServices/Models/PrereleaseVersion.swift b/Sources/OversizeAppStoreServices/Models/PrereleaseVersion.swift index d04674d..a804b37 100644 --- a/Sources/OversizeAppStoreServices/Models/PrereleaseVersion.swift +++ b/Sources/OversizeAppStoreServices/Models/PrereleaseVersion.swift @@ -4,7 +4,7 @@ // import AppStoreAPI -import AppStoreConnect + import OversizeCore public struct PrereleaseVersion: Sendable { diff --git a/Sources/OversizeAppStoreServices/Models/Profile.swift b/Sources/OversizeAppStoreServices/Models/Profile.swift index be0bc39..edbba1a 100644 --- a/Sources/OversizeAppStoreServices/Models/Profile.swift +++ b/Sources/OversizeAppStoreServices/Models/Profile.swift @@ -4,11 +4,10 @@ // import AppStoreAPI -import AppStoreConnect public struct Profile: Sendable { public let name: String - public let platform: BundleID.Platform + public let platform: BundleIDPlatform public let content: String public let isActive: Bool @@ -16,11 +15,12 @@ public struct Profile: Sendable { guard let name = schema.attributes?.name, let content = schema.attributes?.profileContent, let state = schema.attributes?.profileState, - let platform = schema.attributes?.platform + let platformRawValue = schema.attributes?.platform?.rawValue, + let platform: BundleIDPlatform = .init(rawValue: platformRawValue) else { return nil } self.name = name - self.platform = BundleID.Platform(schema: platform) + self.platform = platform self.content = content isActive = state == .active } diff --git a/Sources/OversizeAppStoreServices/Models/PromotedPurchase.swift b/Sources/OversizeAppStoreServices/Models/PromotedPurchase.swift index 0c13853..c64b931 100644 --- a/Sources/OversizeAppStoreServices/Models/PromotedPurchase.swift +++ b/Sources/OversizeAppStoreServices/Models/PromotedPurchase.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // PromotedPurchase.swift, created on 17.01.2025 // @@ -11,9 +11,6 @@ public struct PromotedPurchase: Codable, Equatable, Identifiable, Sendable { public let isVisibleForAllUsers: Bool? public let isEnabled: Bool? public let state: State? - public let inAppPurchaseV2Id: String? - public let subscriptionId: String? - public let promotionImageIds: [String]? public init?(schema: AppStoreAPI.PromotedPurchase) { guard let attributes = schema.attributes else { return nil } @@ -21,9 +18,6 @@ public struct PromotedPurchase: Codable, Equatable, Identifiable, Sendable { isVisibleForAllUsers = attributes.isVisibleForAllUsers isEnabled = attributes.isEnabled state = State(rawValue: attributes.state?.rawValue ?? "") - inAppPurchaseV2Id = schema.relationships?.inAppPurchaseV2?.data?.id - subscriptionId = schema.relationships?.subscription?.data?.id - promotionImageIds = schema.relationships?.promotionImages?.data?.map { $0.id } } public enum State: String, CaseIterable, Codable, Sendable { diff --git a/Sources/OversizeAppStoreServices/Models/Subscription.swift b/Sources/OversizeAppStoreServices/Models/Subscription.swift index 1041beb..44737bc 100644 --- a/Sources/OversizeAppStoreServices/Models/Subscription.swift +++ b/Sources/OversizeAppStoreServices/Models/Subscription.swift @@ -4,36 +4,57 @@ // import AppStoreAPI -import AppStoreConnect + import Foundation import OversizeCore import SwiftUI -public struct Subscription: Sendable, Hashable, Identifiable { +public struct Subscription: Sendable, Identifiable { public let id: String public let name: String public let productID: String public let isFamilySharable: Bool? public let state: State - public let subscriptionPeriod: SubscriptionPeriod - public let reviewNote: String? + public let subscriptionPeriod: SubscriptionPeriod? + public let reviewNote: String public let groupLevel: Int? - public init?(schema: AppStoreAPI.Subscription) { + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.Subscription, included: [SubscriptionResponse.IncludedItem]? = nil) { guard let attributes = schema.attributes, let stateRawValue = schema.attributes?.state?.rawValue, - let state: State = .init(rawValue: stateRawValue), - let subscriptionPeriodValue = schema.attributes?.subscriptionPeriod?.rawValue, - let subscriptionPeriod: SubscriptionPeriod = .init(rawValue: subscriptionPeriodValue) + let state: State = .init(rawValue: stateRawValue) else { return nil } self.state = state - self.subscriptionPeriod = subscriptionPeriod id = schema.id name = attributes.name.valueOrEmpty productID = attributes.productID.valueOrEmpty isFamilySharable = attributes.isFamilySharable - reviewNote = attributes.reviewNote + reviewNote = attributes.reviewNote.valueOrEmpty groupLevel = attributes.groupLevel + if let subscriptionPeriod = attributes.subscriptionPeriod?.rawValue { + self.subscriptionPeriod = .init(rawValue: subscriptionPeriod) + } else { + subscriptionPeriod = nil + } + + relationships = Relationships( + subscriptionLocalizationsIds: schema.relationships?.subscriptionLocalizations?.data?.map { $0.id }, + subscriptionPricesIds: schema.relationships?.prices?.data?.map { $0.id }, + subscriptionGroupId: schema.relationships?.group?.data?.id, + subscriptionAppStoreReviewScreenshotId: schema.relationships?.appStoreReviewScreenshot?.data?.id, + promotedPurchaseId: schema.relationships?.promotedPurchase?.data?.id, + subscriptionOfferCodesIds: schema.relationships?.offerCodes?.data?.map { $0.id }, + subscriptionAvailabilityId: schema.relationships?.subscriptionAvailability?.data?.id, + introductoryOffersIds: schema.relationships?.introductoryOffers?.data?.map { $0.id }, + promotionalOffersIds: schema.relationships?.promotionalOffers?.data?.map { $0.id }, + winBackOffersIds: schema.relationships?.winBackOffers?.data?.map { $0.id }, + imagesIds: schema.relationships?.images?.data?.map { $0.id } + ) + + self.included = .init(included: included) } public enum State: String, CaseIterable, Codable, Sendable { @@ -53,12 +74,10 @@ public struct Subscription: Sendable, Hashable, Identifiable { switch self { case .approved: .green - case .readyToSubmit, .waitingForReview, .inReview, .pendingBinaryApproval: + case .readyToSubmit, .waitingForReview, .inReview, .pendingBinaryApproval, .missingMetadata: .yellow case .developerActionNeeded, .developerRemovedFromSale, .removedFromSale, .rejected: .red - case .missingMetadata: - .gray } } @@ -99,28 +118,141 @@ public struct Subscription: Sendable, Hashable, Identifiable { } } - public enum SubscriptionPeriod: String, CaseIterable, Codable, Sendable { - case oneWeek = "ONE_WEEK" - case oneMonth = "ONE_MONTH" - case twoMonths = "TWO_MONTHS" - case threeMonths = "THREE_MONTHS" - case sixMonths = "SIX_MONTHS" - case oneYear = "ONE_YEAR" + public struct Relationships: Sendable { + public var subscriptionLocalizationsIds: [String]? + public var subscriptionPricesIds: [String]? + public var subscriptionGroupId: String? + public var subscriptionAppStoreReviewScreenshotId: String? + public var promotedPurchaseId: String? + public var subscriptionOfferCodesIds: [String]? + public var subscriptionAvailabilityId: String? + public var introductoryOffersIds: [String]? + public var promotionalOffersIds: [String]? + public var winBackOffersIds: [String]? + public var imagesIds: [String]? - public var displayName: String { - switch self { - case .oneWeek: - "One Week" - case .oneMonth: - "One Month" - case .twoMonths: - "Two Months" - case .threeMonths: - "Three Months" - case .sixMonths: - "Six Months" - case .oneYear: - "One Year" + public init( + subscriptionLocalizationsIds: [String]? = nil, + subscriptionPricesIds: [String]? = nil, + subscriptionGroupId: String? = nil, + subscriptionAppStoreReviewScreenshotId: String? = nil, + promotedPurchaseId: String? = nil, + subscriptionOfferCodesIds: [String]? = nil, + subscriptionAvailabilityId: String? = nil, + introductoryOffersIds: [String]? = nil, + promotionalOffersIds: [String]? = nil, + winBackOffersIds: [String]? = nil, + imagesIds: [String]? = nil + ) { + self.subscriptionLocalizationsIds = subscriptionLocalizationsIds + self.subscriptionPricesIds = subscriptionPricesIds + self.subscriptionGroupId = subscriptionGroupId + self.subscriptionAppStoreReviewScreenshotId = subscriptionAppStoreReviewScreenshotId + self.promotedPurchaseId = promotedPurchaseId + self.subscriptionOfferCodesIds = subscriptionOfferCodesIds + self.subscriptionAvailabilityId = subscriptionAvailabilityId + self.introductoryOffersIds = introductoryOffersIds + self.promotionalOffersIds = promotionalOffersIds + self.winBackOffersIds = winBackOffersIds + self.imagesIds = imagesIds + } + } + + public struct Included: Sendable { + public let subscriptionLocalizations: [SubscriptionLocalization]? + public let subscriptionPrices: [SubscriptionPrice]? + public let subscriptionGroup: SubscriptionGroup? + public let subscriptionAppStoreReviewScreenshot: ImageAsset? + public let promotedPurchase: PromotedPurchase? + public let subscriptionOfferCodes: [SubscriptionOfferCode]? + public let subscriptionAvailability: Bool? + public let introductoryOffers: [SubscriptionIntroductoryOffer]? + public let promotionalOffers: [SubscriptionPromotionalOffer]? + public let winBackOffers: [WinBackOffer]? + public let subscriptionImages: [SubscriptionImage]? + + public init( + subscriptionLocalizations: [SubscriptionLocalization]? = nil, + subscriptionPrices: [SubscriptionPrice]? = nil, + subscriptionGroup: SubscriptionGroup? = nil, + subscriptionAppStoreReviewScreenshot: ImageAsset? = nil, + promotedPurchase: PromotedPurchase? = nil, + subscriptionOfferCodes: [SubscriptionOfferCode]? = nil, + subscriptionAvailability: Bool? = nil, + introductoryOffers: [SubscriptionIntroductoryOffer]? = nil, + promotionalOffers: [SubscriptionPromotionalOffer]? = nil, + winBackOffers: [WinBackOffer]? = nil, + subscriptionImages: [SubscriptionImage]? = nil + ) { + self.subscriptionLocalizations = subscriptionLocalizations + self.subscriptionPrices = subscriptionPrices + self.subscriptionGroup = subscriptionGroup + self.subscriptionAppStoreReviewScreenshot = subscriptionAppStoreReviewScreenshot + self.promotedPurchase = promotedPurchase + self.subscriptionOfferCodes = subscriptionOfferCodes + self.subscriptionAvailability = subscriptionAvailability + self.introductoryOffers = introductoryOffers + self.promotionalOffers = promotionalOffers + self.winBackOffers = winBackOffers + self.subscriptionImages = subscriptionImages + } + + init?(included: [SubscriptionResponse.IncludedItem]?) { + subscriptionLocalizations = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> SubscriptionLocalization? in + if case let .subscriptionLocalization(value) = item { return .init(schema: value) } + return nil + } + + subscriptionPrices = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> SubscriptionPrice? in + if case let .subscriptionPrice(value) = item { return .init(schema: value) } + return nil + } + + subscriptionGroup = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> SubscriptionGroup? in + if case let .subscriptionGroup(value) = item { return .init(schema: value) } + return nil + }.first + + subscriptionAppStoreReviewScreenshot = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> ImageAsset? in + if case let .subscriptionAppStoreReviewScreenshot(value) = item, let imageAsset = value.attributes?.imageAsset { + return .init(schema: imageAsset) + } + return nil + }.first + + promotedPurchase = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> PromotedPurchase? in + if case let .promotedPurchase(value) = item { return .init(schema: value) } + return nil + }.first + + subscriptionOfferCodes = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> SubscriptionOfferCode? in + if case let .subscriptionOfferCode(value) = item { return .init(schema: value) } + return nil + } + + subscriptionAvailability = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> Bool? in + if case let .subscriptionAvailability(value) = item { return value.attributes?.isAvailableInNewTerritories } + return nil + }.first + + introductoryOffers = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> SubscriptionIntroductoryOffer? in + if case let .subscriptionIntroductoryOffer(value) = item { return .init(schema: value) } + return nil + } + + promotionalOffers = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> SubscriptionPromotionalOffer? in + if case let .subscriptionPromotionalOffer(value) = item { return .init(schema: value) } + return nil + } + + winBackOffers = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> WinBackOffer? in + if case let .winBackOffer(value) = item { return .init(schema: value) } + return nil + } + + subscriptionImages = included?.compactMap { (item: SubscriptionResponse.IncludedItem) -> SubscriptionImage? in + if case let .subscriptionImage(value) = item { return .init(schema: value) } + return nil } } } diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionAvailability.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionAvailability.swift new file mode 100644 index 0000000..3492389 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionAvailability.swift @@ -0,0 +1,42 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionAvailability.swift, created on 05.02.2025 +// + +import AppStoreAPI +import OversizeCore + +public struct SubscriptionAvailability: Sendable, Identifiable { + public let id: String + public let isAvailableInNewTerritories: Bool + + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionAvailability, included: [AppStoreAPI.Territory]? = nil) { + guard let isAvailableInNewTerritories = schema.attributes?.isAvailableInNewTerritories else { return nil } + id = schema.id + self.isAvailableInNewTerritories = isAvailableInNewTerritories + + if let availableTerritoriesIds = schema.relationships?.availableTerritories?.data { + relationships = .init(availableTerritoriesIds: availableTerritoriesIds.compactMap { $0.id }) + + } else { + relationships = nil + } + + if let included { + self.included = .init(territories: included.compactMap { .init(schema: $0) }) + } else { + self.included = nil + } + } + + public struct Relationships: Sendable { + public var availableTerritoriesIds: [String]? + } + + public struct Included: Sendable { + public let territories: [Territory]? + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionGroup.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionGroup.swift index ceb2596..dda0dbd 100644 --- a/Sources/OversizeAppStoreServices/Models/SubscriptionGroup.swift +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionGroup.swift @@ -1,10 +1,10 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // SubscriptionGroup.swift, created on 12.01.2025 // import AppStoreAPI -import AppStoreConnect + import Foundation import OversizeCore diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionGroupLocalization.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionGroupLocalization.swift index 4ab81f0..4485ef1 100644 --- a/Sources/OversizeAppStoreServices/Models/SubscriptionGroupLocalization.swift +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionGroupLocalization.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // SubscriptionGroupLocalization.swift, created on 12.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionImage.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionImage.swift new file mode 100644 index 0000000..0379013 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionImage.swift @@ -0,0 +1,55 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionImage.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation + +public struct SubscriptionImage: Sendable, Identifiable { + public let id: String + public let fileSize: Int? + public let fileName: String? + public let sourceFileChecksum: String? + public let assetToken: String? + public let imageAsset: ImageAsset? + public let uploadOperations: [UploadOperation]? + public let state: State? + + public let relationships: Relationships? + + public init?(schema: AppStoreAPI.SubscriptionImage) { + guard let attributes = schema.attributes else { return nil } + + id = schema.id + fileSize = attributes.fileSize + fileName = attributes.fileName + sourceFileChecksum = attributes.sourceFileChecksum + assetToken = attributes.assetToken + imageAsset = attributes.imageAsset.flatMap { ImageAsset(schema: $0) } + uploadOperations = attributes.uploadOperations?.compactMap { UploadOperation(schema: $0) } + state = attributes.state.flatMap { State(rawValue: $0.rawValue) } + + relationships = Relationships( + subscriptionId: schema.relationships?.subscription?.data?.id + ) + } + + public struct Relationships: Sendable { + public let subscriptionId: String? + + public init(subscriptionId: String?) { + self.subscriptionId = subscriptionId + } + } + + public enum State: String, CaseIterable, Codable, Sendable { + case awaitingUpload = "AWAITING_UPLOAD" + case uploadComplete = "UPLOAD_COMPLETE" + case failed = "FAILED" + case prepareForSubmission = "PREPARE_FOR_SUBMISSION" + case waitingForReview = "WAITING_FOR_REVIEW" + case approved = "APPROVED" + case rejected = "REJECTED" + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionIntroductoryOffer.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionIntroductoryOffer.swift new file mode 100644 index 0000000..285839a --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionIntroductoryOffer.swift @@ -0,0 +1,91 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionIntroductoryOffer.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation +import OversizeCore + +public struct SubscriptionIntroductoryOffer: Sendable, Identifiable { + public let id: String + public let startDate: Date? + public let endDate: Date? + public let duration: SubscriptionOfferDuration? + public let offerMode: SubscriptionOfferMode? + public let numberOfPeriods: Int? + + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionIntroductoryOffer, included: [SubscriptionIntroductoryOffersResponse.IncludedItem]? = nil) { + guard let attributes = schema.attributes else { return nil } + + id = schema.id + + startDate = attributes.startDate.valueOrEmpty.toDate() + endDate = attributes.endDate.valueOrEmpty.toDate() + duration = .init(rawValue: attributes.duration?.rawValue ?? "") + offerMode = .init(rawValue: attributes.offerMode?.rawValue ?? "") + numberOfPeriods = attributes.numberOfPeriods + + relationships = Relationships( + subscriptionId: schema.relationships?.subscription?.data?.id, + territoryId: schema.relationships?.territory?.data?.id, + subscriptionPricePointId: schema.relationships?.subscriptionPricePoint?.data?.id + ) + + self.included = .init( + subscription: included?.compactMap { item -> Subscription? in + if case let .subscription(value) = item { return .init(schema: value) } + return nil + }.first, + territory: included?.compactMap { item -> Territory? in + if case let .territory(value) = item { return .init(schema: value) } + return nil + }.first, + subscriptionPricePoint: included?.compactMap { item -> SubscriptionPricePoint? in + if case let .subscriptionPricePoint(value) = item { return .init(schema: value) } + return nil + }.first + ) + } + + public struct Relationships: Sendable { + public let subscriptionId: String? + public let territoryId: String? + public let subscriptionPricePointId: String? + + public init( + subscriptionId: String? = nil, + territoryId: String? = nil, + subscriptionPricePointId: String? = nil + ) { + self.subscriptionId = subscriptionId + self.territoryId = territoryId + self.subscriptionPricePointId = subscriptionPricePointId + } + } + + public struct Included: Sendable { + public let subscription: Subscription? + public let territory: Territory? + public let subscriptionPricePoint: SubscriptionPricePoint? + + public init( + subscription: Subscription? = nil, + territory: Territory? = nil, + subscriptionPricePoint: SubscriptionPricePoint? = nil + ) { + self.subscription = subscription + self.territory = territory + self.subscriptionPricePoint = subscriptionPricePoint + } + } +} + +extension SubscriptionIntroductoryOffer { + static func from(response: AppStoreAPI.SubscriptionIntroductoryOffersResponse) -> [SubscriptionIntroductoryOffer] { + response.data.compactMap { SubscriptionIntroductoryOffer(schema: $0, included: response.included) } + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionLocalization.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionLocalization.swift index 4ae7975..4c076db 100644 --- a/Sources/OversizeAppStoreServices/Models/SubscriptionLocalization.swift +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionLocalization.swift @@ -1,10 +1,11 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // SubscriptionLocalization.swift, created on 12.01.2025 // import AppStoreAPI import Foundation +import SwiftUI public struct SubscriptionLocalization: Codable, Equatable, Identifiable, Sendable { public let id: String @@ -32,5 +33,25 @@ public struct SubscriptionLocalization: Codable, Equatable, Identifiable, Sendab case waitingForReview = "WAITING_FOR_REVIEW" case approved = "APPROVED" case rejected = "REJECTED" + + // Computed property for display name + public var displayName: String { + switch self { + case .prepareForSubmission: "Prepare for Submission" + case .waitingForReview: "Waiting for Review" + case .approved: "Approved" + case .rejected: "Rejected" + } + } + + // Computed property for status color + public var statusColor: Color { + switch self { + case .prepareForSubmission: .gray + case .waitingForReview: .yellow + case .approved: .green + case .rejected: .red + } + } } } diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCode.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCode.swift new file mode 100644 index 0000000..658f07d --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCode.swift @@ -0,0 +1,93 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferCode.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation + +public struct SubscriptionOfferCode: Identifiable, Sendable { + public let id: String + public let name: String? + public let customerEligibilities: [SubscriptionCustomerEligibility]? + public let offerEligibility: SubscriptionOfferEligibility? + public let duration: SubscriptionOfferDuration? + public let offerMode: SubscriptionOfferMode? + public let numberOfPeriods: Int? + public let totalNumberOfCodes: Int? + public let isActive: Bool? + + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionOfferCode, included: [AppStoreAPI.SubscriptionOfferCodesResponse.IncludedItem]? = nil) { + guard let attributes = schema.attributes else { return nil } + + id = schema.id + name = attributes.name + customerEligibilities = attributes.customerEligibilities?.compactMap { .init(rawValue: $0.rawValue) } + offerEligibility = .init(rawValue: attributes.offerEligibility?.rawValue ?? "") + duration = .init(rawValue: attributes.duration?.rawValue ?? "") + offerMode = .init(rawValue: attributes.offerMode?.rawValue ?? "") + numberOfPeriods = attributes.numberOfPeriods + totalNumberOfCodes = attributes.totalNumberOfCodes + isActive = attributes.isActive + + relationships = .init( + subscriptionId: schema.relationships?.subscription?.data?.id, + oneTimeUseCodesIds: schema.relationships?.oneTimeUseCodes?.data?.compactMap { $0.id } ?? [], + customCodesIds: schema.relationships?.customCodes?.data?.compactMap { $0.id } ?? [], + pricesIds: schema.relationships?.prices?.data?.compactMap { $0.id } ?? [] + ) + + var subscriptions: [AppStoreAPI.Subscription] = [] + var oneTimeUseCodes: [AppStoreAPI.SubscriptionOfferCodeOneTimeUseCode] = [] + var customCodes: [AppStoreAPI.SubscriptionOfferCodeCustomCode] = [] + var prices: [AppStoreAPI.SubscriptionOfferCodePrice] = [] + + if let includedItems = included { + for includedItem in includedItems { + switch includedItem { + case let .subscription(subscription): + if schema.relationships?.subscription?.data?.id == subscription.id { + subscriptions.append(subscription) + } + case let .subscriptionOfferCodeOneTimeUseCode(oneTimeUseCode): + if schema.relationships?.oneTimeUseCodes?.data?.contains(where: { $0.id == oneTimeUseCode.id }) ?? false { + oneTimeUseCodes.append(oneTimeUseCode) + } + case let .subscriptionOfferCodeCustomCode(customCode): + if schema.relationships?.customCodes?.data?.contains(where: { $0.id == customCode.id }) ?? false { + customCodes.append(customCode) + } + case let .subscriptionOfferCodePrice(price): + if schema.relationships?.prices?.data?.contains(where: { $0.id == price.id }) ?? false { + prices.append(price) + } + } + } + self.included = .init( + subscriptions: subscriptions.compactMap { .init(schema: $0) }, + oneTimeUseCodes: oneTimeUseCodes.compactMap { .init(schema: $0) }, + customCodes: customCodes.compactMap { .init(schema: $0) }, + prices: prices.compactMap { .init(schema: $0) } + ) + } else { + self.included = nil + } + } + + public struct Relationships: Sendable { + public let subscriptionId: String? + public let oneTimeUseCodesIds: [String] + public let customCodesIds: [String] + public let pricesIds: [String] + } + + public struct Included: Sendable { + public let subscriptions: [Subscription]? + public let oneTimeUseCodes: [SubscriptionOfferCodeOneTimeUseCode]? + public let customCodes: [SubscriptionOfferCodeCustomCode]? + public let prices: [SubscriptionOfferCodePrice]? + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodeCustomCode.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodeCustomCode.swift new file mode 100644 index 0000000..06f2e67 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodeCustomCode.swift @@ -0,0 +1,34 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferCodeCustomCode.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation + +public struct SubscriptionOfferCodeCustomCode: Identifiable, Sendable { + public let id: String + public let customCode: String + public let numberOfCodes: Int + public let createdDate: Date? + public let expirationDate: String? + public let isActive: Bool + + public let relationships: Relationships? + + public init?(schema: AppStoreAPI.SubscriptionOfferCodeCustomCode) { + guard let attributes = schema.attributes else { return nil } + id = schema.id + customCode = attributes.customCode ?? "" + numberOfCodes = attributes.numberOfCodes ?? 0 + createdDate = attributes.createdDate + expirationDate = attributes.expirationDate + isActive = attributes.isActive ?? false + + relationships = .init(offerCodeId: schema.relationships?.offerCode?.data?.id) + } + + public struct Relationships: Sendable { + public let offerCodeId: String? + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodeOneTimeUseCode.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodeOneTimeUseCode.swift new file mode 100644 index 0000000..6f352c8 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodeOneTimeUseCode.swift @@ -0,0 +1,46 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferCodeOneTimeUseCode.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation + +public struct SubscriptionOfferCodeOneTimeUseCode: Identifiable, Sendable { + public let id: String + public let numberOfCodes: Int? + public let createdDate: Date? + public let expirationDate: String? + public let isActive: Bool? + + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionOfferCodeOneTimeUseCode, included: [AppStoreAPI.SubscriptionOfferCode]? = nil) { + guard let attributes = schema.attributes else { return nil } + + id = schema.id + numberOfCodes = attributes.numberOfCodes + createdDate = attributes.createdDate + expirationDate = attributes.expirationDate + isActive = attributes.isActive + + relationships = .init( + offerCodeId: schema.relationships?.offerCode?.data?.id + ) + + if let offerCode = included?.first(where: { $0.id == schema.relationships?.offerCode?.data?.id }) { + self.included = .init(offerCode: .init(schema: offerCode)) + } else { + self.included = nil + } + } + + public struct Relationships: Sendable { + public let offerCodeId: String? + } + + public struct Included: Sendable { + public let offerCode: SubscriptionOfferCode? + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodePrice.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodePrice.swift new file mode 100644 index 0000000..110fff4 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionOfferCodePrice.swift @@ -0,0 +1,56 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferCodePrice.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation + +public struct SubscriptionOfferCodePrice: Identifiable, Sendable { + public let id: String + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionOfferCodePrice, included: [AppStoreAPI.SubscriptionOfferCodePricesResponse.IncludedItem]? = nil) { + id = schema.id + relationships = .init( + territoryId: schema.relationships?.territory?.data?.id, + subscriptionPricePointId: schema.relationships?.subscriptionPricePoint?.data?.id + ) + + var territories: [AppStoreAPI.Territory] = [] + var subscriptionPricePoints: [AppStoreAPI.SubscriptionPricePoint] = [] + + if let includedItems = included { + for includedItem in includedItems { + switch includedItem { + case let .territory(territory): + if schema.relationships?.territory?.data?.id == territory.id { + territories.append(territory) + } + case let .subscriptionPricePoint(subscriptionPricePoint): + if schema.relationships?.subscriptionPricePoint?.data?.id == subscriptionPricePoint.id { + subscriptionPricePoints.append(subscriptionPricePoint) + } + } + } + + self.included = .init( + territory: territories.first.flatMap { .init(schema: $0) }, + subscriptionPricePoint: subscriptionPricePoints.first.flatMap { .init(schema: $0) } + ) + } else { + self.included = nil + } + } + + public struct Relationships: Sendable { + public let territoryId: String? + public let subscriptionPricePointId: String? + } + + public struct Included: Sendable { + public let territory: Territory? + public let subscriptionPricePoint: SubscriptionPricePoint? + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionPrice.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionPrice.swift new file mode 100644 index 0000000..10f35b3 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionPrice.swift @@ -0,0 +1,70 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionPrice.swift, created on 05.02.2025 +// + +import AppStoreAPI +import OversizeCore + +public struct SubscriptionPrice: Sendable, Identifiable { + public let id: String + public var startDate: String? + public var isPreserved: Bool? + + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionPrice, included: [AppStoreAPI.SubscriptionPricesResponse.IncludedItem]? = nil) { + guard let attributes = schema.attributes else { return nil } + id = schema.id + startDate = attributes.startDate + isPreserved = attributes.isPreserved + + relationships = .init( + subscriptionPricePointId: schema.relationships?.subscriptionPricePoint?.data?.id, + territoryId: schema.relationships?.territory?.data?.id + ) + + if let includedItems = included { + var subscriptionPricePoint: SubscriptionPricePoint? + var territory: Territory? + + for includedItem in includedItems { + switch includedItem { + case let .subscriptionPricePoint(value): + if schema.relationships?.subscriptionPricePoint?.data?.id == value.id { + subscriptionPricePoint = .init(schema: value) + } + case let .territory(value): + if schema.relationships?.territory?.data?.id == value.id { + territory = .init(schema: value) + } + } + } + + self.included = .init( + subscriptionPricePoint: subscriptionPricePoint, + territory: territory + ) + } else { + self.included + = nil + } + } + + public struct Relationships: Sendable { + public let subscriptionPricePointId: String? + public let territoryId: String? + } + + public struct Included: Sendable { + public let subscriptionPricePoint: SubscriptionPricePoint? + public let territory: Territory? + } +} + +extension SubscriptionPrice { + static func from(response: AppStoreAPI.SubscriptionPricesResponse) -> [SubscriptionPrice] { + response.data.compactMap { SubscriptionPrice(schema: $0, included: response.included) } + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionPricePoint.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionPricePoint.swift new file mode 100644 index 0000000..bdea41e --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionPricePoint.swift @@ -0,0 +1,39 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionPricePoint.swift, created on 05.02.2025 +// + +import AppStoreAPI +import OversizeCore + +public struct SubscriptionPricePoint: Identifiable, Sendable { + public let id: String + public let customerPrice: String + public let proceeds: String + public let proceedsYear2: String + public let relationships: Relationships + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionPricePoint, included: [AppStoreAPI.Territory]? = nil) { + guard let attributes = schema.attributes else { return nil } + id = schema.id + customerPrice = attributes.customerPrice.valueOrEmpty + proceeds = attributes.proceeds.valueOrEmpty + proceedsYear2 = attributes.proceedsYear2.valueOrEmpty + relationships = .init(territoryId: schema.relationships?.territory?.data?.id) + + if let territory = included?.first(where: { $0.id == schema.relationships?.territory?.data?.id }) { + self.included = .init(territory: .init(schema: territory)) + } else { + self.included = nil + } + } + + public struct Relationships: Sendable { + public let territoryId: String? + } + + public struct Included: Sendable { + public let territory: Territory? + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionPromotionalOffer.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionPromotionalOffer.swift new file mode 100644 index 0000000..6f7116e --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionPromotionalOffer.swift @@ -0,0 +1,75 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionPromotionalOffer.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation +import OversizeCore + +public struct SubscriptionPromotionalOffer: Sendable, Identifiable { + public let id: String + public let name: String? + public let offerCode: String? + public let duration: SubscriptionOfferDuration? + public let offerMode: SubscriptionOfferMode? + public let numberOfPeriods: Int? + + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionPromotionalOffer, included: [SubscriptionPromotionalOfferResponse.IncludedItem]? = nil) { + guard let attributes = schema.attributes else { return nil } + + id = schema.id + name = attributes.name + offerCode = attributes.offerCode + + duration = .init(rawValue: attributes.duration?.rawValue ?? "") + offerMode = .init(rawValue: attributes.offerMode?.rawValue ?? "") + + numberOfPeriods = attributes.numberOfPeriods + + relationships = Relationships( + subscriptionId: schema.relationships?.subscription?.data?.id, + pricesIds: schema.relationships?.prices?.data?.map { $0.id } + ) + + self.included = .init( + subscription: included?.compactMap { (item: SubscriptionPromotionalOfferResponse.IncludedItem) -> Subscription? in + if case let .subscription(value) = item { return .init(schema: value) } + return nil + }.first, + subscriptionPromotionalOfferPrices: included?.compactMap { (item: SubscriptionPromotionalOfferResponse.IncludedItem) -> SubscriptionPromotionalOfferPrice? in + if case let .subscriptionPromotionalOfferPrice(value) = item { return .init(schema: value) } + return nil + } + ) + } + + public struct Relationships: Sendable { + public var subscriptionId: String? + public var pricesIds: [String]? + + public init( + subscriptionId: String? = nil, + pricesIds: [String]? = nil + ) { + self.subscriptionId = subscriptionId + self.pricesIds = pricesIds + } + } + + public struct Included: Sendable { + public let subscription: Subscription? + public let subscriptionPromotionalOfferPrices: [SubscriptionPromotionalOfferPrice]? + + public init( + subscription: Subscription? = nil, + subscriptionPromotionalOfferPrices: [SubscriptionPromotionalOfferPrice]? = nil + ) { + self.subscription = subscription + self.subscriptionPromotionalOfferPrices = subscriptionPromotionalOfferPrices + } + } +} diff --git a/Sources/OversizeAppStoreServices/Models/SubscriptionPromotionalOfferPrice.swift b/Sources/OversizeAppStoreServices/Models/SubscriptionPromotionalOfferPrice.swift new file mode 100644 index 0000000..48ed8db --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/SubscriptionPromotionalOfferPrice.swift @@ -0,0 +1,60 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionPromotionalOfferPrice.swift, created on 05.02.2025 +// + +import AppStoreAPI +import Foundation +import OversizeCore + +public struct SubscriptionPromotionalOfferPrice: Sendable, Identifiable { + public let id: String + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.SubscriptionPromotionalOfferPrice, included: [SubscriptionPromotionalOfferPricesResponse.IncludedItem]? = nil) { + id = schema.id + + relationships = Relationships( + territoryId: schema.relationships?.territory?.data?.id, + subscriptionPricePointId: schema.relationships?.subscriptionPricePoint?.data?.id + ) + + self.included = .init( + territory: included?.compactMap { item -> Territory? in + if case let .territory(value) = item { return .init(schema: value) } + return nil + }.first, + subscriptionPricePoint: included?.compactMap { item -> SubscriptionPricePoint? in + if case let .subscriptionPricePoint(value) = item { return .init(schema: value) } + return nil + }.first + ) + } + + public struct Relationships: Sendable { + public let territoryId: String? + public let subscriptionPricePointId: String? + + public init( + territoryId: String? = nil, + subscriptionPricePointId: String? = nil + ) { + self.territoryId = territoryId + self.subscriptionPricePointId = subscriptionPricePointId + } + } + + public struct Included: Sendable { + public let territory: Territory? + public let subscriptionPricePoint: SubscriptionPricePoint? + + public init( + territory: Territory? = nil, + subscriptionPricePoint: SubscriptionPricePoint? = nil + ) { + self.territory = territory + self.subscriptionPricePoint = subscriptionPricePoint + } + } +} diff --git a/Sources/OversizeAppStoreServices/Models/Territory.swift b/Sources/OversizeAppStoreServices/Models/Territory.swift index c4530fb..9d077a1 100644 --- a/Sources/OversizeAppStoreServices/Models/Territory.swift +++ b/Sources/OversizeAppStoreServices/Models/Territory.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // Territory.swift, created on 24.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/Typs/BundleID.swift b/Sources/OversizeAppStoreServices/Models/Typs/BundleID.swift index a68e057..215f03f 100644 --- a/Sources/OversizeAppStoreServices/Models/Typs/BundleID.swift +++ b/Sources/OversizeAppStoreServices/Models/Typs/BundleID.swift @@ -4,15 +4,9 @@ // import AppStoreAPI -import AppStoreConnect -public enum BundleID: Sendable { - public enum Platform: Sendable { - case iOS - case macOS - - init(schema: AppStoreAPI.BundleIDPlatform) { - self = schema == .iOS ? .iOS : .macOS - } - } +public enum BundleIDPlatform: String, CaseIterable, Codable, Sendable { + case iOS = "IOS" + case macOS = "MAC_OS" + case universal = "UNIVERSAL" } diff --git a/Sources/OversizeAppStoreServices/Models/Typs/CustomerReviewsSort.swift b/Sources/OversizeAppStoreServices/Models/Typs/CustomerReviewsSort.swift index f340674..2e8232a 100644 --- a/Sources/OversizeAppStoreServices/Models/Typs/CustomerReviewsSort.swift +++ b/Sources/OversizeAppStoreServices/Models/Typs/CustomerReviewsSort.swift @@ -1,6 +1,6 @@ // // Copyright © 2024 Alexander Romanov -// Sort.swift, created on 24.11.2024 +// CustomerReviewsSort.swift, created on 24.11.2024 // import Foundation diff --git a/Sources/OversizeAppStoreServices/Models/Typs/InAppPurchaseState.swift b/Sources/OversizeAppStoreServices/Models/Typs/InAppPurchaseState.swift index 23d5c58..516b0d2 100644 --- a/Sources/OversizeAppStoreServices/Models/Typs/InAppPurchaseState.swift +++ b/Sources/OversizeAppStoreServices/Models/Typs/InAppPurchaseState.swift @@ -1,9 +1,8 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchaseState.swift, created on 02.01.2025 // -import AppStoreConnect import Foundation import SwiftUI diff --git a/Sources/OversizeAppStoreServices/Models/Typs/InAppPurchaseType.swift b/Sources/OversizeAppStoreServices/Models/Typs/InAppPurchaseType.swift index 4fae229..c1ca44a 100644 --- a/Sources/OversizeAppStoreServices/Models/Typs/InAppPurchaseType.swift +++ b/Sources/OversizeAppStoreServices/Models/Typs/InAppPurchaseType.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // InAppPurchaseType.swift, created on 02.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionCustomerEligibility.swift b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionCustomerEligibility.swift new file mode 100644 index 0000000..150d14b --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionCustomerEligibility.swift @@ -0,0 +1,12 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionCustomerEligibility.swift, created on 05.02.2025 +// + +import Foundation + +public enum SubscriptionCustomerEligibility: String, CaseIterable, Codable, Sendable { + case new = "NEW" + case existing = "EXISTING" + case expired = "EXPIRED" +} diff --git a/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferDuration.swift b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferDuration.swift new file mode 100644 index 0000000..13e2f91 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferDuration.swift @@ -0,0 +1,34 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferDuration.swift, created on 05.02.2025 +// + +import Foundation + +public enum SubscriptionOfferDuration: String, CaseIterable, Codable, Sendable, Identifiable { + case threeDays = "THREE_DAYS" + case oneWeek = "ONE_WEEK" + case twoWeeks = "TWO_WEEKS" + case oneMonth = "ONE_MONTH" + case twoMonths = "TWO_MONTHS" + case threeMonths = "THREE_MONTHS" + case sixMonths = "SIX_MONTHS" + case oneYear = "ONE_YEAR" + + public var displayName: String { + switch self { + case .threeDays: "3 Days" + case .oneWeek: "1 Week" + case .twoWeeks: "2 Weeks" + case .oneMonth: "1 Month" + case .twoMonths: "2 Months" + case .threeMonths: "3 Months" + case .sixMonths: "6 Months" + case .oneYear: "1 Year" + } + } + + public var id: String { + rawValue + } +} diff --git a/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferEligibility.swift b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferEligibility.swift new file mode 100644 index 0000000..349b363 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferEligibility.swift @@ -0,0 +1,11 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferEligibility.swift, created on 05.02.2025 +// + +import Foundation + +public enum SubscriptionOfferEligibility: String, CaseIterable, Codable, Sendable { + case stackWithIntroOffers = "STACK_WITH_INTRO_OFFERS" + case replaceIntroOffers = "REPLACE_INTRO_OFFERS" +} diff --git a/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferMode.swift b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferMode.swift new file mode 100644 index 0000000..a306e20 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionOfferMode.swift @@ -0,0 +1,24 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferMode.swift, created on 02.02.2025 +// + +import Foundation + +public enum SubscriptionOfferMode: String, CaseIterable, Codable, Sendable, Identifiable { + case payAsYouGo = "PAY_AS_YOU_GO" + case payUpFront = "PAY_UP_FRONT" + case freeTrial = "FREE_TRIAL" + + public var displayName: String { + switch self { + case .payAsYouGo: "Pay As You Go" + case .payUpFront: "Pay Up Front" + case .freeTrial: "Free Trial" + } + } + + public var id: String { + rawValue + } +} diff --git a/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionPeriod.swift b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionPeriod.swift new file mode 100644 index 0000000..067d32a --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/Typs/SubscriptionPeriod.swift @@ -0,0 +1,36 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionPeriod.swift, created on 02.02.2025 +// + +import Foundation + +public enum SubscriptionPeriod: String, CaseIterable, Codable, Sendable, Identifiable { + case oneWeek = "ONE_WEEK" + case oneMonth = "ONE_MONTH" + case twoMonths = "TWO_MONTHS" + case threeMonths = "THREE_MONTHS" + case sixMonths = "SIX_MONTHS" + case oneYear = "ONE_YEAR" + + public var id: String { + rawValue + } + + public var displayName: String { + switch self { + case .oneWeek: + "1 Week" + case .oneMonth: + "1 Month" + case .twoMonths: + "2 Months" + case .threeMonths: + "3 Months" + case .sixMonths: + "6 Months" + case .oneYear: + "1 Year" + } + } +} diff --git a/Sources/OversizeAppStoreServices/Models/Typs/TerritoryRegion.swift b/Sources/OversizeAppStoreServices/Models/Typs/TerritoryRegion.swift index d12ebc5..9bf0126 100644 --- a/Sources/OversizeAppStoreServices/Models/Typs/TerritoryRegion.swift +++ b/Sources/OversizeAppStoreServices/Models/Typs/TerritoryRegion.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // Region.swift, created on 27.01.2025 // @@ -62,7 +62,7 @@ public enum TerritoryRegion: String, Sendable, CaseIterable, Identifiable { .kaz, .kgz, .mac, .npl, .tjk, .tkm, .uzb, // Additional Pacific territories .asm, .cok, .cxr, .gum, .kir, .mhl, .mnp, .ncl, .nfk, .niu, - .nru, .pyf, .tuv, .wlf: + .pyf, .tuv, .wlf: self = .asiaPacific default: diff --git a/Sources/OversizeAppStoreServices/Models/UploadOperation.swift b/Sources/OversizeAppStoreServices/Models/UploadOperation.swift index 2d988ff..df678b0 100644 --- a/Sources/OversizeAppStoreServices/Models/UploadOperation.swift +++ b/Sources/OversizeAppStoreServices/Models/UploadOperation.swift @@ -1,5 +1,5 @@ // -// Copyright © 2025 Aleksandr Romanov +// Copyright © 2025 Alexander Romanov // UploadOperation.swift, created on 17.01.2025 // diff --git a/Sources/OversizeAppStoreServices/Models/WinBackOffer.swift b/Sources/OversizeAppStoreServices/Models/WinBackOffer.swift new file mode 100644 index 0000000..35348c3 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/WinBackOffer.swift @@ -0,0 +1,82 @@ +// +// Copyright © 2025 Alexander Romanov +// WinBackOffer.swift, created on 05.02.2025 +// + +import AppStoreAPI +import OversizeCore + +public struct WinBackOffer: Sendable, Identifiable { + public let id: String + public let referenceName: String? + public let offerID: String? + public let duration: SubscriptionOfferDuration? + public let offerMode: SubscriptionOfferMode? + public let periodCount: Int? + public let customerEligibilityPaidSubscriptionDurationInMonths: Int? + public let customerEligibilityTimeSinceLastSubscribedInMonths: IntegerRange? + public let customerEligibilityWaitBetweenOffersInMonths: Int? + public let startDate: String? + public let endDate: String? + public let priority: Priority? + public let promotionIntent: PromotionIntent? + + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.WinBackOffer, included: [AppStoreAPI.WinBackOfferPrice]? = nil) { + guard let attributes = schema.attributes else { return nil } + + id = schema.id + referenceName = attributes.referenceName + offerID = attributes.offerID + duration = .init(rawValue: attributes.duration?.rawValue ?? "") + offerMode = .init(rawValue: attributes.offerMode?.rawValue ?? "") + periodCount = attributes.periodCount + customerEligibilityPaidSubscriptionDurationInMonths = attributes.customerEligibilityPaidSubscriptionDurationInMonths + customerEligibilityTimeSinceLastSubscribedInMonths = attributes.customerEligibilityTimeSinceLastSubscribedInMonths.map { .init(schema: $0) } + customerEligibilityWaitBetweenOffersInMonths = attributes.customerEligibilityWaitBetweenOffersInMonths + startDate = attributes.startDate + endDate = attributes.endDate + priority = .init(rawValue: attributes.priority?.rawValue ?? "") + promotionIntent = .init(rawValue: attributes.promotionIntent?.rawValue ?? "") + + relationships = Relationships( + pricesIds: schema.relationships?.prices?.data?.map { $0.id } + ) + + if let included { + self.included = .init( + winBackOfferPrices: included.compactMap { .init(schema: $0) } + ) + } else { + self.included = nil + } + } + + public enum Priority: String, CaseIterable, Codable, Sendable { + case high = "HIGH" + case normal = "NORMAL" + } + + public enum PromotionIntent: String, CaseIterable, Codable, Sendable { + case notPromoted = "NOT_PROMOTED" + case useAutoGeneratedAssets = "USE_AUTO_GENERATED_ASSETS" + } + + public struct Relationships: Sendable { + public var pricesIds: [String]? + + public init(pricesIds: [String]? = nil) { + self.pricesIds = pricesIds + } + } + + public struct Included: Sendable { + public let winBackOfferPrices: [WinBackOfferPrice]? + + public init(winBackOfferPrices: [WinBackOfferPrice]? = nil) { + self.winBackOfferPrices = winBackOfferPrices + } + } +} diff --git a/Sources/OversizeAppStoreServices/Models/WinBackOfferPrice.swift b/Sources/OversizeAppStoreServices/Models/WinBackOfferPrice.swift new file mode 100644 index 0000000..5f8b836 --- /dev/null +++ b/Sources/OversizeAppStoreServices/Models/WinBackOfferPrice.swift @@ -0,0 +1,66 @@ +// +// Copyright © 2025 Alexander Romanov +// WinBackOfferPrice.swift, created on 05.02.2025 +// + +import AppStoreAPI +import OversizeCore + +public struct WinBackOfferPrice: Sendable, Identifiable { + public let id: String + public let relationships: Relationships? + public let included: Included? + + public init?(schema: AppStoreAPI.WinBackOfferPrice, included: [WinBackOfferPricesResponse.IncludedItem]? = nil) { + id = schema.id + + relationships = Relationships( + territoryId: schema.relationships?.territory?.data?.id, + subscriptionPricePointId: schema.relationships?.subscriptionPricePoint?.data?.id + ) + + self.included = .init( + territory: included?.compactMap { item -> Territory? in + if case let .territory(value) = item { return .init(schema: value) } + return nil + }.first, + subscriptionPricePoint: included?.compactMap { item -> SubscriptionPricePoint? in + if case let .subscriptionPricePoint(value) = item { return .init(schema: value) } + return nil + }.first + ) + } + + // Static method to create array from collection response + public static func array(from response: WinBackOfferPricesResponse) -> [WinBackOfferPrice] { + response.data.compactMap { + WinBackOfferPrice(schema: $0, included: response.included) + } + } + + public struct Relationships: Sendable { + public let territoryId: String? + public let subscriptionPricePointId: String? + + public init( + territoryId: String? = nil, + subscriptionPricePointId: String? = nil + ) { + self.territoryId = territoryId + self.subscriptionPricePointId = subscriptionPricePointId + } + } + + public struct Included: Sendable { + public let territory: Territory? + public let subscriptionPricePoint: SubscriptionPricePoint? + + public init( + territory: Territory? = nil, + subscriptionPricePoint: SubscriptionPricePoint? = nil + ) { + self.territory = territory + self.subscriptionPricePoint = subscriptionPricePoint + } + } +} diff --git a/Sources/OversizeAppStoreServices/ServiceRegistering.swift b/Sources/OversizeAppStoreServices/ServiceRegistering.swift index fab0a72..88e3057 100644 --- a/Sources/OversizeAppStoreServices/ServiceRegistering.swift +++ b/Sources/OversizeAppStoreServices/ServiceRegistering.swift @@ -14,8 +14,8 @@ public extension Container { self { InAppPurchasesService() } } - var subscribtionsService: Factory { - self { SubscribtionsService() } + var subscriptionsService: Factory { + self { SubscriptionsService() } } var certificateService: Factory { @@ -54,19 +54,7 @@ public extension Container { self { AppStoreVersionSubmissionsService() } } - var analyticsService: Factory { - self { AnalyticsService() } - } - - var salesAndFinanceService: Factory { - self { SalesAndFinanceService() } - } - var cacheService: Factory { self { CacheService() } } - - var perfPowerMetricsService: Factory { - self { PerfPowerMetricsService() } - } } diff --git a/Sources/OversizeAppStoreServices/Services/AppInfoService.swift b/Sources/OversizeAppStoreServices/Services/AppInfoService.swift index b57172a..ea4736d 100644 --- a/Sources/OversizeAppStoreServices/Services/AppInfoService.swift +++ b/Sources/OversizeAppStoreServices/Services/AppInfoService.swift @@ -126,7 +126,7 @@ public actor AppInfoService { ) let request = Resources.v1.ageRatingDeclarations.id(ageRatingDeclarationId).patch( - .init(data: .init(type: .ageRatingDeclarations, id: ageRatingDeclarationId, attributes: requestAttributes)) + .init(data: requestData) ) do { diff --git a/Sources/OversizeAppStoreServices/Services/AppsService.swift b/Sources/OversizeAppStoreServices/Services/AppsService.swift index e172177..5497605 100644 --- a/Sources/OversizeAppStoreServices/Services/AppsService.swift +++ b/Sources/OversizeAppStoreServices/Services/AppsService.swift @@ -193,12 +193,13 @@ public actor AppsService { seedID: String? = nil ) async -> Result { guard let client else { return .failure(.network(type: .unauthorized)) } + guard let bundleIDPlatform: AppStoreAPI.BundleIDPlatform = .init(rawValue: platform.rawValue) else { return .failure(.network(type: .invalidURL)) } let requestData: BundleIDCreateRequest.Data = .init( type: .bundleIDs, attributes: .init( name: name, - platform: platform, + platform: bundleIDPlatform, identifier: identifier, seedID: seedID ) @@ -206,7 +207,7 @@ public actor AppsService { let request = Resources.v1.bundleIDs.post(.init(data: requestData)) do { - let data = try await client.send(request).data + _ = try await client.send(request).data return .success(true) } catch { return .failure(.network(type: .noResponse)) diff --git a/Sources/OversizeAppStoreServices/Services/BuildsService.swift b/Sources/OversizeAppStoreServices/Services/BuildsService.swift index 4daf86e..94eb838 100644 --- a/Sources/OversizeAppStoreServices/Services/BuildsService.swift +++ b/Sources/OversizeAppStoreServices/Services/BuildsService.swift @@ -37,7 +37,6 @@ public actor BuildsService { public func fetchBuildBundlesId(buildBundlesId: String, forse: Bool = false) async -> Result<[BuildBundleFileSize], AppError> { guard let client else { return .failure(.network(type: .unauthorized)) } - let pathKey = "buildBundlesId\(buildBundlesId)" return await cacheService.fetchWithCache(key: "buildBundlesId\(buildBundlesId)", force: forse) { let request = Resources.v1.buildBundles.id(buildBundlesId).buildBundleFileSizes.get() return try await client.send(request).data diff --git a/Sources/OversizeAppStoreServices/Services/InAppPurchasesService.swift b/Sources/OversizeAppStoreServices/Services/InAppPurchasesService.swift index ce0d1e7..dd0b2e0 100644 --- a/Sources/OversizeAppStoreServices/Services/InAppPurchasesService.swift +++ b/Sources/OversizeAppStoreServices/Services/InAppPurchasesService.swift @@ -244,7 +244,7 @@ public actor InAppPurchasesService { ) async -> Result<[InAppPurchasePrice], AppError> { let filterTerritoryIds: [String]? = filterTerritory?.compactMap { $0.id } guard let client else { return .failure(.network(type: .unauthorized)) } - return await cacheService.fetchWithCache(key: "fetchInAppPurchasePriceScheduleAutomaticPrices\(inAppPurchasePriceSchedulesId)\(filterTerritoryIds)", force: force) { + return await cacheService.fetchWithCache(key: "fetchInAppPurchasePriceScheduleAutomaticPrices\(inAppPurchasePriceSchedulesId)\(filterTerritoryIds ?? [])", force: force) { let request = Resources.v1.inAppPurchasePriceSchedules.id(inAppPurchasePriceSchedulesId).automaticPrices.get( filterTerritory: filterTerritoryIds, limit: 200, @@ -263,7 +263,7 @@ public actor InAppPurchasesService { ) async -> Result<[InAppPurchasePrice], AppError> { guard let client else { return .failure(.network(type: .unauthorized)) } let filterTerritoryIds: [String]? = filterTerritory?.compactMap { $0.id } - return await cacheService.fetchWithCache(key: "fetchInAppPurchasePriceScheduleManualPrices\(inAppPurchasePriceSchedulesId)\(filterTerritoryIds)", force: force) { + return await cacheService.fetchWithCache(key: "fetchInAppPurchasePriceScheduleManualPrices\(inAppPurchasePriceSchedulesId)\(filterTerritoryIds ?? [])", force: force) { let request = Resources.v1.inAppPurchasePriceSchedules.id(inAppPurchasePriceSchedulesId).manualPrices.get( filterTerritory: filterTerritoryIds, limit: 200, @@ -461,7 +461,7 @@ public actor InAppPurchasesService { guard let client else { return .failure(.network(type: .unauthorized)) } let request = Resources.v2.inAppPurchases.id(inAppPurchaseV2Id).delete do { - let data = try await client.send(request) + let _ = try await client.send(request) return .success(true) } catch { return .failure(.network(type: .noResponse)) diff --git a/Sources/OversizeAppStoreServices/Services/SubscribtionsService.swift b/Sources/OversizeAppStoreServices/Services/SubscribtionsService.swift index 57fd5a4..64a8e68 100644 --- a/Sources/OversizeAppStoreServices/Services/SubscribtionsService.swift +++ b/Sources/OversizeAppStoreServices/Services/SubscribtionsService.swift @@ -1,6 +1,6 @@ // -// Copyright © 2025 Aleksandr Romanov -// SubscribtionsService.swift, created on 12.01.2025 +// Copyright © 2025 Alexander Romanov +// subscriptionsService.swift, created on 12.01.2025 // import AppStoreAPI @@ -9,7 +9,7 @@ import Factory import Foundation import OversizeModels -public actor SubscribtionsService { +public actor SubscriptionsService { private let client: AppStoreConnectClient? @Injected(\.cacheService) private var cacheService: CacheService @@ -45,4 +45,561 @@ public actor SubscribtionsService { data.data.compactMap { .init(schema: $0, included: data.included) } } } + + public func fetchSubscription(subscriptionId: String, force: Bool = false) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + return await cacheService.fetchWithCache(key: "fetchSubscription\(subscriptionId)", force: force) { + let request = Resources.v1.subscriptions.id(subscriptionId).get() + return try await client.send(request) + }.flatMap { + guard let build: Subscription = .init(schema: $0.data) else { + return .failure(.network(type: .decode)) + } + return .success(build) + } + } + + public func fetchSubscriptionIncludedAll(subscriptionId: String, force: Bool = false) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + return await cacheService.fetchWithCache(key: "fetchSubscriptionIncludedAll\(subscriptionId)", force: force) { + let request = Resources.v1.subscriptions.id(subscriptionId).get( + include: [ + .subscriptionLocalizations, + .appStoreReviewScreenshot, + .group, + .introductoryOffers, + .promotionalOffers, + .offerCodes, + .prices, + .promotedPurchase, + .winBackOffers, + .images, + ] + ) + return try await client.send(request) + }.flatMap { + guard let build: Subscription = .init(schema: $0.data, included: $0.included) else { + return .failure(.network(type: .decode)) + } + return .success(build) + } + } + + public func postSubscriptionGroup(appId: String, referenceName: String) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + let requestData: SubscriptionGroupCreateRequest.Data = .init( + type: .subscriptionGroups, + attributes: .init(referenceName: referenceName), + relationships: .init( + app: .init( + data: .init( + type: .apps, + id: appId + ) + ) + ) + ) + let request = Resources.v1.subscriptionGroups.post(.init(data: requestData)) + do { + let data = try await client.send(request).data + guard let app = SubscriptionGroup(schema: data) else { + return .failure(.network(type: .decode)) + } + return .success(app) + } catch { + return handleRequestFailure(error: error, replaces: [:]) + } + } + + public func postSubscription( + subscriptionGroupId: String, + name: String, + productID: String, + isFamilySharable: Bool? = nil, + subscriptionPeriod: SubscriptionPeriod? = nil, + reviewNote: String? = nil, + groupLevel: Int? = nil + ) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + + var subscriptionPeriodRequest: SubscriptionCreateRequest.Data.Attributes.SubscriptionPeriod? { + if let subscriptionPeriod { + .init(rawValue: subscriptionPeriod.rawValue) + } else { + nil + } + } + + let requestAttributes: SubscriptionCreateRequest.Data.Attributes = .init( + name: name, + productID: productID, + isFamilySharable: isFamilySharable, + subscriptionPeriod: subscriptionPeriodRequest, + reviewNote: reviewNote, + groupLevel: groupLevel + ) + + let requestData: SubscriptionCreateRequest.Data = .init( + type: .subscriptions, + attributes: requestAttributes, + relationships: .init( + group: .init( + data: .init( + type: .subscriptionGroups, + id: subscriptionGroupId + ) + ) + ) + ) + let request = Resources.v1.subscriptions.post(.init(data: requestData)) + do { + let data = try await client.send(request).data + guard let app = Subscription(schema: data) else { + return .failure(.network(type: .decode)) + } + return .success(app) + } catch { + return handleRequestFailure(error: error, replaces: [:]) + } + } + + public func fetchSubscriptionPricePointsEqualizations( + subscriptionPricePointId: String, + force: Bool = false + ) async -> Result<[SubscriptionPricePoint], AppError> { + guard let client else { return .failure(.network(type: .unauthorized)) } + return await cacheService.fetchWithCache(key: "fetchSubscriptionPricePointsEqualizations\(subscriptionPricePointId)", force: force) { + let request = Resources.v1.subscriptionPricePoints.id(subscriptionPricePointId).equalizations.get(limit: 200, include: [.territory]) + return try await client.send(request) + }.map { data in + data.data.compactMap { .init(schema: $0, included: data.included) } + } + } + + public func fetchSubscriptionPricePoints( + subscriptionId: String, + filterTerritory: [Territory]? = nil, + force: Bool = false + ) async -> Result<[SubscriptionPricePoint], AppError> { + guard let client else { return .failure(.network(type: .unauthorized)) } + + var filterTerritorIds: [String]? = nil + if let filterTerritory { + filterTerritorIds = filterTerritory.compactMap { $0.id } + } + + return await cacheService.fetchWithCache(key: "fetchSubscriptionPricePoints\(subscriptionId)\(filterTerritorIds?.joined(separator: "-"))", force: force) { + let request = Resources.v1.subscriptions.id(subscriptionId).pricePoints.get(filterTerritory: filterTerritorIds, limit: 2000, include: [.territory]) + return try await client.send(request) + }.map { data in + data.data.compactMap { .init(schema: $0, included: data.included) } + } + } + + public func postSubscriptionAvailabilities( + subscriptionId: String, + isAvailableInNewTerritories: Bool, + availableTerritories: [Territory] + ) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + + let requestData = SubscriptionAvailabilityCreateRequest.Data( + type: .subscriptionAvailabilities, + attributes: .init( + isAvailableInNewTerritories: isAvailableInNewTerritories + ), + relationships: .init( + subscription: .init(data: .init(type: .subscriptions, id: subscriptionId)), + availableTerritories: .init( + data: availableTerritories.compactMap { .init( + type: .territories, + id: $0.id + ) } + ) + ) + ) + + let request = Resources.v1.subscriptionAvailabilities.post(.init(data: requestData)) + do { + let data = try await client.send(request).data + guard let subscriptionAvailability = SubscriptionAvailability(schema: data) else { + return .failure(.network(type: .decode)) + } + return .success(subscriptionAvailability) + } catch { + return handleRequestFailure(error: error, replaces: [:]) + } + } + + public func fetchSubscriptionIntroductoryOffers( + subscriptionId: String, + filterTerritory: [Territory]? = nil, + force: Bool = false + ) async -> Result<[SubscriptionIntroductoryOffer], AppError> { + guard let client else { return .failure(.network(type: .unauthorized)) } + + var filterTerritoryIds: [String]? = nil + if let filterTerritory { + filterTerritoryIds = filterTerritory.compactMap { $0.id } + } + + return await cacheService.fetchWithCache(key: "fetchSubscriptionIntroductoryOffers\(subscriptionId)\(filterTerritoryIds?.joined(separator: "-"))", force: force) { + let request = Resources.v1.subscriptions.id(subscriptionId).introductoryOffers.get( + filterTerritory: filterTerritoryIds, + limit: 200, + include: [ + .subscription, + .subscriptionPricePoint, + .territory, + ] + ) + + return try await client.send(request) + }.map { SubscriptionIntroductoryOffer.from(response: $0) } + } + + public func fetchSubscriptionAvailability( + subscriptionId: String, + force: Bool = false + ) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + return await cacheService.fetchWithCache(key: "fetchSubscriptionAvailability\(subscriptionId)", force: force) { + let request = Resources.v1.subscriptions.id(subscriptionId).subscriptionAvailability.get( + limitAvailableTerritories: 50 + ) + return try await client.send(request) + }.flatMap { + guard let availability = SubscriptionAvailability(schema: $0.data, included: $0.included) else { + return .failure(.network(type: .decode)) + } + return .success(availability) + } + } + + public func fetchSubscriptionPrices( + subscriptionId: String, + filterTerritory: [Territory]? = nil, + force: Bool = false + ) async -> Result<[SubscriptionPrice], AppError> { + guard let client else { return .failure(.network(type: .unauthorized)) } + + var filterTerritoryIds: [String]? = nil + if let filterTerritory { + filterTerritoryIds = filterTerritory.compactMap { $0.id } + } + + return await cacheService.fetchWithCache(key: "fetchSubscriptionPrices\(subscriptionId)\(filterTerritoryIds?.joined(separator: "-"))", force: force) { + let request = Resources.v1.subscriptions.id(subscriptionId).prices.get( + filterTerritory: filterTerritoryIds, + limit: 200, + include: [ + .territory, + .subscriptionPricePoint, + ] + ) + return try await client.send(request) + }.map { SubscriptionPrice.from(response: $0) } + } + + public func patchSubscription( + subscriptionsId: String, + name: String? = nil, + isFamilySharable: Bool? = nil, + subscriptionPeriod: SubscriptionPeriod? = nil, + reviewNote: String? = nil, + groupLevel: Int? = nil + ) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + + var subscriptionPeriodRequest: SubscriptionUpdateRequest.Data.Attributes.SubscriptionPeriod? { + if let subscriptionPeriod { + .init(rawValue: subscriptionPeriod.rawValue) + } else { + nil + } + } + + let requestAttributes: SubscriptionUpdateRequest.Data.Attributes = .init( + name: name, + isFamilySharable: isFamilySharable, + subscriptionPeriod: subscriptionPeriodRequest, + reviewNote: reviewNote, + groupLevel: groupLevel + ) + + let requestData: SubscriptionUpdateRequest.Data = .init( + type: .subscriptions, + id: subscriptionsId, + attributes: requestAttributes + ) + + let request = Resources.v1.subscriptions.id(subscriptionsId).patch(.init(data: requestData)) + + do { + let data = try await client.send(request).data + guard let app = Subscription(schema: data) else { + return .failure(.network(type: .decode)) + } + return .success(app) + } catch { + return handleRequestFailure(error: error, replaces: [:]) + } + } + + public func fetchSubscriptionAvailabilitiesAvailableTerritories( + subscriptionAvailabilitiyId: String, + force: Bool = false + ) async -> Result<[Territory], AppError> { + guard let client else { return .failure(.network(type: .unauthorized)) } + return await cacheService.fetchWithCache(key: "fetchSubscriptionAvailabilitiesAvailableTerritories\(subscriptionAvailabilitiyId)", force: force) { + let request = Resources.v1.subscriptionAvailabilities.id(subscriptionAvailabilitiyId).availableTerritories.get(limit: 200) + return try await client.send(request).data + }.map { data in + data.compactMap { .init(schema: $0) } + } + } + + public func fetchSubscriptionPromotionalOfferPrices( + subscriptionPromotionalOfferId: String, + force: Bool = false + ) async -> Result<[SubscriptionPromotionalOfferPrice], AppError> { + guard let client else { return .failure(.network(type: .unauthorized)) } + return await cacheService.fetchWithCache(key: "fetchSubscriptionPromotionalOfferPrices\(subscriptionPromotionalOfferId)", force: force) { + let request = Resources.v1.subscriptionPromotionalOffers.id(subscriptionPromotionalOfferId).prices.get() + return try await client.send(request) + }.map { data in + data.data.compactMap { .init(schema: $0, included: data.included) } + } + } + + public func createPromotionalOffer( + subscriptionId: String, + name: String, + offerCode: String, + duration: SubscriptionOfferDuration, + offerMode: SubscriptionOfferMode, + numberOfPeriods: Int, + subscriptionPromotionalOfferPriceId: String + ) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + + guard let duration: AppStoreAPI.SubscriptionOfferDuration = .init(rawValue: duration.rawValue), + let offerMode: AppStoreAPI.SubscriptionOfferMode = .init(rawValue: offerMode.rawValue) else { return .failure(.network(type: .invalidURL)) } + + let requestData = SubscriptionPromotionalOfferCreateRequest.Data( + type: .subscriptionPromotionalOffers, + attributes: .init( + name: name, + offerCode: offerCode, + duration: duration, + offerMode: offerMode, + numberOfPeriods: numberOfPeriods + ), + relationships: .init( + subscription: .init( + data: .init(type: .subscriptions, id: subscriptionId) + ), prices: .init(data: [.init(type: .subscriptionPromotionalOfferPrices, id: subscriptionPromotionalOfferPriceId)]) + ) + ) + + let request = Resources.v1.subscriptionPromotionalOffers.post(.init(data: requestData)) + do { + let data = try await client.send(request).data + guard let offer = SubscriptionPromotionalOffer(schema: data) else { + return .failure(.network(type: .decode)) + } + return .success(offer) + } catch { + return handleRequestFailure(error: error) + } + } + + public func createSubscriptionLocalization( + subscriptionId: String, + name: String, + description: String?, + locale: AppStoreLanguage + ) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + + let requestData = SubscriptionLocalizationCreateRequest.Data( + type: .subscriptionLocalizations, + attributes: .init( + name: name, + locale: locale.rawValue, + description: description + ), + relationships: .init( + subscription: .init( + data: .init(type: .subscriptions, id: subscriptionId) + ) + ) + ) + + let request = Resources.v1.subscriptionLocalizations.post(.init(data: requestData)) + do { + let data = try await client.send(request).data + guard let localization = SubscriptionLocalization(schema: data) else { + return .failure(.network(type: .decode)) + } + return .success(localization) + } catch { + return handleRequestFailure(error: error) + } + } + + public func patchSubscriptionPrice( + subscriptionsId: String, + pricePountId: String, + prices: [SubscriptionPricePoint] + ) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + + let relationships = SubscriptionUpdateRequest.Data.Relationships( + prices: .init(data: prices.map { .init(type: .subscriptionPrices, id: $0.id) }) + ) + + let requestData = SubscriptionUpdateRequest.Data( + type: .subscriptions, + id: subscriptionsId, + relationships: relationships + ) + + let includedItems: [SubscriptionUpdateRequest.IncludedItem] = prices.map { price in + let pricePointData = SubscriptionPriceInlineCreate.Relationships.SubscriptionPricePoint.Data( + type: .subscriptionPricePoints, + id: pricePountId + ) + + let relationship = SubscriptionPriceInlineCreate.Relationships( + subscriptionPricePoint: .init(data: pricePointData) + ) + + return .subscriptionPriceInlineCreate( + SubscriptionPriceInlineCreate( + type: .subscriptionPrices, + id: price.id, + relationships: relationship + ) + ) + } + + let request = Resources.v1.subscriptions.id(subscriptionsId).patch( + .init(data: requestData, included: includedItems) + ) + + do { + let data = try await client.send(request).data + guard let subscription = Subscription(schema: data) else { + return .failure(.network(type: .decode)) + } + return .success(subscription) + } catch { + return handleRequestFailure(error: error, replaces: [:]) + } + } + + public func patchSubscriptionIntroductoryOffers( + subscriptionsId: String, + startDate: Date? = nil, + endDate: Date? = nil, + duration: SubscriptionOfferDuration, + offerMode: SubscriptionOfferMode, + numberOfPeriods: Int, + territories: [Territory] + ) async -> Result { + guard let client else { return .failure(.network(type: .unauthorized)) } + + guard let duration: AppStoreAPI.SubscriptionOfferDuration = .init(rawValue: duration.rawValue), + let offerMode: AppStoreAPI.SubscriptionOfferMode = .init(rawValue: offerMode.rawValue) + else { + return .failure(.network(type: .invalidURL)) + } + + let includedItems: [SubscriptionUpdateRequest.IncludedItem] = territories.enumerated().map { index, territory in + let temporaryId = "newIntroOffer-\(index)" + + let attributes = SubscriptionIntroductoryOfferInlineCreate.Attributes( + startDate: startDate?.toString(), + endDate: endDate?.toString(), + duration: duration, + offerMode: offerMode, + numberOfPeriods: numberOfPeriods + ) + + let relationship = SubscriptionIntroductoryOfferInlineCreate.Relationships( + territory: .init( + data: .init( + type: .territories, + id: territory.id + ) + ) + ) + + return .subscriptionIntroductoryOfferInlineCreate( + .init( + id: temporaryId, + attributes: attributes, + relationships: relationship + ) + ) + } + + let introductoryOffersData = includedItems.compactMap { item -> SubscriptionUpdateRequest.Data.Relationships.IntroductoryOffers.Datum? in + guard case let .subscriptionIntroductoryOfferInlineCreate(offer) = item else { return nil } + return .init( + type: .subscriptionIntroductoryOffers, + id: offer.id ?? "" + ) + } + + let relationships = SubscriptionUpdateRequest.Data.Relationships( + introductoryOffers: .init(data: introductoryOffersData) + ) + + let requestData = SubscriptionUpdateRequest.Data( + type: .subscriptions, + id: subscriptionsId, + relationships: relationships + ) + + let request = Resources.v1.subscriptions.id(subscriptionsId).patch( + .init(data: requestData, included: includedItems) + ) + + do { + let data = try await client.send(request).data + guard let subscription = Subscription(schema: data) else { + return .failure(.network(type: .decode)) + } + return .success(subscription) + } catch { + return handleRequestFailure(error: error, replaces: [:]) + } + } +} + +extension SubscriptionsService { + func handleRequestFailure(error: Error, replaces: [String: String] = [:]) -> Result { + if let responseError = error as? ResponseError { + switch responseError { + case let .requestFailure(errorResponse, _, _): + if let errors = errorResponse?.errors, let firstError = errors.first { + var title = firstError.title + var detail = firstError.detail + + for (placeholder, replacement) in replaces { + title = title.replacingOccurrences(of: placeholder, with: replacement) + detail = detail.replacingOccurrences(of: placeholder, with: replacement) + } + + return .failure(AppError.network(type: .apiError(title, detail))) + } + return .failure(AppError.network(type: .unknown)) + default: + return .failure(AppError.network(type: .unknown)) + } + } + return .failure(AppError.network(type: .unknown)) + } } diff --git a/Sources/OversizeAppStoreServices/Services/VersionsService.swift b/Sources/OversizeAppStoreServices/Services/VersionsService.swift index 576f2e2..814ff38 100644 --- a/Sources/OversizeAppStoreServices/Services/VersionsService.swift +++ b/Sources/OversizeAppStoreServices/Services/VersionsService.swift @@ -396,7 +396,7 @@ public actor VersionsService { guard let client else { return .failure(.network(type: .unauthorized)) } let request = Resources.v1.appStoreVersionLocalizations.id(localizationId).delete do { - let data = try await client.send(request) + let _ = try await client.send(request) return .success(true) } catch { return .failure(.network(type: .noResponse)) diff --git a/Sources/OversizeAppStoreServices/Models/AnalyticsReport.swift b/Sources/OversizeMetricServices/Models/AnalyticsReport.swift similarity index 97% rename from Sources/OversizeAppStoreServices/Models/AnalyticsReport.swift rename to Sources/OversizeMetricServices/Models/AnalyticsReport.swift index 20caba4..60c1173 100644 --- a/Sources/OversizeAppStoreServices/Models/AnalyticsReport.swift +++ b/Sources/OversizeMetricServices/Models/AnalyticsReport.swift @@ -4,7 +4,7 @@ // import AppStoreAPI -import AppStoreConnect + import OversizeCore public struct AnalyticsReport: Identifiable, Hashable, Sendable { diff --git a/Sources/OversizeAppStoreServices/Models/AnalyticsReportRequest.swift b/Sources/OversizeMetricServices/Models/AnalyticsReportRequest.swift similarity index 90% rename from Sources/OversizeAppStoreServices/Models/AnalyticsReportRequest.swift rename to Sources/OversizeMetricServices/Models/AnalyticsReportRequest.swift index 4fc7a5b..4fe4071 100644 --- a/Sources/OversizeAppStoreServices/Models/AnalyticsReportRequest.swift +++ b/Sources/OversizeMetricServices/Models/AnalyticsReportRequest.swift @@ -4,7 +4,7 @@ // import AppStoreAPI -import AppStoreConnect + import Foundation import OversizeCore @@ -22,7 +22,7 @@ public struct AnalyticsReportRequest: Identifiable, Sendable { self.accessType = accessType isStoppedDueToInactivity = schema.attributes?.isStoppedDueToInactivity - if let analyticsReport = included?.filter { $0.id == schema.relationships?.reports?.data?.first?.id } { + if let analyticsReport = included?.filter({ $0.id == schema.relationships?.reports?.data?.first?.id }) { self.included = .init(analyticsReports: analyticsReport.compactMap { .init(schema: $0) }) } else { self.included = nil diff --git a/Sources/OversizeAppStoreServices/Models/FinanceReport.swift b/Sources/OversizeMetricServices/Models/FinanceReport.swift similarity index 100% rename from Sources/OversizeAppStoreServices/Models/FinanceReport.swift rename to Sources/OversizeMetricServices/Models/FinanceReport.swift diff --git a/Sources/OversizeAppStoreServices/Models/InstallReport.swift b/Sources/OversizeMetricServices/Models/InstallReport.swift similarity index 100% rename from Sources/OversizeAppStoreServices/Models/InstallReport.swift rename to Sources/OversizeMetricServices/Models/InstallReport.swift diff --git a/Sources/OversizeAppStoreServices/Models/SalesReports.swift b/Sources/OversizeMetricServices/Models/SalesReports.swift similarity index 100% rename from Sources/OversizeAppStoreServices/Models/SalesReports.swift rename to Sources/OversizeMetricServices/Models/SalesReports.swift diff --git a/Sources/OversizeAppStoreServices/Models/XcodeMetrics.swift b/Sources/OversizeMetricServices/Models/XcodeMetrics.swift similarity index 99% rename from Sources/OversizeAppStoreServices/Models/XcodeMetrics.swift rename to Sources/OversizeMetricServices/Models/XcodeMetrics.swift index dcbcd9a..8c0c8b0 100644 --- a/Sources/OversizeAppStoreServices/Models/XcodeMetrics.swift +++ b/Sources/OversizeMetricServices/Models/XcodeMetrics.swift @@ -1,5 +1,4 @@ import AppStoreAPI -import AppStoreConnect import Foundation public struct LocalXcodeMetrics: Codable, Equatable, Sendable { diff --git a/Sources/OversizeMetricServices/ServiceRegistering.swift b/Sources/OversizeMetricServices/ServiceRegistering.swift new file mode 100644 index 0000000..3e97b4c --- /dev/null +++ b/Sources/OversizeMetricServices/ServiceRegistering.swift @@ -0,0 +1,20 @@ +// +// Copyright © 2024 Alexander Romanov +// ServiceRegistering.swift, created on 13.07.2024 +// + +import Factory + +public extension Container { + var analyticsService: Factory { + self { AnalyticsService() } + } + + var salesAndFinanceService: Factory { + self { SalesAndFinanceService() } + } + + var perfPowerMetricsService: Factory { + self { PerfPowerMetricsService() } + } +} diff --git a/Sources/OversizeAppStoreServices/Services/AnalyticsService.swift b/Sources/OversizeMetricServices/Services/AnalyticsService.swift similarity index 99% rename from Sources/OversizeAppStoreServices/Services/AnalyticsService.swift rename to Sources/OversizeMetricServices/Services/AnalyticsService.swift index 58c27fd..a21a280 100644 --- a/Sources/OversizeAppStoreServices/Services/AnalyticsService.swift +++ b/Sources/OversizeMetricServices/Services/AnalyticsService.swift @@ -6,6 +6,7 @@ import AppStoreAPI import AppStoreConnect import Foundation +import OversizeAppStoreServices import OversizeCore import OversizeModels diff --git a/Sources/OversizeAppStoreServices/Services/PerfPowerMetricsService.swift b/Sources/OversizeMetricServices/Services/PerfPowerMetricsService.swift similarity index 97% rename from Sources/OversizeAppStoreServices/Services/PerfPowerMetricsService.swift rename to Sources/OversizeMetricServices/Services/PerfPowerMetricsService.swift index d8dc250..d1a840e 100644 --- a/Sources/OversizeAppStoreServices/Services/PerfPowerMetricsService.swift +++ b/Sources/OversizeMetricServices/Services/PerfPowerMetricsService.swift @@ -5,6 +5,7 @@ import AppStoreAPI import AppStoreConnect +import OversizeAppStoreServices import OversizeModels public actor PerfPowerMetricsService { diff --git a/Sources/OversizeAppStoreServices/Services/SalesAndFinanceService.swift b/Sources/OversizeMetricServices/Services/SalesAndFinanceService.swift similarity index 99% rename from Sources/OversizeAppStoreServices/Services/SalesAndFinanceService.swift rename to Sources/OversizeMetricServices/Services/SalesAndFinanceService.swift index 6612950..a5046b6 100644 --- a/Sources/OversizeAppStoreServices/Services/SalesAndFinanceService.swift +++ b/Sources/OversizeMetricServices/Services/SalesAndFinanceService.swift @@ -8,6 +8,7 @@ import AppStoreConnect import CodableCSV import Foundation import Gzip +import OversizeAppStoreServices import OversizeCore import OversizeModels diff --git a/Tests/OversizeAppStoreServicesTests/AppStoreAgeRatingTests.swift b/Tests/OversizeAppStoreServicesTests/AppStoreAgeRatingTests.swift new file mode 100644 index 0000000..f677d47 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/AppStoreAgeRatingTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// AppStoreAgeRatingTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct AppStoreAgeRatingTests { + @Test("AppStoreAgeRating should have same number of cases as AppStoreAPI") + func checkAppStoreAgeRatingCount() throws { + #expect(OversizeAppStoreServices.AppStoreAgeRating.allCases.count == AppStoreAPI.AppStoreAgeRating.allCases.count) + } + + @Test("AppStoreAgeRating should have the same cases as AppStoreAPI") + func checkAppStoreAgeRatingCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.AppStoreAgeRating.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.AppStoreAgeRating.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated AppStoreAgeRating cases do not match") + } + + @Test("AppStoreAgeRating should match raw values with AppStoreAPI") + func checkAppStoreAgeRatingRawValues() throws { + for rating in OversizeAppStoreServices.AppStoreAgeRating.allCases { + let generatedRating = AppStoreAPI.AppStoreAgeRating(rawValue: rating.rawValue) + #expect(generatedRating != nil, "No matching case in AppStoreAPI for \(rating.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/AppStoreLanguageTests.swift b/Tests/OversizeAppStoreServicesTests/AppStoreLanguageTests.swift new file mode 100644 index 0000000..24f7c45 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/AppStoreLanguageTests.swift @@ -0,0 +1,44 @@ +// +// Copyright © 2025 Alexander Romanov +// AppStoreLanguageTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct AppStoreLanguageTests { + @Test("Should have correct raw values") + func testRawValues() throws { + #expect(AppStoreLanguage.englishUS.rawValue == "en-US") + #expect(AppStoreLanguage.russian.rawValue == "ru") + #expect(AppStoreLanguage.japanese.rawValue == "ja") + #expect(AppStoreLanguage.chineseSimplified.rawValue == "zh-Hans") + #expect(AppStoreLanguage.spanishMEX.rawValue == "es-MX") + } + + @Test("Should have correct display names") + func testDisplayNames() throws { + #expect(AppStoreLanguage.englishUS.displayName == "English (US)") + #expect(AppStoreLanguage.russian.displayName == "Russian") + #expect(AppStoreLanguage.japanese.displayName == "Japanese") + #expect(AppStoreLanguage.chineseSimplified.displayName == "Chinese (Simplified)") + #expect(AppStoreLanguage.spanishMEX.displayName == "Spanish (Mexico)") + } + + @Test("Should have correct flag emojis") + func testFlagEmojis() throws { + #expect(AppStoreLanguage.englishUS.flagEmoji == "🇺🇸") + #expect(AppStoreLanguage.russian.flagEmoji == "🇷🇺") + #expect(AppStoreLanguage.japanese.flagEmoji == "🇯🇵") + #expect(AppStoreLanguage.chineseSimplified.flagEmoji == "🇨🇳") + #expect(AppStoreLanguage.spanishMEX.flagEmoji == "🇲🇽") + } + + @Test("Should conform to Identifiable correctly") + func testIdentifiable() throws { + #expect(AppStoreLanguage.englishUS.id == "en-US") + #expect(AppStoreLanguage.russian.id == "ru") + #expect(AppStoreLanguage.japanese.id == "ja") + } +} diff --git a/Tests/OversizeAppStoreServicesTests/AppStoreVersionStateTests.swift b/Tests/OversizeAppStoreServicesTests/AppStoreVersionStateTests.swift new file mode 100644 index 0000000..a7e409d --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/AppStoreVersionStateTests.swift @@ -0,0 +1,45 @@ +// +// Copyright © 2025 Alexander Romanov +// AppStoreAgeRatingTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct AppStoreVersionStateTests { + @Test("AppStoreVersionState should have same number of cases as AppStoreAPI") + func checkAppStoreVersionStateCount() throws { + #expect(OversizeAppStoreServices.AppStoreVersionState.allCases.count == AppStoreAPI.AppStoreVersionState.allCases.count) + } + + @Test("AppStoreVersionState should have the same cases as AppStoreAPI") + func checkAppStoreVersionStateCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.AppStoreVersionState.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.AppStoreVersionState.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated AppStoreVersionState cases do not match") + } + + @Test("AppStoreVersionState should match raw values with AppStoreAPI") + func checkAppStoreVersionStateRawValues() throws { + for state in OversizeAppStoreServices.AppStoreVersionState.allCases { + let generatedState = AppStoreAPI.AppStoreVersionState(rawValue: state.rawValue) + #expect(generatedState != nil, "No matching case in AppStoreAPI for \(state.rawValue)") + } + } + + @Test("AppStoreVersionState should have correct display names") + func checkAppStoreVersionStateDisplayNames() throws { + for state in OversizeAppStoreServices.AppStoreVersionState.allCases { + #expect(!state.displayName.isEmpty, "Display name should not be empty for \(state)") + } + } + + @Test("AppStoreVersionState should have valid status colors") + func checkAppStoreVersionStateColors() throws { + for state in OversizeAppStoreServices.AppStoreVersionState.allCases { + #expect(state.statusColor != nil, "Status color should not be nil for \(state)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/BuildAudienceTypeTests.swift b/Tests/OversizeAppStoreServicesTests/BuildAudienceTypeTests.swift new file mode 100644 index 0000000..d7774d7 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/BuildAudienceTypeTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// BuildAudienceTypeTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct BuildAudienceTypeTests { + @Test("BuildAudienceType should have same number of cases as AppStoreAPI") + func checkBuildAudienceTypeCount() throws { + #expect(OversizeAppStoreServices.BuildAudienceType.allCases.count == AppStoreAPI.BuildAudienceType.allCases.count) + } + + @Test("BuildAudienceType should have the same cases as AppStoreAPI") + func checkBuildAudienceTypeCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.BuildAudienceType.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.BuildAudienceType.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated BuildAudienceType cases do not match") + } + + @Test("BuildAudienceType should match raw values with AppStoreAPI") + func checkBuildAudienceTypeRawValues() throws { + for type in OversizeAppStoreServices.BuildAudienceType.allCases { + let generatedType = AppStoreAPI.BuildAudienceType(rawValue: type.rawValue) + #expect(generatedType != nil, "No matching case in AppStoreAPI for \(type.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/BundleIDPlatformTests.swift b/Tests/OversizeAppStoreServicesTests/BundleIDPlatformTests.swift new file mode 100644 index 0000000..ad83579 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/BundleIDPlatformTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// BundleIDPlatformTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct BundleIDPlatformTests { + @Test("BundleIDPlatform should have same number of cases as AppStoreAPI") + func checkBundleIDPlatformCount() throws { + #expect(OversizeAppStoreServices.BundleIDPlatform.allCases.count == AppStoreAPI.BundleIDPlatform.allCases.count) + } + + @Test("BundleIDPlatform should have the same cases as AppStoreAPI") + func checkBundleIDPlatformCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.BundleIDPlatform.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.BundleIDPlatform.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated BundleIDPlatform cases do not match") + } + + @Test("BundleIDPlatform should match raw values with AppStoreAPI") + func checkBundleIDPlatformRawValues() throws { + for platform in OversizeAppStoreServices.BundleIDPlatform.allCases { + let generatedPlatform = AppStoreAPI.BundleIDPlatform(rawValue: platform.rawValue) + #expect(generatedPlatform != nil, "No matching case in AppStoreAPI for \(platform.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/ContentRightsDeclarationTests.swift b/Tests/OversizeAppStoreServicesTests/ContentRightsDeclarationTests.swift new file mode 100644 index 0000000..0579867 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/ContentRightsDeclarationTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// ContentRightsDeclarationTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct ContentRightsDeclarationTests { + @Test("ContentRightsDeclaration should have same number of cases as AppStoreAPI") + func checkContentRightsDeclarationCount() throws { + #expect(OversizeAppStoreServices.ContentRightsDeclaration.allCases.count == AppStoreAPI.App.Attributes.ContentRightsDeclaration.allCases.count) + } + + @Test("ContentRightsDeclaration should have the same cases as AppStoreAPI") + func checkContentRightsDeclarationCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.ContentRightsDeclaration.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.App.Attributes.ContentRightsDeclaration.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated ContentRightsDeclaration cases do not match") + } + + @Test("ContentRightsDeclaration should match raw values with AppStoreAPI") + func checkContentRightsDeclarationRawValues() throws { + for declaration in OversizeAppStoreServices.ContentRightsDeclaration.allCases { + let generatedDeclaration = AppStoreAPI.App.Attributes.ContentRightsDeclaration(rawValue: declaration.rawValue) + #expect(generatedDeclaration != nil, "No matching case in AppStoreAPI for \(declaration.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/CustomerReviewsSortTests.swift b/Tests/OversizeAppStoreServicesTests/CustomerReviewsSortTests.swift new file mode 100644 index 0000000..428e3e9 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/CustomerReviewsSortTests.swift @@ -0,0 +1,38 @@ +// +// Copyright © 2025 Alexander Romanov +// CustomerReviewsSortTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct CustomerReviewsSortTests { + @Test("CustomerReviewsSort should have same number of cases as AppStoreAPI") + func checkCustomerReviewsSortCount() throws { + #expect(OversizeAppStoreServices.CustomerReviewsSort.allCases.count == AppStoreAPI.Resources.V1.Apps.WithID.CustomerReviews.Sort.allCases.count) + } + + @Test("CustomerReviewsSort should have the same cases as AppStoreAPI") + func checkCustomerReviewsSortCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.CustomerReviewsSort.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.Resources.V1.Apps.WithID.CustomerReviews.Sort.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated CustomerReviewsSort cases do not match") + } + + @Test("CustomerReviewsSort should match raw values with AppStoreAPI") + func checkCustomerReviewsSortRawValues() throws { + for sort in OversizeAppStoreServices.CustomerReviewsSort.allCases { + let generatedSort = AppStoreAPI.Resources.V1.Apps.WithID.CustomerReviews.Sort(rawValue: sort.rawValue) + #expect(generatedSort != nil, "No matching case in AppStoreAPI for \(sort.rawValue)") + } + } + + @Test("CustomerReviewsSort should have valid display names") + func checkCustomerReviewsSortDisplayNames() throws { + for sort in OversizeAppStoreServices.CustomerReviewsSort.allCases { + #expect(!sort.displayName.isEmpty, "Display name should not be empty for \(sort)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/InAppPurchaseTypeTests.swift b/Tests/OversizeAppStoreServicesTests/InAppPurchaseTypeTests.swift new file mode 100644 index 0000000..1aa6cfb --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/InAppPurchaseTypeTests.swift @@ -0,0 +1,50 @@ +// +// Copyright © 2025 Alexander Romanov +// TypesTests.swift, created on 05.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct InAppPurchaseTypeTests { + // MARK: - InAppPurchaseType Tests + + @Test("InAppPurchaseType should have same number of cases as AppStoreAPI") + func checkInAppPurchaseTypeCount() throws { + #expect(OversizeAppStoreServices.InAppPurchaseType.allCases.count == AppStoreAPI.InAppPurchaseType.allCases.count) + } + + @Test("InAppPurchaseType should have the same cases as AppStoreAPI") + func checkInAppPurchaseTypeCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.InAppPurchaseType.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.InAppPurchaseType.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated InAppPurchaseType cases do not match") + } + + @Test("InAppPurchaseType values should match expected raw values and display names") + func checkInAppPurchaseTypeValues() throws { + for type in OversizeAppStoreServices.InAppPurchaseType.allCases { + switch type { + case .consumable: + #expect(type.rawValue == "CONSUMABLE") + #expect(type.displayName == "Consumable") + case .nonConsumable: + #expect(type.rawValue == "NON_CONSUMABLE") + #expect(type.displayName == "Non-Consumable") + case .nonRenewingSubscription: + #expect(type.rawValue == "NON_RENEWING_SUBSCRIPTION") + #expect(type.displayName == "Non-Renewing Subscription") + } + } + } + + @Test("InAppPurchaseType should match raw values with AppStoreAPI") + func checkInAppPurchaseTypeRawValues() throws { + for type in OversizeAppStoreServices.InAppPurchaseType.allCases { + let generatedType = AppStoreAPI.InAppPurchaseType(rawValue: type.rawValue) + #expect(generatedType != nil, "No matching case in AppStoreAPI for \(type.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/OversizeAppStoreServicesTests.swift b/Tests/OversizeAppStoreServicesTests/OversizeAppStoreServicesTests.swift deleted file mode 100644 index c82580b..0000000 --- a/Tests/OversizeAppStoreServicesTests/OversizeAppStoreServicesTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -@testable import OversizeAppStoreServices -import XCTest - -final class OversizeAppStoreServicesTests: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest - - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } -} diff --git a/Tests/OversizeAppStoreServicesTests/PlatformTests.swift b/Tests/OversizeAppStoreServicesTests/PlatformTests.swift new file mode 100644 index 0000000..195d2b0 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/PlatformTests.swift @@ -0,0 +1,45 @@ +// +// Copyright © 2025 Alexander Romanov +// PlatformTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct PlatformTests { + @Test("Platform should have same number of cases as AppStoreAPI") + func checkPlatformCount() throws { + #expect(OversizeAppStoreServices.Platform.allCases.count == AppStoreAPI.Platform.allCases.count) + } + + @Test("Platform should have the same cases as AppStoreAPI") + func checkPlatformCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.Platform.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.Platform.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated Platform cases do not match") + } + + @Test("Platform should match raw values with AppStoreAPI") + func checkPlatformRawValues() throws { + for platform in OversizeAppStoreServices.Platform.allCases { + let generatedPlatform = AppStoreAPI.Platform(rawValue: platform.rawValue) + #expect(generatedPlatform != nil, "No matching case in AppStoreAPI for \(platform.rawValue)") + } + } + + @Test("Platform should have valid display names") + func checkPlatformDisplayNames() throws { + for platform in OversizeAppStoreServices.Platform.allCases { + #expect(!platform.displayName.isEmpty, "Display name should not be empty for \(platform)") + } + } + + @Test("Platform should have valid icons") + func checkPlatformIcons() throws { + for platform in OversizeAppStoreServices.Platform.allCases { + #expect(platform.icon != nil, "Icon should not be nil for \(platform)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/ProcessingStateTests.swift b/Tests/OversizeAppStoreServicesTests/ProcessingStateTests.swift new file mode 100644 index 0000000..d01b590 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/ProcessingStateTests.swift @@ -0,0 +1,38 @@ +// +// Copyright © 2025 Alexander Romanov +// ProcessingStateTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct ProcessingStateTests { + @Test("ProcessingState should have same number of cases as AppStoreAPI") + func checkProcessingStateCount() throws { + #expect(OversizeAppStoreServices.ProcessingState.allCases.count == AppStoreAPI.Build.Attributes.ProcessingState.allCases.count) + } + + @Test("ProcessingState should have the same cases as AppStoreAPI") + func checkProcessingStateCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.ProcessingState.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.Build.Attributes.ProcessingState.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated ProcessingState cases do not match") + } + + @Test("ProcessingState should match raw values with AppStoreAPI") + func checkProcessingStateRawValues() throws { + for state in OversizeAppStoreServices.ProcessingState.allCases { + let generatedState = AppStoreAPI.Build.Attributes.ProcessingState(rawValue: state.rawValue) + #expect(generatedState != nil, "No matching case in AppStoreAPI for \(state.rawValue)") + } + } + + @Test("ProcessingState should have valid display names") + func checkProcessingStateDisplayNames() throws { + for state in OversizeAppStoreServices.ProcessingState.allCases { + #expect(!state.displayName.isEmpty, "Display name should not be empty for \(state)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/ReleaseTypeTests.swift b/Tests/OversizeAppStoreServicesTests/ReleaseTypeTests.swift new file mode 100644 index 0000000..24564f4 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/ReleaseTypeTests.swift @@ -0,0 +1,38 @@ +// +// Copyright © 2025 Alexander Romanov +// ReleaseTypeTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct ReleaseTypeTests { + @Test("ReleaseType should have same number of cases as AppStoreAPI") + func checkReleaseTypeCount() throws { + #expect(OversizeAppStoreServices.ReleaseType.allCases.count == AppStoreAPI.AppStoreVersion.Attributes.ReleaseType.allCases.count) + } + + @Test("ReleaseType should have the same cases as AppStoreAPI") + func checkReleaseTypeCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.ReleaseType.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.AppStoreVersion.Attributes.ReleaseType.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated ReleaseType cases do not match") + } + + @Test("ReleaseType should match raw values with AppStoreAPI") + func checkReleaseTypeRawValues() throws { + for type in OversizeAppStoreServices.ReleaseType.allCases { + let generatedType = AppStoreAPI.AppStoreVersion.Attributes.ReleaseType(rawValue: type.rawValue) + #expect(generatedType != nil, "No matching case in AppStoreAPI for \(type.rawValue)") + } + } + + @Test("ReleaseType should have valid display names") + func checkReleaseTypeDisplayNames() throws { + for type in OversizeAppStoreServices.ReleaseType.allCases { + #expect(!type.displayName.isEmpty, "Display name should not be empty for \(type)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/ReviewTypeTests.swift b/Tests/OversizeAppStoreServicesTests/ReviewTypeTests.swift new file mode 100644 index 0000000..efc916e --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/ReviewTypeTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// ReviewTypeTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct ReviewTypeTests { + @Test("ReviewType should have same number of cases as AppStoreAPI") + func checkReviewTypeCount() throws { + #expect(OversizeAppStoreServices.ReviewType.allCases.count == AppStoreAPI.AppStoreVersion.Attributes.ReviewType.allCases.count) + } + + @Test("ReviewType should have the same cases as AppStoreAPI") + func checkReviewTypeCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.ReviewType.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.AppStoreVersion.Attributes.ReviewType.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated ReviewType cases do not match") + } + + @Test("ReviewType should match raw values with AppStoreAPI") + func checkReviewTypeRawValues() throws { + for type in OversizeAppStoreServices.ReviewType.allCases { + let generatedType = AppStoreAPI.AppStoreVersion.Attributes.ReviewType(rawValue: type.rawValue) + #expect(generatedType != nil, "No matching case in AppStoreAPI for \(type.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/SubscriptionCustomerEligibilityTests.swift b/Tests/OversizeAppStoreServicesTests/SubscriptionCustomerEligibilityTests.swift new file mode 100644 index 0000000..d29f4bc --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/SubscriptionCustomerEligibilityTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionCustomerEligibilityTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct SubscriptionCustomerEligibilityTests { + @Test("SubscriptionCustomerEligibility should have same number of cases as AppStoreAPI") + func checkSubscriptionCustomerEligibilityCount() throws { + #expect(OversizeAppStoreServices.SubscriptionCustomerEligibility.allCases.count == AppStoreAPI.SubscriptionCustomerEligibility.allCases.count) + } + + @Test("SubscriptionCustomerEligibility should have the same cases as AppStoreAPI") + func checkSubscriptionCustomerEligibilityCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.SubscriptionCustomerEligibility.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.SubscriptionCustomerEligibility.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated SubscriptionCustomerEligibility cases do not match") + } + + @Test("SubscriptionCustomerEligibility should match raw values with AppStoreAPI") + func checkSubscriptionCustomerEligibilityRawValues() throws { + for eligibility in OversizeAppStoreServices.SubscriptionCustomerEligibility.allCases { + let generatedEligibility = AppStoreAPI.SubscriptionCustomerEligibility(rawValue: eligibility.rawValue) + #expect(generatedEligibility != nil, "No matching case in AppStoreAPI for \(eligibility.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/SubscriptionOfferDurationTests.swift b/Tests/OversizeAppStoreServicesTests/SubscriptionOfferDurationTests.swift new file mode 100644 index 0000000..b15f069 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/SubscriptionOfferDurationTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferDurationTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct SubscriptionOfferDurationTests { + @Test("SubscriptionOfferDuration should have same number of cases as AppStoreAPI") + func checkSubscriptionOfferDurationCount() throws { + #expect(OversizeAppStoreServices.SubscriptionOfferDuration.allCases.count == AppStoreAPI.SubscriptionOfferDuration.allCases.count) + } + + @Test("SubscriptionOfferDuration should have the same cases as AppStoreAPI") + func checkSubscriptionOfferDurationCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.SubscriptionOfferDuration.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.SubscriptionOfferDuration.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated SubscriptionOfferDuration cases do not match") + } + + @Test("SubscriptionOfferDuration should match raw values with AppStoreAPI") + func checkSubscriptionOfferDurationRawValues() throws { + for duration in OversizeAppStoreServices.SubscriptionOfferDuration.allCases { + let generatedDuration = AppStoreAPI.SubscriptionOfferDuration(rawValue: duration.rawValue) + #expect(generatedDuration != nil, "No matching case in AppStoreAPI for \(duration.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/SubscriptionOfferEligibilityTests.swift b/Tests/OversizeAppStoreServicesTests/SubscriptionOfferEligibilityTests.swift new file mode 100644 index 0000000..ebe6671 --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/SubscriptionOfferEligibilityTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferEligibilityTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct SubscriptionOfferEligibilityTests { + @Test("SubscriptionOfferEligibility should have same number of cases as AppStoreAPI") + func checkSubscriptionOfferEligibilityCount() throws { + #expect(OversizeAppStoreServices.SubscriptionOfferEligibility.allCases.count == AppStoreAPI.SubscriptionOfferEligibility.allCases.count) + } + + @Test("SubscriptionOfferEligibility should have the same cases as AppStoreAPI") + func checkSubscriptionOfferEligibilityCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.SubscriptionOfferEligibility.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.SubscriptionOfferEligibility.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated SubscriptionOfferEligibility cases do not match") + } + + @Test("SubscriptionOfferEligibility should match raw values with AppStoreAPI") + func checkSubscriptionOfferEligibilityRawValues() throws { + for eligibility in OversizeAppStoreServices.SubscriptionOfferEligibility.allCases { + let generatedEligibility = AppStoreAPI.SubscriptionOfferEligibility(rawValue: eligibility.rawValue) + #expect(generatedEligibility != nil, "No matching case in AppStoreAPI for \(eligibility.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/SubscriptionOfferModeTests.swift b/Tests/OversizeAppStoreServicesTests/SubscriptionOfferModeTests.swift new file mode 100644 index 0000000..10a4f1a --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/SubscriptionOfferModeTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionOfferModeTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct SubscriptionOfferModeTests { + @Test("SubscriptionOfferMode should have same number of cases as AppStoreAPI") + func checkSubscriptionOfferModeCount() throws { + #expect(OversizeAppStoreServices.SubscriptionOfferMode.allCases.count == AppStoreAPI.SubscriptionOfferMode.allCases.count) + } + + @Test("SubscriptionOfferMode should have the same cases as AppStoreAPI") + func checkSubscriptionOfferModeCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.SubscriptionOfferMode.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.SubscriptionOfferMode.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated SubscriptionOfferMode cases do not match") + } + + @Test("SubscriptionOfferMode should match raw values with AppStoreAPI") + func checkSubscriptionOfferModeRawValues() throws { + for mode in OversizeAppStoreServices.SubscriptionOfferMode.allCases { + let generatedMode = AppStoreAPI.SubscriptionOfferMode(rawValue: mode.rawValue) + #expect(generatedMode != nil, "No matching case in AppStoreAPI for \(mode.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/SubscriptionPeriodTests.swift b/Tests/OversizeAppStoreServicesTests/SubscriptionPeriodTests.swift new file mode 100644 index 0000000..e363b7d --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/SubscriptionPeriodTests.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Alexander Romanov +// SubscriptionPeriodTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct SubscriptionPeriodTests { + @Test("SubscriptionPeriod should have same number of cases as AppStoreAPI") + func checkSubscriptionPeriodCount() throws { + #expect(OversizeAppStoreServices.SubscriptionPeriod.allCases.count == AppStoreAPI.Subscription.Attributes.SubscriptionPeriod.allCases.count) + } + + @Test("SubscriptionPeriod should have the same cases as AppStoreAPI") + func checkSubscriptionPeriodCasesMatch() throws { + let localCases = Set(OversizeAppStoreServices.SubscriptionPeriod.allCases.map { $0.rawValue }) + let generatedCases = Set(AppStoreAPI.Subscription.Attributes.SubscriptionPeriod.allCases.map { $0.rawValue }) + + #expect(localCases == generatedCases, "Local and generated SubscriptionPeriod cases do not match") + } + + @Test("SubscriptionPeriod should match raw values with AppStoreAPI") + func checkSubscriptionPeriodRawValues() throws { + for period in OversizeAppStoreServices.SubscriptionPeriod.allCases { + let generatedPeriod = AppStoreAPI.Subscription.Attributes.SubscriptionPeriod(rawValue: period.rawValue) + #expect(generatedPeriod != nil, "No matching case in AppStoreAPI for \(period.rawValue)") + } + } +} diff --git a/Tests/OversizeAppStoreServicesTests/TerritoryRegionTests.swift b/Tests/OversizeAppStoreServicesTests/TerritoryRegionTests.swift new file mode 100644 index 0000000..9adc84b --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/TerritoryRegionTests.swift @@ -0,0 +1,63 @@ +// +// Copyright © 2025 Alexander Romanov +// TerritoryRegionTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct TerritoryRegionTests { + @Test("Should initialize with valid territory code") + func testInitWithValidCode() throws { + #expect(TerritoryRegion(territoryCode: .usa) == .unitedStatesAndCanada) + #expect(TerritoryRegion(territoryCode: .fra) == .europe) + #expect(TerritoryRegion(territoryCode: .ind) == .africaMiddleEastIndia) + #expect(TerritoryRegion(territoryCode: .mex) == .latinAmericaCaribbean) + #expect(TerritoryRegion(territoryCode: .jpn) == .asiaPacific) + } + + @Test("Should initialize with valid country ID") + func testInitWithValidCountryID() throws { + #expect(TerritoryRegion(countryID: "USA") == .unitedStatesAndCanada) + #expect(TerritoryRegion(countryID: "FRA") == .europe) + #expect(TerritoryRegion(countryID: "IND") == .africaMiddleEastIndia) + #expect(TerritoryRegion(countryID: "MEX") == .latinAmericaCaribbean) + #expect(TerritoryRegion(countryID: "JPN") == .asiaPacific) + } + + @Test("Should handle invalid country ID") + func testInitWithInvalidCountryID() throws { + #expect(TerritoryRegion(countryID: "INVALID") == .unknown) + #expect(TerritoryRegion(countryID: "") == .unknown) + #expect(TerritoryRegion(countryID: "123") == .unknown) + } + + @Test("AllCases should contain all main regions") + func testAllCases() throws { + let expectedCount = 5 // Excluding .unknown + #expect(TerritoryRegion.allCases.count == expectedCount) + #expect(TerritoryRegion.allCases.contains(.unitedStatesAndCanada)) + #expect(TerritoryRegion.allCases.contains(.europe)) + #expect(TerritoryRegion.allCases.contains(.africaMiddleEastIndia)) + #expect(TerritoryRegion.allCases.contains(.latinAmericaCaribbean)) + #expect(TerritoryRegion.allCases.contains(.asiaPacific)) + #expect(!TerritoryRegion.allCases.contains(.unknown)) + } + + @Test("Should have correct raw values") + func testRawValues() throws { + #expect(TerritoryRegion.unitedStatesAndCanada.rawValue == "The United States and Canada") + #expect(TerritoryRegion.europe.rawValue == "Europe") + #expect(TerritoryRegion.africaMiddleEastIndia.rawValue == "Africa, Middle East, and India") + #expect(TerritoryRegion.latinAmericaCaribbean.rawValue == "Latin America and the Caribbean") + #expect(TerritoryRegion.asiaPacific.rawValue == "Asia Pacific") + #expect(TerritoryRegion.unknown.rawValue == "Unknown Region") + } + + @Test("Should return .unknown for unhandled territory code") + func testInitWithUnhandledTerritoryCode() throws { + // TerritoryCode.bgd ("BGD" for Bangladesh) is not handled explicitly in TerritoryRegion initializer + #expect(TerritoryRegion(territoryCode: .bgd) == .unknown) + } +} diff --git a/Tests/OversizeAppStoreServicesTests/TerritoryTests.swift b/Tests/OversizeAppStoreServicesTests/TerritoryTests.swift new file mode 100644 index 0000000..d3ce67a --- /dev/null +++ b/Tests/OversizeAppStoreServicesTests/TerritoryTests.swift @@ -0,0 +1,93 @@ +// +// Copyright © 2025 Alexander Romanov +// TerritoryTests.swift, created on 06.02.2025 +// + +import AppStoreAPI +@testable import OversizeAppStoreServices +import Testing + +@Suite struct TerritoryTests { + @Test("Should initialize correctly from valid schema") + func testInitWithValidSchema() throws { + let schema = AppStoreAPI.Territory( + id: "USA", + attributes: .init(currency: "USD") + ) + + let territory = Territory(schema: schema) + + #expect(territory != nil) + #expect(territory?.id == "USA") + #expect(territory?.currency.identifier == "USD") + #expect(territory?.code == .usa) + #expect(territory?.region == .unitedStatesAndCanada) + } + + @Test("Should return nil for invalid schema") + func testInitWithInvalidSchema() throws { + let invalidSchema = AppStoreAPI.Territory( + id: "INVALID", + attributes: nil + ) + + let territory = Territory(schema: invalidSchema) + #expect(territory == nil) + } + + @Test("Should have correct display name") + func testDisplayName() throws { + let schema = AppStoreAPI.Territory( + id: "USA", + attributes: .init(currency: "USD") + ) + + let territory = Territory(schema: schema) + #expect(territory?.displayName == TerritoryCode.usa.displayName) + } + + @Test("Should have correct display flag") + func testDisplayFlag() throws { + let schema = AppStoreAPI.Territory( + id: "USA", + attributes: .init(currency: "USD") + ) + + let territory = Territory(schema: schema) + #expect(territory?.displayFlag == TerritoryCode.usa.flagEmoji) + } + + @Test("Should handle different currencies correctly") + func testCurrencyHandling() throws { + let territories = [ + ("USA", "USD"), + ("GBR", "GBP"), + ("DEU", "EUR"), + ("JPN", "JPY"), + ] + + for (id, currency) in territories { + let schema = AppStoreAPI.Territory( + id: id, + attributes: .init(currency: currency) + ) + + let territory = Territory(schema: schema) + #expect(territory?.currency.identifier == currency) + } + } + + @Test("Should initialize schema with unhandled region as .unknown") + func testInitWithSchemaUnhandledRegion() throws { + // TerritoryCode.bgd ("BGD" - Bangladesh) is not handled in TerritoryRegion initializer, + // so the region should be .unknown. + let schema = AppStoreAPI.Territory( + id: "BGD", + attributes: .init(currency: "BDT") + ) + + let territory = Territory(schema: schema) + #expect(territory != nil) + #expect(territory?.region == .unknown) + } +}