Skip to content

Commit

Permalink
Merge pull request #95 from alexanderjordanbaker/AppStoreServerAPI112
Browse files Browse the repository at this point in the history
Add support for App Store Server API v1.12 and App Store Server Notif…
  • Loading branch information
alexanderjordanbaker authored Jun 11, 2024
2 parents 7d9c1c9 + 910a676 commit e746f5d
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 10 deletions.
15 changes: 12 additions & 3 deletions appstoreserverlibrary/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import calendar
import datetime
from enum import IntEnum
from enum import IntEnum, Enum
from typing import Any, Dict, List, Optional, Type, TypeVar, Union
from attr import define
import requests
Expand Down Expand Up @@ -450,6 +450,14 @@ def __init__(self, http_status_code: int, raw_api_error: Optional[int] = None, e
except ValueError:
pass

class GetTransactionHistoryVersion(str, Enum):
V1 = "v1"
"""
.. deprecated:: 1.3.0
"""

V2 = "v2"

class AppStoreServerAPIClient:
def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment):
if environment == Environment.XCODE:
Expand Down Expand Up @@ -606,14 +614,15 @@ def get_notification_history(self, pagination_token: Optional[str], notification

return self._make_request("/inApps/v1/notifications/history", "POST", queryParameters, notification_history_request, NotificationHistoryResponse)

def get_transaction_history(self, transaction_id: str, revision: Optional[str], transaction_history_request: TransactionHistoryRequest) -> HistoryResponse:
def get_transaction_history(self, transaction_id: str, revision: Optional[str], transaction_history_request: TransactionHistoryRequest, version: GetTransactionHistoryVersion = GetTransactionHistoryVersion.V1) -> HistoryResponse:
"""
Get a customer's in-app purchase transaction history for your app.
https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history
:param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.
:param revision: A token you provide to get the next set of up to 20 transactions. All responses include a revision token. Note: For requests that use the revision token, include the same query parameters from the initial request. Use the revision token from the previous HistoryResponse.
:param transaction_history_request: The request parameters that includes the startDate,endDate,productIds,productTypes and optional query constraints.
:param version: The version of the Get Transaction History endpoint to use. V2 is recommended.
:return: A response that contains the customer's transaction history for an app.
:throws APIException: If a response was returned indicating the request could not be processed
"""
Expand Down Expand Up @@ -645,7 +654,7 @@ def get_transaction_history(self, transaction_id: str, revision: Optional[str],
if transaction_history_request.revoked is not None:
queryParameters["revoked"] = [str(transaction_history_request.revoked)]

return self._make_request("/inApps/v1/history/" + transaction_id, "GET", queryParameters, None, HistoryResponse)
return self._make_request("/inApps/" + version + "/history/" + transaction_id, "GET", queryParameters, None, HistoryResponse)

def get_transaction_info(self, transaction_id: str) -> TransactionInfoResponse:
"""
Expand Down
27 changes: 27 additions & 0 deletions appstoreserverlibrary/models/JWSRenewalInfoDecodedPayload.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .LibraryUtility import AttrsRawValueAware
from .OfferType import OfferType
from .PriceIncreaseStatus import PriceIncreaseStatus
from .OfferDiscountType import OfferDiscountType

@define
class JWSRenewalInfoDecodedPayload(AttrsRawValueAware):
Expand Down Expand Up @@ -140,4 +141,30 @@ class JWSRenewalInfoDecodedPayload(AttrsRawValueAware):
The UNIX time, in milliseconds, that the most recent auto-renewable subscription purchase expires.
https://developer.apple.com/documentation/appstoreserverapi/renewaldate
"""

currency: Optional[str] = attr.ib(default=None)
"""
The currency code for the renewalPrice of the subscription.
https://developer.apple.com/documentation/appstoreserverapi/currency
"""

renewalPrice: Optional[int] = attr.ib(default=None)
"""
The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period.
https://developer.apple.com/documentation/appstoreserverapi/renewalprice
"""

offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType')
"""
The payment mode of the discount offer.
https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype
"""

rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType')
"""
See offerDiscountType
"""
3 changes: 2 additions & 1 deletion appstoreserverlibrary/models/NotificationTypeV2.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ class NotificationTypeV2(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):
TEST = "TEST"
RENEWAL_EXTENSION = "RENEWAL_EXTENSION"
REFUND_REVERSED = "REFUND_REVERSED"
EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN"
EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN"
ONE_TIME_CHARGE = "ONE_TIME_CHARGE"
5 changes: 4 additions & 1 deletion tests/resources/models/signedRenewalInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
"signedDate": 1698148800000,
"environment": "LocalTesting",
"recentSubscriptionStartDate": 1698148800000,
"renewalDate": 1698148850000
"renewalDate": 1698148850000,
"renewalPrice": 9990,
"currency": "USD",
"offerDiscountType": "PAY_AS_YOU_GO"
}
47 changes: 42 additions & 5 deletions tests/test_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import unittest

from requests import Response
from appstoreserverlibrary.api_client import APIError, APIException, AppStoreServerAPIClient
from appstoreserverlibrary.api_client import APIError, APIException, AppStoreServerAPIClient, GetTransactionHistoryVersion
from appstoreserverlibrary.models.AccountTenure import AccountTenure
from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus
from appstoreserverlibrary.models.ConsumptionRequest import ConsumptionRequest
Expand Down Expand Up @@ -220,7 +220,7 @@ def test_get_notification_history(self):
]
self.assertEqual(expected_notification_history, notification_history_response.notificationHistory)

def test_get_transaction_history(self):
def test_get_transaction_history_v1(self):
client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json',
'GET',
'https://local-testing-base-url/inApps/v1/history/1234',
Expand All @@ -246,7 +246,44 @@ def test_get_transaction_history(self):
subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']
)

history_response = client.get_transaction_history('1234', 'revision_input', request)
history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1)

self.assertIsNotNone(history_response)
self.assertEqual('revision_output', history_response.revision)
self.assertTrue(history_response.hasMore)
self.assertEqual('com.example', history_response.bundleId)
self.assertEqual(323232, history_response.appAppleId)
self.assertEqual(Environment.LOCAL_TESTING, history_response.environment)
self.assertEqual('LocalTesting', history_response.rawEnvironment)
self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions)

def test_get_transaction_history_v2(self):
client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json',
'GET',
'https://local-testing-base-url/inApps/v2/history/1234',
{'revision': ['revision_input'],
'startDate': ['123455'],
'endDate': ['123456'],
'productId': ['com.example.1', 'com.example.2'],
'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],
'sort': ['ASCENDING'],
'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],
'inAppOwnershipType': ['FAMILY_SHARED'],
'revoked': ['False']},
None)

request = TransactionHistoryRequest(
sort=Order.ASCENDING,
productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],
endDate=123456,
startDate=123455,
revoked=False,
inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,
productIds=['com.example.1', 'com.example.2'],
subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']
)

history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2)

self.assertIsNotNone(history_response)
self.assertEqual('revision_output', history_response.revision)
Expand Down Expand Up @@ -397,7 +434,7 @@ def test_unknown_error(self):
def test_get_transaction_history_with_unknown_environment(self):
client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json',
'GET',
'https://local-testing-base-url/inApps/v1/history/1234',
'https://local-testing-base-url/inApps/v2/history/1234',
{'revision': ['revision_input'],
'startDate': ['123455'],
'endDate': ['123456'],
Expand All @@ -420,7 +457,7 @@ def test_get_transaction_history_with_unknown_environment(self):
subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']
)

history_response = client.get_transaction_history('1234', 'revision_input', request)
history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2)

self.assertIsNone(history_response.environment)
self.assertEqual("LocalTestingxxx", history_response.rawEnvironment)
Expand Down
4 changes: 4 additions & 0 deletions tests/test_decoded_payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ def test_renewal_info_decoding(self):
self.assertEqual("LocalTesting", renewal_info.rawEnvironment)
self.assertEqual(1698148800000, renewal_info.recentSubscriptionStartDate)
self.assertEqual(1698148850000, renewal_info.renewalDate)
self.assertEqual(9990, renewal_info.renewalPrice)
self.assertEqual("USD", renewal_info.currency)
self.assertEqual(OfferDiscountType.PAY_AS_YOU_GO, renewal_info.offerDiscountType)
self.assertEqual("PAY_AS_YOU_GO", renewal_info.rawOfferDiscountType)

def test_notification_decoding(self):
signed_notification = create_signed_data_from_json('tests/resources/models/signedNotification.json')
Expand Down

0 comments on commit e746f5d

Please sign in to comment.