Skip to content

Commit 20da0cf

Browse files
Merge pull request #85 from alexanderjordanbaker/ASSAv1.11
Add support for App Store Server API v1.11 and App Store Server Notif…
2 parents 982919f + 352b2d4 commit 20da0cf

File tree

10 files changed

+123
-4
lines changed

10 files changed

+123
-4
lines changed

appstoreserverlibrary/api_client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,15 @@ class APIError(IntEnum):
308308
An error that indicates the transaction identifier doesn’t represent a consumable in-app purchase.
309309
310310
https://developer.apple.com/documentation/appstoreserverapi/invalidtransactionnotconsumableerror
311+
312+
.. deprecated:: 1.11
313+
"""
314+
315+
INVALID_TRANSACTION_TYPE_NOT_SUPPORTED = 4000047
316+
"""
317+
An error that indicates the transaction identifier represents an unsupported in-app purchase type.
318+
319+
https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror
311320
"""
312321

313322
SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030004

appstoreserverlibrary/models/ConsumptionRequest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .LifetimeDollarsRefunded import LifetimeDollarsRefunded
1313
from .Platform import Platform
1414
from .PlayTime import PlayTime
15+
from .RefundPreference import RefundPreference
1516
from .UserStatus import UserStatus
1617

1718
@define
@@ -137,4 +138,16 @@ class ConsumptionRequest(AttrsRawValueAware):
137138
rawUserStatus: Optional[int] = UserStatus.create_raw_attr('userStatus')
138139
"""
139140
See userStatus
141+
"""
142+
143+
refundPreference: Optional[RefundPreference] = RefundPreference.create_main_attr('rawRefundPreference')
144+
"""
145+
A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund.
146+
147+
https://developer.apple.com/documentation/appstoreserverapi/refundpreference
148+
"""
149+
150+
rawRefundPreference: Optional[int] = RefundPreference.create_raw_attr('refundPreference')
151+
"""
152+
See refundPreference
140153
"""
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) 2024 Apple Inc. Licensed under MIT License.
2+
3+
from enum import Enum
4+
5+
from .LibraryUtility import AppStoreServerLibraryEnumMeta
6+
7+
class ConsumptionRequestReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):
8+
"""
9+
The customer-provided reason for a refund request.
10+
11+
https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason
12+
"""
13+
UNINTENDED_PURCHASE = "UNINTENDED_PURCHASE"
14+
FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE"
15+
UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE"
16+
LEGAL = "LEGAL"
17+
OTHER = "OTHER"

appstoreserverlibrary/models/Data.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from attr import define
55
import attr
66

7+
from .ConsumptionRequestReason import ConsumptionRequestReason
78
from .Environment import Environment
89
from .Status import Status
910
from .LibraryUtility import AttrsRawValueAware
@@ -71,4 +72,16 @@ class Data(AttrsRawValueAware):
7172
rawStatus: Optional[int] = Status.create_raw_attr('status')
7273
"""
7374
See status
75+
"""
76+
77+
consumptionRequestReason: Optional[ConsumptionRequestReason] = ConsumptionRequestReason.create_main_attr('rawConsumptionRequestReason')
78+
"""
79+
The reason the customer requested the refund.
80+
81+
https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason
82+
"""
83+
84+
rawConsumptionRequestReason: Optional[str] = ConsumptionRequestReason.create_raw_attr('consumptionRequestReason')
85+
"""
86+
See consumptionRequestReason
7487
"""

appstoreserverlibrary/models/NotificationTypeV2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class NotificationTypeV2(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):
88
"""
9-
A notification type value that App Store Server Notifications V2 uses.
9+
The type that describes the in-app purchase or external purchase event for which the App Store sends the version 2 notification.
1010
1111
https://developer.apple.com/documentation/appstoreservernotifications/notificationtype
1212
"""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) 2024 Apple Inc. Licensed under MIT License.
2+
3+
from enum import IntEnum
4+
5+
from .LibraryUtility import AppStoreServerLibraryEnumMeta
6+
7+
class RefundPreference(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):
8+
"""
9+
A value that indicates your preferred outcome for the refund request.
10+
11+
https://developer.apple.com/documentation/appstoreserverapi/refundpreference
12+
"""
13+
UNDECLARED = 0
14+
PREFER_GRANT = 1
15+
PREFER_DECLINE = 2
16+
NO_PREFERENCE = 3

appstoreserverlibrary/models/Subtype.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class Subtype(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):
88
"""
9-
A notification subtype value that App Store Server Notifications V2 uses.
9+
A string that provides details about select notification types in version 2.
1010
1111
https://developer.apple.com/documentation/appstoreservernotifications/subtype
1212
"""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"notificationType": "CONSUMPTION_REQUEST",
3+
"notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20",
4+
"data": {
5+
"environment": "LocalTesting",
6+
"appAppleId": 41234,
7+
"bundleId": "com.example",
8+
"bundleVersion": "1.2.3",
9+
"signedTransactionInfo": "signed_transaction_info_value",
10+
"signedRenewalInfo": "signed_renewal_info_value",
11+
"status": 1,
12+
"consumptionRequestReason": "UNINTENDED_PURCHASE"
13+
},
14+
"version": "2.0",
15+
"signedDate": 1698148900000
16+
}

tests/test_api_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from appstoreserverlibrary.models.Platform import Platform
2828
from appstoreserverlibrary.models.PlayTime import PlayTime
2929
from appstoreserverlibrary.models.PriceIncreaseStatus import PriceIncreaseStatus
30+
from appstoreserverlibrary.models.RefundPreference import RefundPreference
3031
from appstoreserverlibrary.models.RevocationReason import RevocationReason
3132
from appstoreserverlibrary.models.SendAttemptItem import SendAttemptItem
3233
from appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult
@@ -309,7 +310,8 @@ def test_send_consumption_data(self):
309310
'playTime': 5,
310311
'lifetimeDollarsRefunded': 6,
311312
'lifetimeDollarsPurchased': 7,
312-
'userStatus': 4})
313+
'userStatus': 4,
314+
'refundPreference': 3})
313315

314316
consumptionRequest = ConsumptionRequest(
315317
customerConsented=True,
@@ -322,7 +324,8 @@ def test_send_consumption_data(self):
322324
playTime=PlayTime.ONE_DAY_TO_FOUR_DAYS,
323325
lifetimeDollarsRefunded=LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS,
324326
lifetimeDollarsPurchased=LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER,
325-
userStatus=UserStatus.LIMITED_ACCESS
327+
userStatus=UserStatus.LIMITED_ACCESS,
328+
refundPreference=RefundPreference.NO_PREFERENCE
326329
)
327330

328331
client.send_consumption_data('49571273', consumptionRequest)

tests/test_decoded_payloads.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional
44
import unittest
55
from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus
6+
from appstoreserverlibrary.models.ConsumptionRequestReason import ConsumptionRequestReason
67
from appstoreserverlibrary.models.Environment import Environment
78
from appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent
89
from appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType
@@ -133,6 +134,37 @@ def test_notification_decoding(self):
133134
self.assertEqual("signed_renewal_info_value", notification.data.signedRenewalInfo);
134135
self.assertEqual(Status.ACTIVE, notification.data.status)
135136
self.assertEqual(1, notification.data.rawStatus)
137+
self.assertIsNone(notification.data.consumptionRequestReason)
138+
self.assertIsNone(notification.data.rawConsumptionRequestReason)
139+
140+
def test_consumption_request_notification_decoding(self):
141+
signed_notification = create_signed_data_from_json('tests/resources/models/signedConsumptionRequestNotification.json')
142+
143+
signed_data_verifier = get_default_signed_data_verifier()
144+
145+
notification = signed_data_verifier.verify_and_decode_notification(signed_notification)
146+
147+
self.assertEqual(NotificationTypeV2.CONSUMPTION_REQUEST, notification.notificationType)
148+
self.assertEqual("CONSUMPTION_REQUEST", notification.rawNotificationType)
149+
self.assertIsNone(notification.subtype)
150+
self.assertIsNone(notification.rawSubtype)
151+
self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID)
152+
self.assertEqual("2.0", notification.version)
153+
self.assertEqual(1698148900000, notification.signedDate)
154+
self.assertIsNotNone(notification.data)
155+
self.assertIsNone(notification.summary)
156+
self.assertIsNone(notification.externalPurchaseToken)
157+
self.assertEqual(Environment.LOCAL_TESTING, notification.data.environment)
158+
self.assertEqual("LocalTesting", notification.data.rawEnvironment)
159+
self.assertEqual(41234, notification.data.appAppleId)
160+
self.assertEqual("com.example", notification.data.bundleId)
161+
self.assertEqual("1.2.3", notification.data.bundleVersion)
162+
self.assertEqual("signed_transaction_info_value", notification.data.signedTransactionInfo)
163+
self.assertEqual("signed_renewal_info_value", notification.data.signedRenewalInfo);
164+
self.assertEqual(Status.ACTIVE, notification.data.status)
165+
self.assertEqual(1, notification.data.rawStatus)
166+
self.assertEqual(ConsumptionRequestReason.UNINTENDED_PURCHASE, notification.data.consumptionRequestReason)
167+
self.assertEqual("UNINTENDED_PURCHASE", notification.data.rawConsumptionRequestReason)
136168

137169
def test_summary_notification_decoding(self):
138170
signed_summary_notification = create_signed_data_from_json('tests/resources/models/signedSummaryNotification.json')

0 commit comments

Comments
 (0)