From c4df6829ca3b772bddede3ec415d6d5736bea4e6 Mon Sep 17 00:00:00 2001 From: slamhan Date: Wed, 23 Sep 2026 11:39:46 +0800 Subject: [PATCH] fix(offers): show each offer price's territory and price point The four offer price lists (IAP offer codes, subscription offer codes, promotional offers, win-back offers) returned entries with territory and price point empty. The mappers read them from relationship data, which Apple only sends when the request asks for it with include=. The requests now include territory and the price point. Verified live: a promotional offer priced in 175 territories lists all 175 with territory and the exact price point set, over CLI and REST. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + .../SDKInAppPurchaseOfferCodeRepository.swift | 5 +++-- .../SDKSubscriptionOfferCodeRepository.swift | 5 +++-- .../SDKSubscriptionPromotionalOfferRepository.swift | 5 +++-- .../WinBackOffers/SDKWinBackOfferRepository.swift | 5 +++-- .../SDKInAppPurchaseOfferCodeRepositoryTests.swift | 12 ++++++++++++ .../SDKSubscriptionOfferCodeRepositoryTests.swift | 12 ++++++++++++ ...SubscriptionPromotionalOfferRepositoryTests.swift | 12 ++++++++++++ .../SDKWinBackOfferRepositoryTests.swift | 12 ++++++++++++ 9 files changed, 61 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb86f35..77274f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`asc subscriptions prices set-batch` sends one request instead of one per territory** — it now sends a single `PATCH /v1/subscriptions/{id}` with every price inlined (`SubscriptionPriceInlineCreate`) instead of one `POST /v1/subscriptionPrices` per territory. Pricing all 175 territories took 175 sequential POSTs (over a minute per subscription), and a failure partway through left some territories priced and others not; Apple now applies the batch all-or-nothing. The REST `POST /api/v1/subscriptions/:id/prices` route benefits too, since it uses the same repository method. - **Subscription price schedules no longer drop manual prices after the first 50** — `subscription-price-schedule get` (and the read-back after `set-batch`) fetched `/v1/subscriptions/{id}/prices` without a `limit`, so only Apple's default first page came back and the other territories were filled with the first price's equalizations, showing wrong prices where custom per-territory prices were set. It now requests `limit=200` and follows the pagination cursor. - **The other per-territory price lists no longer stop at 50** — the same truncation affected `iap-price-schedule get` (manual prices), `iap-offer-codes prices list`, `subscription-offer-codes prices list`, `subscription-promotional-offers prices list` and `win-back-offers prices list`. They called Apple without a `limit` and read one page, so any territory past Apple's default 50 was missing. They now request `limit=200` and follow every page through a shared `APIClient.requestAllPages(_:nextCursor:)` helper, which `subscription-price-schedule get` uses too. +- **Offer price lists now show each price's territory and price point** — `iap-offer-codes prices list`, `subscription-offer-codes prices list`, `subscription-promotional-offers prices list` and `win-back-offers prices list` returned entries with only `id` and the offer ID, because Apple sends a price's territory and price point only when the request asks for them with `include=`. The requests now include both, so `territory` and the price-point ID are filled in, over CLI and REST. --- diff --git a/Sources/Infrastructure/Apps/InAppPurchases/OfferCodes/SDKInAppPurchaseOfferCodeRepository.swift b/Sources/Infrastructure/Apps/InAppPurchases/OfferCodes/SDKInAppPurchaseOfferCodeRepository.swift index ed6c0fb4..47b084be 100644 --- a/Sources/Infrastructure/Apps/InAppPurchases/OfferCodes/SDKInAppPurchaseOfferCodeRepository.swift +++ b/Sources/Infrastructure/Apps/InAppPurchases/OfferCodes/SDKInAppPurchaseOfferCodeRepository.swift @@ -84,9 +84,10 @@ public struct SDKInAppPurchaseOfferCodeRepository: InAppPurchaseOfferCodeReposit // MARK: - Prices public func listPrices(offerCodeId: String) async throws -> [Domain.InAppPurchaseOfferCodePrice] { - // One price per territory — follow every page to get all 175. + // One price per territory — follow every page to get all 175. `include` makes Apple + // return each price's territory and price point linkage, which the mapper reads. let pages = try await client.requestAllPages( - APIEndpoint.v1.inAppPurchaseOfferCodes.id(offerCodeId).prices.get(parameters: .init(limit: 200)), + APIEndpoint.v1.inAppPurchaseOfferCodes.id(offerCodeId).prices.get(parameters: .init(limit: 200, include: [.territory, .pricePoint])), nextCursor: { $0.meta?.paging.nextCursor } ) return pages.flatMap(\.data).map { mapPrice($0, offerCodeId: offerCodeId) } diff --git a/Sources/Infrastructure/Apps/Subscriptions/OfferCodes/SDKSubscriptionOfferCodeRepository.swift b/Sources/Infrastructure/Apps/Subscriptions/OfferCodes/SDKSubscriptionOfferCodeRepository.swift index 6c8fa385..e25567dc 100644 --- a/Sources/Infrastructure/Apps/Subscriptions/OfferCodes/SDKSubscriptionOfferCodeRepository.swift +++ b/Sources/Infrastructure/Apps/Subscriptions/OfferCodes/SDKSubscriptionOfferCodeRepository.swift @@ -189,9 +189,10 @@ public struct SDKSubscriptionOfferCodeRepository: SubscriptionOfferCodeRepositor // MARK: - Prices public func listPrices(offerCodeId: String) async throws -> [Domain.SubscriptionOfferCodePrice] { - // One price per territory — follow every page to get all 175. + // One price per territory — follow every page to get all 175. `include` makes Apple + // return each price's territory and price point linkage, which the mapper reads. let pages = try await client.requestAllPages( - APIEndpoint.v1.subscriptionOfferCodes.id(offerCodeId).prices.get(parameters: .init(limit: 200)), + APIEndpoint.v1.subscriptionOfferCodes.id(offerCodeId).prices.get(parameters: .init(limit: 200, include: [.territory, .subscriptionPricePoint])), nextCursor: { $0.meta?.paging.nextCursor } ) return pages.flatMap(\.data).map { mapPrice($0, offerCodeId: offerCodeId) } diff --git a/Sources/Infrastructure/Apps/Subscriptions/PromotionalOffers/SDKSubscriptionPromotionalOfferRepository.swift b/Sources/Infrastructure/Apps/Subscriptions/PromotionalOffers/SDKSubscriptionPromotionalOfferRepository.swift index 7145a40c..db6c6f66 100644 --- a/Sources/Infrastructure/Apps/Subscriptions/PromotionalOffers/SDKSubscriptionPromotionalOfferRepository.swift +++ b/Sources/Infrastructure/Apps/Subscriptions/PromotionalOffers/SDKSubscriptionPromotionalOfferRepository.swift @@ -69,9 +69,10 @@ public struct SDKSubscriptionPromotionalOfferRepository: SubscriptionPromotional } public func listPrices(offerId: String) async throws -> [Domain.SubscriptionPromotionalOfferPrice] { - // One price per territory — follow every page to get all 175. + // One price per territory — follow every page to get all 175. `include` makes Apple + // return each price's territory and price point linkage, which the mapper reads. let pages = try await client.requestAllPages( - APIEndpoint.v1.subscriptionPromotionalOffers.id(offerId).prices.get(parameters: .init(limit: 200)), + APIEndpoint.v1.subscriptionPromotionalOffers.id(offerId).prices.get(parameters: .init(limit: 200, include: [.territory, .subscriptionPricePoint])), nextCursor: { $0.meta?.paging.nextCursor } ) return pages.flatMap(\.data).map { mapPrice($0, offerId: offerId) } diff --git a/Sources/Infrastructure/Apps/Subscriptions/WinBackOffers/SDKWinBackOfferRepository.swift b/Sources/Infrastructure/Apps/Subscriptions/WinBackOffers/SDKWinBackOfferRepository.swift index 1f82aff7..62d64256 100644 --- a/Sources/Infrastructure/Apps/Subscriptions/WinBackOffers/SDKWinBackOfferRepository.swift +++ b/Sources/Infrastructure/Apps/Subscriptions/WinBackOffers/SDKWinBackOfferRepository.swift @@ -158,9 +158,10 @@ public struct SDKWinBackOfferRepository: WinBackOfferRepository, @unchecked Send } public func listPrices(offerId: String) async throws -> [Domain.WinBackOfferPrice] { - // One price per territory — follow every page to get all 175. + // One price per territory — follow every page to get all 175. `include` makes Apple + // return each price's territory and price point linkage, which the mapper reads. let pages = try await client.requestAllPages( - APIEndpoint.v1.winBackOffers.id(offerId).prices.get(parameters: .init(limit: 200)), + APIEndpoint.v1.winBackOffers.id(offerId).prices.get(parameters: .init(limit: 200, include: [.territory, .subscriptionPricePoint])), nextCursor: { $0.meta?.paging.nextCursor } ) return pages.flatMap(\.data).map { mapPrice($0, offerId: offerId) } diff --git a/Tests/InfrastructureTests/Apps/InAppPurchases/OfferCodes/SDKInAppPurchaseOfferCodeRepositoryTests.swift b/Tests/InfrastructureTests/Apps/InAppPurchases/OfferCodes/SDKInAppPurchaseOfferCodeRepositoryTests.swift index 6474af94..ac018148 100644 --- a/Tests/InfrastructureTests/Apps/InAppPurchases/OfferCodes/SDKInAppPurchaseOfferCodeRepositoryTests.swift +++ b/Tests/InfrastructureTests/Apps/InAppPurchases/OfferCodes/SDKInAppPurchaseOfferCodeRepositoryTests.swift @@ -403,4 +403,16 @@ struct SDKInAppPurchaseOfferCodeRepositoryTests { #expect(result.last?.territory == "T174") #expect(result.last?.pricePointId == "pp-174") } + + @Test func `offer code prices ask Apple for each price's territory and price point`() async throws { + // Apple only links each price to its territory and price point when asked via `include`. + let stub = StubAPIClient() + stub.willReturn(InAppPurchaseOfferPricesResponse(data: [], links: .init(this: ""))) + + let repo = SDKInAppPurchaseOfferCodeRepository(client: stub) + _ = try await repo.listPrices(offerCodeId: "oc-1") + + let query = Dictionary(uniqueKeysWithValues: (stub.lastQuery ?? []).map { ($0.0, $0.1 ?? "") }) + #expect(query["include"] == "territory,pricePoint") + } } diff --git a/Tests/InfrastructureTests/Apps/Subscriptions/OfferCodes/SDKSubscriptionOfferCodeRepositoryTests.swift b/Tests/InfrastructureTests/Apps/Subscriptions/OfferCodes/SDKSubscriptionOfferCodeRepositoryTests.swift index 8afb363e..990db551 100644 --- a/Tests/InfrastructureTests/Apps/Subscriptions/OfferCodes/SDKSubscriptionOfferCodeRepositoryTests.swift +++ b/Tests/InfrastructureTests/Apps/Subscriptions/OfferCodes/SDKSubscriptionOfferCodeRepositoryTests.swift @@ -438,4 +438,16 @@ struct SDKSubscriptionOfferCodeRepositoryTests { #expect(result.last?.territory == "T174") #expect(result.last?.subscriptionPricePointId == "pp-174") } + + @Test func `offer code prices ask Apple for each price's territory and price point`() async throws { + // Apple only links each price to its territory and price point when asked via `include`. + let stub = StubAPIClient() + stub.willReturn(SubscriptionOfferCodePricesResponse(data: [], links: .init(this: ""))) + + let repo = SDKSubscriptionOfferCodeRepository(client: stub) + _ = try await repo.listPrices(offerCodeId: "oc-1") + + let query = Dictionary(uniqueKeysWithValues: (stub.lastQuery ?? []).map { ($0.0, $0.1 ?? "") }) + #expect(query["include"] == "territory,subscriptionPricePoint") + } } diff --git a/Tests/InfrastructureTests/Apps/Subscriptions/SDKSubscriptionPromotionalOfferRepositoryTests.swift b/Tests/InfrastructureTests/Apps/Subscriptions/SDKSubscriptionPromotionalOfferRepositoryTests.swift index 20582ad9..69c5cbb6 100644 --- a/Tests/InfrastructureTests/Apps/Subscriptions/SDKSubscriptionPromotionalOfferRepositoryTests.swift +++ b/Tests/InfrastructureTests/Apps/Subscriptions/SDKSubscriptionPromotionalOfferRepositoryTests.swift @@ -133,4 +133,16 @@ struct SDKSubscriptionPromotionalOfferRepositoryTests { #expect(result.last?.territory == "T174") #expect(result.last?.subscriptionPricePointId == "pp-174") } + + @Test func `promotional offer prices ask Apple for each price's territory and price point`() async throws { + // Apple only links each price to its territory and price point when asked via `include`. + let stub = StubAPIClient() + stub.willReturn(SubscriptionPromotionalOfferPricesResponse(data: [], links: .init(this: ""))) + + let repo = SDKSubscriptionPromotionalOfferRepository(client: stub) + _ = try await repo.listPrices(offerId: "po-1") + + let query = Dictionary(uniqueKeysWithValues: (stub.lastQuery ?? []).map { ($0.0, $0.1 ?? "") }) + #expect(query["include"] == "territory,subscriptionPricePoint") + } } diff --git a/Tests/InfrastructureTests/Apps/Subscriptions/SDKWinBackOfferRepositoryTests.swift b/Tests/InfrastructureTests/Apps/Subscriptions/SDKWinBackOfferRepositoryTests.swift index 8bc096e9..3a9a8b7c 100644 --- a/Tests/InfrastructureTests/Apps/Subscriptions/SDKWinBackOfferRepositoryTests.swift +++ b/Tests/InfrastructureTests/Apps/Subscriptions/SDKWinBackOfferRepositoryTests.swift @@ -192,4 +192,16 @@ struct SDKWinBackOfferRepositoryTests { #expect(result.last?.territory == "T174") #expect(result.last?.subscriptionPricePointId == "pp-174") } + + @Test func `win-back offer prices ask Apple for each price's territory and price point`() async throws { + // Apple only links each price to its territory and price point when asked via `include`. + let stub = StubAPIClient() + stub.willReturn(WinBackOfferPricesResponse(data: [], links: .init(this: ""))) + + let repo = SDKWinBackOfferRepository(client: stub) + _ = try await repo.listPrices(offerId: "wb-1") + + let query = Dictionary(uniqueKeysWithValues: (stub.lastQuery ?? []).map { ($0.0, $0.1 ?? "") }) + #expect(query["include"] == "territory,subscriptionPricePoint") + } }