diff --git a/appstoreserverlibrary/api_client.py b/appstoreserverlibrary/api_client.py index 6f946199..87369674 100644 --- a/appstoreserverlibrary/api_client.py +++ b/appstoreserverlibrary/api_client.py @@ -31,6 +31,7 @@ from .models.StatusResponse import StatusResponse from .models.TransactionHistoryRequest import TransactionHistoryRequest from .models.TransactionInfoResponse import TransactionInfoResponse +from .models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest T = TypeVar('T') @@ -326,6 +327,28 @@ class APIError(IntEnum): https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror """ + INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR = 4000183 + """ + An error that indicates the app account token value is not a valid UUID. + + https://developer.apple.com/documentation/appstoreserverapi/invalidappaccounttokenuuiderror + """ + + FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR = 4000185 + """ + An error that indicates the transaction is for a product the customer obtains through Family Sharing, + which the endpoint doesn’t support. + + https://developer.apple.com/documentation/appstoreserverapi/familytransactionnotsupportederror + """ + + TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR = 4000187 + """ + An error that indicates the endpoint expects an original transaction identifier. + + https://developer.apple.com/documentation/appstoreserverapi/transactionidisnotoriginaltransactioniderror + """ + SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030004 """ An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state. @@ -724,6 +747,16 @@ def send_consumption_data(self, transaction_id: str, consumption_request: Consum """ self._make_request("/inApps/v1/transactions/consumption/" + transaction_id, "PUT", {}, consumption_request, None) + def set_app_account_token(self, original_transaction_id: str, update_app_account_token_request: UpdateAppAccountTokenRequest): + """ + Sets the app account token value for a purchase the customer makes outside your app, or updates its value in an existing transaction. + https://developer.apple.com/documentation/appstoreserverapi/set-app-account-token + + :param original_transaction_id The original transaction identifier of the transaction to receive the app account token update. + :param update_app_account_token_request The request body that contains a valid app account token value. + :raises APIException: If a response was returned indicating the request could not be processed + """ + self._make_request("/inApps/v1/transactions/" + original_transaction_id + "/appAccountToken", "PUT", {}, update_app_account_token_request, None) class AsyncAppStoreServerAPIClient(BaseAppStoreServerAPIClient): def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment): @@ -926,3 +959,14 @@ async def send_consumption_data(self, transaction_id: str, consumption_request: :raises APIException: If a response was returned indicating the request could not be processed """ await self._make_request("/inApps/v1/transactions/consumption/" + transaction_id, "PUT", {}, consumption_request, None) + + async def set_app_account_token(self, original_transaction_id: str, update_app_account_token_request: UpdateAppAccountTokenRequest): + """ + Sets the app account token value for a purchase the customer makes outside your app, or updates its value in an existing transaction. + https://developer.apple.com/documentation/appstoreserverapi/set-app-account-token + + :param original_transaction_id The original transaction identifier of the transaction to receive the app account token update. + :param update_app_account_token_request The request body that contains a valid app account token value. + :raises APIException: If a response was returned indicating the request could not be processed + """ + await self._make_request("/inApps/v1/transactions/" + original_transaction_id + "/appAccountToken", "PUT", {}, update_app_account_token_request, None) diff --git a/appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py b/appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py new file mode 100644 index 00000000..b3e8163a --- /dev/null +++ b/appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py @@ -0,0 +1,18 @@ +# Copyright (c) 2025 Apple Inc. Licensed under MIT License. +from attr import define +import attr + +@define +class UpdateAppAccountTokenRequest: + """ + The request body that contains an app account token value. + + https://developer.apple.com/documentation/appstoreserverapi/updateappaccounttokenrequest + """ + + appAccountToken: str = attr.ib() + """ + The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. + + https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken + """ diff --git a/tests/resources/models/familyTransactionNotSupportedError.json b/tests/resources/models/familyTransactionNotSupportedError.json new file mode 100644 index 00000000..54af6742 --- /dev/null +++ b/tests/resources/models/familyTransactionNotSupportedError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4000185, + "errorMessage": "Invalid request. Family Sharing transactions aren't supported by this endpoint." +} \ No newline at end of file diff --git a/tests/resources/models/invalidAppAccountTokenUUIDError.json b/tests/resources/models/invalidAppAccountTokenUUIDError.json new file mode 100644 index 00000000..2045fed0 --- /dev/null +++ b/tests/resources/models/invalidAppAccountTokenUUIDError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4000183, + "errorMessage": "Invalid request. The app account token field must be a valid UUID." +} \ No newline at end of file diff --git a/tests/resources/models/transactionIdNotOriginalTransactionId.json b/tests/resources/models/transactionIdNotOriginalTransactionId.json new file mode 100644 index 00000000..02f33b18 --- /dev/null +++ b/tests/resources/models/transactionIdNotOriginalTransactionId.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4000187, + "errorMessage": "Invalid request. The transaction ID provided is not an original transaction ID." +} \ No newline at end of file diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 84cbab40..a3151fe9 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -26,17 +26,14 @@ from appstoreserverlibrary.models.OrderLookupStatus import OrderLookupStatus from appstoreserverlibrary.models.Platform import Platform from appstoreserverlibrary.models.PlayTime import PlayTime -from appstoreserverlibrary.models.PriceIncreaseStatus import PriceIncreaseStatus from appstoreserverlibrary.models.RefundPreference import RefundPreference -from appstoreserverlibrary.models.RevocationReason import RevocationReason from appstoreserverlibrary.models.SendAttemptItem import SendAttemptItem from appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult from appstoreserverlibrary.models.Status import Status from appstoreserverlibrary.models.SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem from appstoreserverlibrary.models.Subtype import Subtype from appstoreserverlibrary.models.TransactionHistoryRequest import Order, ProductType, TransactionHistoryRequest -from appstoreserverlibrary.models.TransactionReason import TransactionReason -from appstoreserverlibrary.models.Type import Type +from appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest from appstoreserverlibrary.models.UserStatus import UserStatus from tests.util import decode_json_from_signed_date, read_data_from_binary_file, read_data_from_file @@ -495,6 +492,75 @@ def test_get_transaction_history_with_malformed_app_apple_id(self): self.assertFalse(True) + def test_set_app_account_token(self): + client = self.get_client_with_body(b'', + 'PUT', + 'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken', + {}, + { + 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813' + }) + update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813') + client.set_app_account_token('49571273', update_app_account_token_request) + + def test_invalid_app_account_token_error(self): + client = self.get_client_with_body_from_file('tests/resources/models/invalidAppAccountTokenUUIDError.json', + 'PUT', + 'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken', + {}, + None, + 400) + try: + client.set_app_account_token('49571273', None) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000183, e.raw_api_error) + self.assertEqual(APIError.INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR, e.api_error) + self.assertEqual("Invalid request. The app account token field must be a valid UUID.", e.error_message) + return + + self.assertFalse(True) + + def test_family_transaction_not_supported_error(self): + client = self.get_client_with_body_from_file('tests/resources/models/familyTransactionNotSupportedError.json', + 'PUT', + 'https://local-testing-base-url/inApps/v1/transactions/1234' + '/appAccountToken', + {}, + None, + 400) + try: + client.set_app_account_token('1234', None) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000185, e.raw_api_error) + self.assertEqual(APIError.FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR, e.api_error) + self.assertEqual("Invalid request. Family Sharing transactions aren't supported by this endpoint.", e.error_message) + return + + self.assertFalse(True) + + def test_transaction_id_not_original_transaction_id_error(self): + client = self.get_client_with_body_from_file( + 'tests/resources/models/transactionIdNotOriginalTransactionId.json', + 'PUT', + 'https://local-testing-base-url/inApps/v1/transactions/1234' + '/appAccountToken', + {}, + None, + 400) + try: + client.set_app_account_token('1234', None) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000187, e.raw_api_error) + self.assertEqual(APIError.TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR, e.api_error) + self.assertEqual("Invalid request. The transaction ID provided is not an original transaction ID.", e.error_message) + return + + self.assertFalse(True) + + def get_signing_key(self): return read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') diff --git a/tests/test_api_client_async.py b/tests/test_api_client_async.py index 21eea0a0..1114001f 100644 --- a/tests/test_api_client_async.py +++ b/tests/test_api_client_async.py @@ -38,6 +38,7 @@ from appstoreserverlibrary.models.TransactionHistoryRequest import Order, ProductType, TransactionHistoryRequest from appstoreserverlibrary.models.TransactionReason import TransactionReason from appstoreserverlibrary.models.Type import Type +from appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest from appstoreserverlibrary.models.UserStatus import UserStatus from tests.util import decode_json_from_signed_date, read_data_from_binary_file, read_data_from_file @@ -496,6 +497,74 @@ async def test_get_transaction_history_with_malformed_app_apple_id(self): self.assertFalse(True) + async def test_set_app_account_token(self): + client = self.get_client_with_body(b'', + 'PUT', + 'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken', + {}, + { + 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813' + }) + update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813') + await client.set_app_account_token('49571273', update_app_account_token_request) + + async def test_invalid_app_account_token_error(self): + client = self.get_client_with_body_from_file('tests/resources/models/invalidAppAccountTokenUUIDError.json', + 'PUT', + 'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken', + {}, + None, + 400) + try: + await client.set_app_account_token('49571273', None) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000183, e.raw_api_error) + self.assertEqual(APIError.INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR, e.api_error) + self.assertEqual("Invalid request. The app account token field must be a valid UUID.", e.error_message) + return + + self.assertFalse(True) + + async def test_family_transaction_not_supported_error(self): + client = self.get_client_with_body_from_file('tests/resources/models/familyTransactionNotSupportedError.json', + 'PUT', + 'https://local-testing-base-url/inApps/v1/transactions/1234' + '/appAccountToken', + {}, + None, + 400) + try: + await client.set_app_account_token('1234', None) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000185, e.raw_api_error) + self.assertEqual(APIError.FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR, e.api_error) + self.assertEqual("Invalid request. Family Sharing transactions aren't supported by this endpoint.", e.error_message) + return + + self.assertFalse(True) + + async def test_transaction_id_not_original_transaction_id_error(self): + client = self.get_client_with_body_from_file( + 'tests/resources/models/transactionIdNotOriginalTransactionId.json', + 'PUT', + 'https://local-testing-base-url/inApps/v1/transactions/1234' + '/appAccountToken', + {}, + None, + 400) + try: + await client.set_app_account_token('1234', None) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000187, e.raw_api_error) + self.assertEqual(APIError.TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR, e.api_error) + self.assertEqual("Invalid request. The transaction ID provided is not an original transaction ID.", e.error_message) + return + + self.assertFalse(True) + def get_signing_key(self): return read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')