From 4813700784a6a65d01276d4505b12a75cc4ceac0 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Fri, 18 Sep 2026 17:32:05 -0700 Subject: [PATCH] Adding Advanced Commerce API client calls --- index.ts | 90 +++++++++++ tests/unit-tests/api_client.test.ts | 234 +++++++++++++++++++++++++++- 2 files changed, 323 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 67ba4c1..179da09 100644 --- a/index.ts +++ b/index.ts @@ -186,6 +186,18 @@ export { RenewalCommitmentInfo } from './models/RenewalCommitmentInfo' export { TransactionCommitmentInfo } from './models/TransactionCommitmentInfo' import jsonwebtoken = require('jsonwebtoken'); +import { AdvancedCommerceRequestRefundRequest } from './models/AdvancedCommerceRequestRefundRequest'; +import { AdvancedCommerceRequestRefundResponse, AdvancedCommerceRequestRefundResponseValidator } from './models/AdvancedCommerceRequestRefundResponse'; +import { AdvancedCommerceSubscriptionCancelRequest } from './models/AdvancedCommerceSubscriptionCancelRequest'; +import { AdvancedCommerceSubscriptionCancelResponse, AdvancedCommerceSubscriptionCancelResponseValidator } from './models/AdvancedCommerceSubscriptionCancelResponse'; +import { AdvancedCommerceSubscriptionChangeMetadataRequest } from './models/AdvancedCommerceSubscriptionChangeMetadataRequest'; +import { AdvancedCommerceSubscriptionChangeMetadataResponse, AdvancedCommerceSubscriptionChangeMetadataResponseValidator } from './models/AdvancedCommerceSubscriptionChangeMetadataResponse'; +import { AdvancedCommerceSubscriptionMigrateRequest } from './models/AdvancedCommerceSubscriptionMigrateRequest'; +import { AdvancedCommerceSubscriptionMigrateResponse, AdvancedCommerceSubscriptionMigrateResponseValidator } from './models/AdvancedCommerceSubscriptionMigrateResponse'; +import { AdvancedCommerceSubscriptionPriceChangeRequest } from './models/AdvancedCommerceSubscriptionPriceChangeRequest'; +import { AdvancedCommerceSubscriptionPriceChangeResponse, AdvancedCommerceSubscriptionPriceChangeResponseValidator } from './models/AdvancedCommerceSubscriptionPriceChangeResponse'; +import { AdvancedCommerceSubscriptionRevokeRequest } from './models/AdvancedCommerceSubscriptionRevokeRequest'; +import { AdvancedCommerceSubscriptionRevokeResponse, AdvancedCommerceSubscriptionRevokeResponseValidator } from './models/AdvancedCommerceSubscriptionRevokeResponse'; import { AppTransactionInfoResponse, AppTransactionInfoResponseValidator } from './models/AppTransactionInfoResponse'; import { NotificationHistoryRequest } from './models/NotificationHistoryRequest'; import { NotificationHistoryResponse, NotificationHistoryResponseValidator } from './models/NotificationHistoryResponse'; @@ -713,6 +725,84 @@ export class AppStoreServerAPIClient { await this.makeRequest("/inApps/v1/transactions/" + transactionId + "/finish", "POST", {}, null, null, undefined); } + /** + * Increase or decrease the price of an auto-renewable subscription, a bundle, or individual items within a subscription at the next renewal. + * + * @param transactionId A transaction identifier of the auto-renewable subscription that is subject to the price change. + * @param subscriptionPriceChangeRequest The request body that contains the details of the price change. + * @return A response that contains signed JWS renewal and JWS transaction information after a subscription price change request. + * @throws APIException If a response was returned indicating the request could not be processed + * {@link https://developer.apple.com/documentation/advancedcommerceapi/change-subscription-price Change Subscription Price} + */ + public async changeSubscriptionPrice(transactionId: string, subscriptionPriceChangeRequest: AdvancedCommerceSubscriptionPriceChangeRequest): Promise { + return await this.makeRequest("/advancedCommerce/v1/subscription/changePrice/" + transactionId, "POST", {}, subscriptionPriceChangeRequest, new AdvancedCommerceSubscriptionPriceChangeResponseValidator(), 'application/json'); + } + + /** + * Turn off automatic renewal to cancel a customer's auto-renewable subscription. + * + * @param transactionId The transaction identifier of the auto-renewable subscription to cancel. + * @param subscriptionCancelRequest The request body that includes information about the subscription to cancel. + * @return The response body for a successful subscription cancellation. + * @throws APIException If a response was returned indicating the request could not be processed + * {@link https://developer.apple.com/documentation/advancedcommerceapi/cancel-a-subscription Cancel a Subscription} + */ + public async cancelSubscription(transactionId: string, subscriptionCancelRequest: AdvancedCommerceSubscriptionCancelRequest): Promise { + return await this.makeRequest("/advancedCommerce/v1/subscription/cancel/" + transactionId, "POST", {}, subscriptionCancelRequest, new AdvancedCommerceSubscriptionCancelResponseValidator(), 'application/json'); + } + + /** + * Immediately cancel a customer's subscription and all the items that are included in the subscription, and request a full or prorated refund. + * + * @param transactionId The transaction identifier of the auto-renewable subscription to revoke. + * @param subscriptionRevokeRequest The request body you provide to terminate a subscription and all its items immediately. + * @return The response body for a successful revoke-subscription request. + * @throws APIException If a response was returned indicating the request could not be processed + * {@link https://developer.apple.com/documentation/advancedcommerceapi/revoke-subscription Revoke Subscription} + */ + public async revokeSubscription(transactionId: string, subscriptionRevokeRequest: AdvancedCommerceSubscriptionRevokeRequest): Promise { + return await this.makeRequest("/advancedCommerce/v1/subscription/revoke/" + transactionId, "POST", {}, subscriptionRevokeRequest, new AdvancedCommerceSubscriptionRevokeResponseValidator(), 'application/json'); + } + + /** + * Request a refund for a one-time charge or subscription transaction. + * + * @param transactionId The transaction identifier for which you request a refund. + * @param requestRefundRequest The request body for requesting a refund for a transaction. + * @return The response body for a transaction refund request. + * @throws APIException If a response was returned indicating the request could not be processed + * {@link https://developer.apple.com/documentation/advancedcommerceapi/request-transaction-refund Request Transaction Refund} + */ + public async requestTransactionRefund(transactionId: string, requestRefundRequest: AdvancedCommerceRequestRefundRequest): Promise { + return await this.makeRequest("/advancedCommerce/v1/transaction/requestRefund/" + transactionId, "POST", {}, requestRefundRequest, new AdvancedCommerceRequestRefundResponseValidator(), 'application/json'); + } + + /** + * Update the SKU, display name, and description associated with a subscription, without affecting the subscription's billing or its service. + * + * @param transactionId The transaction identifier of the auto-renewable subscription to get changes to its metadata. + * @param subscriptionChangeMetadataRequest The request body that contains the metadata changes. + * @return The response body for a successful subscription metadata change. + * @throws APIException If a response was returned indicating the request could not be processed + * {@link https://developer.apple.com/documentation/advancedcommerceapi/change-subscription-metadata Change Subscription Metadata} + */ + public async changeSubscriptionMetadata(transactionId: string, subscriptionChangeMetadataRequest: AdvancedCommerceSubscriptionChangeMetadataRequest): Promise { + return await this.makeRequest("/advancedCommerce/v1/subscription/changeMetadata/" + transactionId, "POST", {}, subscriptionChangeMetadataRequest, new AdvancedCommerceSubscriptionChangeMetadataResponseValidator(), 'application/json'); + } + + /** + * Migrate a subscription that a customer purchased through Apple In-App Purchase to a subscription you manage using the Advanced Commerce API. + * + * @param transactionId The transaction identifier of the auto-renewable subscription to migrate. + * @param subscriptionMigrateRequest The request body that contains the details for the migration. + * @return A response that contains signed renewal and transaction information after a subscription successfully migrates to the Advanced Commerce API. + * @throws APIException If a response was returned indicating the request could not be processed + * {@link https://developer.apple.com/documentation/advancedcommerceapi/migrate-subscription-to-advanced-commerce-api Migrate a Subscription to Advanced Commerce API} + */ + public async migrateSubscriptionToAdvancedCommerceAPI(transactionId: string, subscriptionMigrateRequest: AdvancedCommerceSubscriptionMigrateRequest): Promise { + return await this.makeRequest("/advancedCommerce/v1/subscription/migrate/" + transactionId, "POST", {}, subscriptionMigrateRequest, new AdvancedCommerceSubscriptionMigrateResponseValidator(), 'application/json'); + } + private createBearerToken(): string { const payload = { bid: this.bundleId diff --git a/tests/unit-tests/api_client.test.ts b/tests/unit-tests/api_client.test.ts index d575ffd..1b59a2e 100644 --- a/tests/unit-tests/api_client.test.ts +++ b/tests/unit-tests/api_client.test.ts @@ -1,6 +1,9 @@ // Copyright (c) 2023 Apple Inc. Licensed under MIT License. import { AccountTenure } from "../../models/AccountTenure"; +import { AdvancedCommerceEffective } from "../../models/AdvancedCommerceEffective"; +import { AdvancedCommerceRefundReason } from "../../models/AdvancedCommerceRefundReason"; +import { AdvancedCommerceRefundType } from "../../models/AdvancedCommerceRefundType"; import { ConsumptionRequest } from "../../models/ConsumptionRequest"; import { ConsumptionRequestV1 } from "../../models/ConsumptionRequestV1"; import { UpdateAppAccountTokenRequest } from "../../models/UpdateAppAccountTokenRequest"; @@ -20,7 +23,7 @@ import { readFile } from "../util" import { InAppOwnershipType } from "../../models/InAppOwnershipType"; import { RefundPreference } from "../../models/RefundPreference"; import { RefundPreferenceV1 } from "../../models/RefundPreferenceV1"; -import { APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, GetTransactionHistoryVersion, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index"; +import { AdvancedCommerceRequestRefundRequest, AdvancedCommerceSubscriptionCancelRequest, AdvancedCommerceSubscriptionChangeMetadataRequest, AdvancedCommerceSubscriptionMigrateRequest, AdvancedCommerceSubscriptionPriceChangeRequest, AdvancedCommerceSubscriptionRevokeRequest, APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, GetTransactionHistoryVersion, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index"; import { Response } from "node-fetch"; import jsonwebtoken = require('jsonwebtoken'); @@ -1144,4 +1147,233 @@ describe('The api client ', () => { await client.finishTransaction("1234"); }) + + it('calls changeSubscriptionPrice', async () => { + const client = getClientWithBody("tests/resources/models/advancedCommerceSubscriptionPriceChangeResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/advancedCommerce/v1/subscription/changePrice/4124214").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(requestBody).toBeTruthy() + const body = JSON.parse(requestBody as string) + expect("7c80bb86-f892-4b21-a919-1357811d6c4f").toBe(body.requestInfo.requestReferenceId) + expect("USD").toBe(body.currency) + expect("USA").toBe(body.storefront) + expect("AD_FREE_1M").toBe(body.items[0].SKU) + expect(12990).toBe(body.items[0].price) + expect(["ADVANCED_FEATURES_1M"]).toStrictEqual(body.items[0].dependentSKUs) + }); + + const subscriptionPriceChangeRequest: AdvancedCommerceSubscriptionPriceChangeRequest = { + requestInfo: { + requestReferenceId: "7c80bb86-f892-4b21-a919-1357811d6c4f" + }, + items: [ + { + SKU: "AD_FREE_1M", + price: 12990, + dependentSKUs: ["ADVANCED_FEATURES_1M"] + } + ], + currency: "USD", + storefront: "USA" + } + + const response = await client.changeSubscriptionPrice("4124214", subscriptionPriceChangeRequest); + + expect(response).toBeTruthy() + expect("signed_renewal_info").toBe(response.signedRenewalInfo) + expect("signed_transaction_info").toBe(response.signedTransactionInfo) + }) + + it('calls cancelSubscription', async () => { + const client = getClientWithBody("tests/resources/models/advancedCommerceSubscriptionCancelResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/advancedCommerce/v1/subscription/cancel/4124214").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(requestBody).toBeTruthy() + const body = JSON.parse(requestBody as string) + expect("932c6903-0ab8-4469-9f21-015f6fab013c").toBe(body.requestInfo.requestReferenceId) + expect("USA").toBe(body.storefront) + }); + + const subscriptionCancelRequest: AdvancedCommerceSubscriptionCancelRequest = { + requestInfo: { + requestReferenceId: "932c6903-0ab8-4469-9f21-015f6fab013c" + }, + storefront: "USA" + } + + const response = await client.cancelSubscription("4124214", subscriptionCancelRequest); + + expect(response).toBeTruthy() + expect("signed_renewal_info").toBe(response.signedRenewalInfo) + expect("signed_transaction_info").toBe(response.signedTransactionInfo) + }) + + it('calls revokeSubscription', async () => { + const client = getClientWithBody("tests/resources/models/advancedCommerceSubscriptionRevokeResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/advancedCommerce/v1/subscription/revoke/4124214").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(requestBody).toBeTruthy() + const body = JSON.parse(requestBody as string) + expect("932c6903-0ab8-4469-9f21-015f6fab013c").toBe(body.requestInfo.requestReferenceId) + expect("UNINTENDED_PURCHASE").toBe(body.refundReason) + expect(true).toBe(body.refundRiskingPreference) + expect("PRORATED").toBe(body.refundType) + expect("USA").toBe(body.storefront) + }); + + const subscriptionRevokeRequest: AdvancedCommerceSubscriptionRevokeRequest = { + requestInfo: { + requestReferenceId: "932c6903-0ab8-4469-9f21-015f6fab013c" + }, + refundReason: AdvancedCommerceRefundReason.UNINTENDED_PURCHASE, + refundRiskingPreference: true, + refundType: AdvancedCommerceRefundType.PRORATED, + storefront: "USA" + } + + const response = await client.revokeSubscription("4124214", subscriptionRevokeRequest); + + expect(response).toBeTruthy() + expect("signed_renewal_info").toBe(response.signedRenewalInfo) + expect("signed_transaction_info").toBe(response.signedTransactionInfo) + }) + + it('calls requestTransactionRefund', async () => { + const client = getClientWithBody("tests/resources/models/advancedCommerceRequestRefundResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/advancedCommerce/v1/transaction/requestRefund/4124214").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(requestBody).toBeTruthy() + const body = JSON.parse(requestBody as string) + expect("932c6903-0ab8-4469-9f21-015f6fab013c").toBe(body.requestInfo.requestReferenceId) + expect(true).toBe(body.refundRiskingPreference) + expect("USD").toBe(body.currency) + expect("USA").toBe(body.storefront) + expect("AD_FREE_1M").toBe(body.items[0].SKU) + expect("UNSATISFIED_WITH_PURCHASE").toBe(body.items[0].refundReason) + expect("FULL").toBe(body.items[0].refundType) + expect(true).toBe(body.items[0].revoke) + }); + + const requestRefundRequest: AdvancedCommerceRequestRefundRequest = { + requestInfo: { + requestReferenceId: "932c6903-0ab8-4469-9f21-015f6fab013c" + }, + items: [ + { + SKU: "AD_FREE_1M", + refundReason: AdvancedCommerceRefundReason.UNSATISFIED_WITH_PURCHASE, + refundType: AdvancedCommerceRefundType.FULL, + revoke: true + } + ], + refundRiskingPreference: true, + currency: "USD", + storefront: "USA" + } + + const response = await client.requestTransactionRefund("4124214", requestRefundRequest); + + expect(response).toBeTruthy() + expect("signed_transaction_info_value").toBe(response.signedTransactionInfo) + }) + + it('calls changeSubscriptionMetadata', async () => { + const client = getClientWithBody("tests/resources/models/advancedCommerceSubscriptionChangeMetadataResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/advancedCommerce/v1/subscription/changeMetadata/4124214").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(requestBody).toBeTruthy() + const body = JSON.parse(requestBody as string) + expect("932c6903-0ab8-4469-9f21-015f6fab013c").toBe(body.requestInfo.requestReferenceId) + expect("NEXT_BILL_CYCLE").toBe(body.descriptors.effective) + expect("Remove ads and unlock advanced features.").toBe(body.descriptors.description) + expect("Ad-free package").toBe(body.descriptors.displayName) + expect("AD_FREE_1M").toBe(body.items[0].currentSKU) + expect("NEXT_BILL_CYCLE").toBe(body.items[0].effective) + expect("AD_FREE_1M_V2").toBe(body.items[0].SKU) + expect("USA").toBe(body.storefront) + expect("C003-00-1").toBe(body.taxCode) + }); + + const subscriptionChangeMetadataRequest: AdvancedCommerceSubscriptionChangeMetadataRequest = { + requestInfo: { + requestReferenceId: "932c6903-0ab8-4469-9f21-015f6fab013c" + }, + descriptors: { + effective: AdvancedCommerceEffective.NEXT_BILL_CYCLE, + description: "Remove ads and unlock advanced features.", + displayName: "Ad-free package" + }, + items: [ + { + currentSKU: "AD_FREE_1M", + effective: AdvancedCommerceEffective.NEXT_BILL_CYCLE, + SKU: "AD_FREE_1M_V2" + } + ], + storefront: "USA", + taxCode: "C003-00-1" + } + + const response = await client.changeSubscriptionMetadata("4124214", subscriptionChangeMetadataRequest); + + expect(response).toBeTruthy() + expect("signed_renewal_info").toBe(response.signedRenewalInfo) + expect("signed_transaction_info").toBe(response.signedTransactionInfo) + }) + + it('calls migrateSubscriptionToAdvancedCommerceAPI', async () => { + const client = getClientWithBody("tests/resources/models/advancedCommerceSubscriptionMigrateResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => { + expect("POST").toBe(method) + expect("/advancedCommerce/v1/subscription/migrate/4124214").toBe(path) + expect(parsedQueryParameters.entries.length).toBe(0) + + expect(requestBody).toBeTruthy() + const body = JSON.parse(requestBody as string) + expect("932c6903-0ab8-4469-9f21-015f6fab013c").toBe(body.requestInfo.requestReferenceId) + expect("Remove ads and unlock advanced features.").toBe(body.descriptors.description) + expect("Ad-free package").toBe(body.descriptors.displayName) + expect("AD_FREE_1M").toBe(body.items[0].SKU) + expect("Remove ads for the service.").toBe(body.items[0].description) + expect("Ad-free monthly plan").toBe(body.items[0].displayName) + expect("com.example.base").toBe(body.targetProductId) + expect("C003-00-1").toBe(body.taxCode) + expect("USA").toBe(body.storefront) + }); + + const subscriptionMigrateRequest: AdvancedCommerceSubscriptionMigrateRequest = { + requestInfo: { + requestReferenceId: "932c6903-0ab8-4469-9f21-015f6fab013c" + }, + descriptors: { + description: "Remove ads and unlock advanced features.", + displayName: "Ad-free package" + }, + items: [ + { + SKU: "AD_FREE_1M", + description: "Remove ads for the service.", + displayName: "Ad-free monthly plan" + } + ], + targetProductId: "com.example.base", + taxCode: "C003-00-1", + storefront: "USA" + } + + const response = await client.migrateSubscriptionToAdvancedCommerceAPI("4124214", subscriptionMigrateRequest); + + expect(response).toBeTruthy() + expect("signed_renewal_info_value").toBe(response.signedRenewalInfo) + expect("signed_transaction_info_value").toBe(response.signedTransactionInfo) + }) }) \ No newline at end of file