Skip to content

Commit 5d1e4a0

Browse files
committed
feat(prices): add REST endpoints for IAP and subscription price setting
- Add IAPPricesController for setting manual base price of IAPs - Add SubscriptionPricesController for batch-setting subscription prices - Update RESTRoutes to include new price-setting controllers - Add corresponding test command entries for new price set commands
1 parent 9a38169 commit 5d1e4a0

7 files changed

Lines changed: 81 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Domain
2+
import Foundation
3+
import Hummingbird
4+
import HummingbirdWebSocket
5+
import Infrastructure
6+
7+
/// Routes for setting (or changing) the manual base price of an IAP. Calling with a new
8+
/// `baseTerritory` is how the iOS app implements "Change Base Territory" — ASC replaces
9+
/// the previous schedule with a new one and equalizes other territories from the new base.
10+
struct IAPPricesController: Sendable {
11+
let repo: any InAppPurchasePriceRepository
12+
13+
func addRoutes(to group: RouterGroup<BasicWebSocketRequestContext>) {
14+
group.post("/iap/:iapId/prices/set") { request, context -> Response in
15+
guard let iapId = context.parameters.get("iapId") else { return jsonError("Missing iapId") }
16+
let body = try await request.body.collect(upTo: 64 * 1024)
17+
let json = (try? JSONSerialization.jsonObject(with: body) as? [String: Any]) ?? [:]
18+
guard let baseTerritory = json["baseTerritory"] as? String ?? json["base-territory"] as? String else {
19+
return jsonError("Missing baseTerritory", status: .badRequest)
20+
}
21+
guard let pricePointId = json["pricePointId"] as? String ?? json["price-point-id"] as? String else {
22+
return jsonError("Missing pricePointId", status: .badRequest)
23+
}
24+
let schedule = try await self.repo.setPriceSchedule(
25+
iapId: iapId,
26+
baseTerritory: baseTerritory,
27+
pricePointId: pricePointId
28+
)
29+
return try restFormat([schedule])
30+
}
31+
}
32+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Domain
2+
import Foundation
3+
import Hummingbird
4+
import HummingbirdWebSocket
5+
import Infrastructure
6+
7+
/// Routes for batch-setting per-territory prices on a subscription.
8+
struct SubscriptionPricesController: Sendable {
9+
let repo: any SubscriptionPriceRepository
10+
11+
func addRoutes(to group: RouterGroup<BasicWebSocketRequestContext>) {
12+
group.post("/subscriptions/:subscriptionId/prices/set-batch") { request, context -> Response in
13+
guard let subscriptionId = context.parameters.get("subscriptionId") else {
14+
return jsonError("Missing subscriptionId")
15+
}
16+
let body = try await request.body.collect(upTo: 256 * 1024)
17+
let json = (try? JSONSerialization.jsonObject(with: body) as? [String: Any]) ?? [:]
18+
// Accept either `{ prices: [{territory, pricePointId, ...}] }` (preferred — mirrors
19+
// iOS's `setPrices(prices:)`) or a flat dict `{ "USA": "spp-1", "JPN": "spp-2" }`.
20+
var inputs: [SubscriptionPriceInput] = []
21+
if let array = json["prices"] as? [[String: Any]] {
22+
inputs = array.compactMap { entry -> SubscriptionPriceInput? in
23+
guard let territory = entry["territory"] as? String,
24+
let pricePointId = entry["pricePointId"] as? String ?? entry["price-point-id"] as? String
25+
else { return nil }
26+
return SubscriptionPriceInput(
27+
territory: territory,
28+
pricePointId: pricePointId,
29+
startDate: entry["startDate"] as? String,
30+
preserveCurrentPrice: entry["preserveCurrentPrice"] as? Bool
31+
)
32+
}
33+
} else if let dict = json["prices"] as? [String: String] {
34+
inputs = dict.map { SubscriptionPriceInput(territory: $0.key, pricePointId: $0.value) }
35+
}
36+
guard !inputs.isEmpty else {
37+
return jsonError("Missing or empty prices", status: .badRequest)
38+
}
39+
let schedule = try await self.repo.setPrices(subscriptionId: subscriptionId, prices: inputs)
40+
return try restFormat([schedule])
41+
}
42+
}
43+
}

Sources/ASCCommand/Commands/Web/RESTRoutes.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ enum RESTRoutes {
6969
SubscriptionPricePointsController(repo: subPriceRepo).addRoutes(to: v1)
7070
SubscriptionPriceScheduleController(repo: subPriceRepo).addRoutes(to: v1)
7171
SubscriptionEqualizationsController(repo: subPriceRepo).addRoutes(to: v1)
72+
SubscriptionPricesController(repo: subPriceRepo).addRoutes(to: v1)
7273
}
7374
if let iapOfferCodeRepo = try? factory.makeInAppPurchaseOfferCodeRepository(authProvider: auth),
7475
let subOfferCodeRepo = try? factory.makeSubscriptionOfferCodeRepository(authProvider: auth) {
@@ -96,6 +97,7 @@ enum RESTRoutes {
9697
IAPPricePointsController(repo: iapPriceRepo).addRoutes(to: v1)
9798
IAPPriceScheduleController(repo: iapPriceRepo).addRoutes(to: v1)
9899
IAPEqualizationsController(repo: iapPriceRepo).addRoutes(to: v1)
100+
IAPPricesController(repo: iapPriceRepo).addRoutes(to: v1)
99101
}
100102

101103
// Subscription detail listings

Tests/ASCCommandTests/Commands/IAP/IAPCreateTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ struct IAPCreateTests {
4141
"listLocalizations" : "asc iap-localizations list --iap-id iap-new",
4242
"listOfferCodes" : "asc iap-offer-codes list --iap-id iap-new",
4343
"listPricePoints" : "asc iap price-points list --iap-id iap-new",
44+
"setPrice" : "asc iap prices set --base-territory <territory> --iap-id iap-new --price-point-id <price-point-id>",
4445
"update" : "asc iap update --iap-id iap-new --reference-name <name>"
4546
},
4647
"appId" : "app-1",

Tests/ASCCommandTests/Commands/IAP/IAPListTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct IAPListTests {
3737
"listLocalizations" : "asc iap-localizations list --iap-id iap-1",
3838
"listOfferCodes" : "asc iap-offer-codes list --iap-id iap-1",
3939
"listPricePoints" : "asc iap price-points list --iap-id iap-1",
40+
"setPrice" : "asc iap prices set --base-territory <territory> --iap-id iap-1 --price-point-id <price-point-id>",
4041
"update" : "asc iap update --iap-id iap-1 --reference-name <name>"
4142
},
4243
"appId" : "app-1",

Tests/ASCCommandTests/Commands/Subscriptions/SubscriptionsCreateTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ struct SubscriptionsCreateTests {
4747
"listPricePoints" : "asc subscriptions price-points list --subscription-id sub-new",
4848
"listPromotionalOffers" : "asc subscription-promotional-offers list --subscription-id sub-new",
4949
"listWinBackOffers" : "asc win-back-offers list --subscription-id sub-new",
50+
"setPrices" : "asc subscriptions prices set-batch --price <territory>=<price-point-id> --subscription-id sub-new",
5051
"update" : "asc subscriptions update --name <name> --subscription-id sub-new"
5152
},
5253
"groupId" : "grp-1",

Tests/ASCCommandTests/Commands/Subscriptions/SubscriptionsListTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ struct SubscriptionsListTests {
4343
"listPricePoints" : "asc subscriptions price-points list --subscription-id sub-1",
4444
"listPromotionalOffers" : "asc subscription-promotional-offers list --subscription-id sub-1",
4545
"listWinBackOffers" : "asc win-back-offers list --subscription-id sub-1",
46+
"setPrices" : "asc subscriptions prices set-batch --price <territory>=<price-point-id> --subscription-id sub-1",
4647
"update" : "asc subscriptions update --name <name> --subscription-id sub-1"
4748
},
4849
"groupId" : "grp-1",

0 commit comments

Comments
 (0)