diff --git a/.github/workflows/ci-release-docs.yml b/.github/workflows/ci-release-docs.yml index 00540066..42733b15 100644 --- a/.github/workflows/ci-release-docs.yml +++ b/.github/workflows/ci-release-docs.yml @@ -20,7 +20,9 @@ jobs: pip install -r requirements.txt pip install -r docs/requirements.txt - name: Sphinx Api Docs - run: sphinx-apidoc -F -H "App Store Server Library" -A "Apple Inc." -V "0.2.1" -e -a -o _staging . tests pyproject.toml + run: | + VERSION=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])') + sphinx-apidoc -F -H "App Store Server Library" -A "Apple Inc." -V "$VERSION" -e -a -o _staging . tests pyproject.toml - name: Spinx build run: sphinx-build -b html _staging _build - name: Upload docs diff --git a/README.md b/README.md index 1fa307c5..8acdf5ce 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Apple App Store Server Python Library -The [Python](https://github.com/apple/app-store-server-library-python) server library for the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi), [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications), [Retention Messaging API](https://developer.apple.com/documentation/retentionmessaging), and [Advanced Commerce API](https://developer.apple.com/documentation/AdvancedCommerceAPI). Also available in [Swift](https://github.com/apple/app-store-server-library-swift), [Node.js](https://github.com/apple/app-store-server-library-node), and [Java](https://github.com/apple/app-store-server-library-java). +The [Python](https://github.com/apple/app-store-server-library-python) server library for the [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi), [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications), [Retention Messaging API](https://developer.apple.com/documentation/retentionmessaging), and [Advanced Commerce API](https://developer.apple.com/documentation/advancedcommerceapi). Also available in [Swift](https://github.com/apple/app-store-server-library-swift), [Node.js](https://github.com/apple/app-store-server-library-node), and [Java](https://github.com/apple/app-store-server-library-java). ## Table of Contents 1. [Installation](#installation) @@ -113,6 +113,8 @@ try: for transaction in response.signedTransactions: transactions.append(transaction) print(transactions) +except ValueError as e: + print(e) except APIException as e: print(e) diff --git a/appstoreserverlibrary/__init__.py b/appstoreserverlibrary/__init__.py index e69de29b..8f5faa26 100644 --- a/appstoreserverlibrary/__init__.py +++ b/appstoreserverlibrary/__init__.py @@ -0,0 +1 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. diff --git a/appstoreserverlibrary/api_client.py b/appstoreserverlibrary/api_client.py index 072c2b51..5a3f5e36 100644 --- a/appstoreserverlibrary/api_client.py +++ b/appstoreserverlibrary/api_client.py @@ -3,6 +3,7 @@ import calendar import datetime import re +from urllib.parse import quote import warnings from enum import IntEnum, Enum from typing import Any, Dict, List, MutableMapping, Optional, Type, TypeVar, Union @@ -34,6 +35,8 @@ from .models.Environment import Environment from .models.ExtendRenewalDateRequest import ExtendRenewalDateRequest from .models.ExtendRenewalDateResponse import ExtendRenewalDateResponse +from .models.GetCustomerGroupsResponse import GetCustomerGroupsResponse +from .models.GetGroupMembersResponse import GetGroupMembersResponse from .models.GetImageListResponse import GetImageListResponse from .models.GetMessageListResponse import GetMessageListResponse from .models.HistoryResponse import HistoryResponse @@ -63,352 +66,1121 @@ T = TypeVar('T') class APIError(IntEnum): + """ + Understand the error codes that App Store Server API responses return. + + https://developer.apple.com/documentation/appstoreserverapi/error-codes + """ GENERAL_BAD_REQUEST = 4000000 """ An error that indicates an invalid request. - + https://developer.apple.com/documentation/appstoreserverapi/generalbadrequesterror """ INVALID_APP_IDENTIFIER = 4000002 """ An error that indicates an invalid app identifier. - + https://developer.apple.com/documentation/appstoreserverapi/invalidappidentifiererror """ + INVALID_LIMIT = 4000004 + """ + An error that indicates the request limit is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidlimiterror + """ + INVALID_REQUEST_REVISION = 4000005 """ An error that indicates an invalid request revision. - + https://developer.apple.com/documentation/appstoreserverapi/invalidrequestrevisionerror """ INVALID_TRANSACTION_ID = 4000006 """ An error that indicates an invalid transaction identifier. - + https://developer.apple.com/documentation/appstoreserverapi/invalidtransactioniderror """ INVALID_ORIGINAL_TRANSACTION_ID = 4000008 """ An error that indicates an invalid original transaction identifier. - + https://developer.apple.com/documentation/appstoreserverapi/invalidoriginaltransactioniderror """ INVALID_EXTEND_BY_DAYS = 4000009 """ An error that indicates an invalid extend-by-days value. - + https://developer.apple.com/documentation/appstoreserverapi/invalidextendbydayserror """ INVALID_EXTEND_REASON_CODE = 4000010 """ An error that indicates an invalid reason code. - + https://developer.apple.com/documentation/appstoreserverapi/invalidextendreasoncodeerror """ INVALID_REQUEST_IDENTIFIER = 4000011 """ An error that indicates an invalid request identifier. - + https://developer.apple.com/documentation/appstoreserverapi/invalidrequestidentifiererror """ START_DATE_TOO_FAR_IN_PAST = 4000012 """ - An error that indicates that the start date is earlier than the earliest allowed date. - + An error that indicates the start date is earlier than the earliest allowed date. + https://developer.apple.com/documentation/appstoreserverapi/startdatetoofarinpasterror """ START_DATE_AFTER_END_DATE = 4000013 """ - An error that indicates that the end date precedes the start date, or the two dates are equal. - + An error that indicates the end date precedes the start date, or the two dates are equal. + https://developer.apple.com/documentation/appstoreserverapi/startdateafterenddateerror """ INVALID_PAGINATION_TOKEN = 4000014 """ An error that indicates the pagination token is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidpaginationtokenerror """ INVALID_START_DATE = 4000015 """ An error that indicates the start date is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidstartdateerror """ INVALID_END_DATE = 4000016 """ An error that indicates the end date is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidenddateerror """ - + PAGINATION_TOKEN_EXPIRED = 4000017 """ An error that indicates the pagination token expired. - + https://developer.apple.com/documentation/appstoreserverapi/paginationtokenexpirederror """ INVALID_NOTIFICATION_TYPE = 4000018 """ An error that indicates the notification type or subtype is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidnotificationtypeerror """ MULTIPLE_FILTERS_SUPPLIED = 4000019 """ - An error that indicates the request is invalid because it has too many constraints applied. - + An error that indicates the request is invalid because it has too many applied constraints. + https://developer.apple.com/documentation/appstoreserverapi/multiplefilterssuppliederror """ INVALID_TEST_NOTIFICATION_TOKEN = 4000020 """ An error that indicates the test notification token is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidtestnotificationtokenerror """ INVALID_SORT = 4000021 """ - An error that indicates an invalid sort parameter. - + An error that indicates the sort parameter is invalid. + https://developer.apple.com/documentation/appstoreserverapi/invalidsorterror """ INVALID_PRODUCT_TYPE = 4000022 """ - An error that indicates an invalid product type parameter. - + An error that indicates the product type parameter is invalid. + https://developer.apple.com/documentation/appstoreserverapi/invalidproducttypeerror """ INVALID_PRODUCT_ID = 4000023 """ An error that indicates the product ID parameter is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidproductiderror """ INVALID_SUBSCRIPTION_GROUP_IDENTIFIER = 4000024 """ - An error that indicates an invalid subscription group identifier. - + An error that indicates the subscription group identifier is invalid. + https://developer.apple.com/documentation/appstoreserverapi/invalidsubscriptiongroupidentifiererror """ INVALID_EXCLUDE_REVOKED = 4000025 """ An error that indicates the query parameter exclude-revoked is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidexcluderevokederror - .. deprecated:: 1.5 + .. deprecated:: 1.0.0 + Use :attr:`INVALID_REVOKED` instead. """ INVALID_IN_APP_OWNERSHIP_TYPE = 4000026 """ An error that indicates an invalid in-app ownership type parameter. - + https://developer.apple.com/documentation/appstoreserverapi/invalidinappownershiptypeerror """ INVALID_EMPTY_STOREFRONT_COUNTRY_CODE_LIST = 4000027 """ An error that indicates a required storefront country code is empty. - + https://developer.apple.com/documentation/appstoreserverapi/invalidemptystorefrontcountrycodelisterror """ INVALID_STOREFRONT_COUNTRY_CODE = 4000028 """ An error that indicates a storefront code is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidstorefrontcountrycodeerror """ INVALID_REVOKED = 4000030 """ An error that indicates the revoked parameter contains an invalid value. - + https://developer.apple.com/documentation/appstoreserverapi/invalidrevokederror """ INVALID_STATUS = 4000031 """ An error that indicates the status parameter is invalid. - + https://developer.apple.com/documentation/appstoreserverapi/invalidstatuserror """ - INVALID_ACCOUNT_TENURE = 4000032 + INVALID_ACCOUNT_TENURE = 4000032 + """ + An error that indicates the value of the account tenure field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidaccounttenureerror + """ + + INVALID_APP_ACCOUNT_TOKEN = 4000033 + """ + An error that indicates the value of the app account token field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidappaccounttokenerror + """ + + INVALID_CONSUMPTION_STATUS = 4000034 + """ + An error that indicates the value of the consumption status field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidconsumptionstatuserror + """ + + INVALID_CUSTOMER_CONSENTED = 4000035 + """ + An error that indicates the customer consented field is invalid or doesn’t indicate that the customer consented. + + https://developer.apple.com/documentation/appstoreserverapi/invalidcustomerconsentederror + """ + + INVALID_DELIVERY_STATUS = 4000036 + """ + An error that indicates the value in the delivery status field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invaliddeliverystatuserror + """ + + INVALID_LIFETIME_DOLLARS_PURCHASED = 4000037 + """ + An error that indicates the value in the lifetime dollars purchased field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidlifetimedollarspurchasederror + """ + + INVALID_LIFETIME_DOLLARS_REFUNDED = 4000038 + """ + An error that indicates the value in the lifetime dollars refunded field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidlifetimedollarsrefundederror + """ + + INVALID_PLATFORM = 4000039 + """ + An error that indicates the value in the platform field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidplatformerror + """ + + INVALID_PLAY_TIME = 4000040 + """ + An error that indicates the value in the playtime field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidplaytimeerror + """ + + INVALID_SAMPLE_CONTENT_PROVIDED = 4000041 + """ + An error that indicates the value in the sample content provided field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidsamplecontentprovidederror + """ + + INVALID_USER_STATUS = 4000042 + """ + An error that indicates the value in the user status field is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invaliduserstatuserror + """ + + INVALID_TRANSACTION_NOT_CONSUMABLE = 4000043 + """ + An error that indicates the transaction identifier doesn’t represent a consumable In-App Purchase. + + https://developer.apple.com/documentation/appstoreserverapi/invalidtransactionnotconsumableerror + + .. deprecated:: 1.2.0 + Use :attr:`INVALID_TRANSACTION_TYPE_NOT_SUPPORTED` instead. + """ + + INVALID_REFUND_PREFERENCE = 4000044 + """ + An error that indicates an invalid refund preference value. + + https://developer.apple.com/documentation/appstoreserverapi/invalidrefundpreferenceerror + """ + + INVALID_TRANSACTION_TYPE_NOT_SUPPORTED = 4000047 + """ + An error that indicates the transaction identifier represents an unsupported In-App Purchase type. + + https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror + """ + + APP_TRANSACTION_ID_NOT_SUPPORTED = 4000048 + """ + An error that indicates the endpoint doesn’t support an app transaction ID. + + https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror + """ + + INVALID_CURRENCY = 4000053 + """ + The currency value is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidcurrencyerror + """ + + ONE_ITEM_NEEDED_IN_MODIFY = 4000063 + """ + At least one type of change must be provided in a modify subscription request. + + https://developer.apple.com/documentation/advancedcommerceapi/oneitemneededinmodifyerror + """ + + NULL_REQUEST_INFO = 4000079 + """ + The required field, requestInfo, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullrequestinfoerror + """ + + NULL_REQUEST_REFERENCE_ID = 4000080 + """ + The required field, requestReferenceId, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullrequestreferenceiderror + """ + + INVALID_REQUEST_REFERENCE_ID = 4000081 + """ + The requestReferenceId value is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidrequestreferenceiderror + """ + + INVALID_CONSISTENCY_TOKEN = 4000082 + """ + The consistencyToken value is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidconsistencytokenerror + """ + + NULL_VERSION = 4000083 + """ + The required field, version, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullversionerror + """ + + UNEXPECTED_VERSION = 4000084 + """ + The value of version is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/unexpectedversionerror + """ + + NULL_TRANSACTION_ID = 4000085 + """ + The required field, transactionId, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nulltransactioniderror + """ + + NEGATIVE_PRICE = 4000086 + """ + The price field must contain a positive number. + + https://developer.apple.com/documentation/advancedcommerceapi/negativepriceerror + """ + + SKU_LENGTH_EXCEEDED = 4000087 + """ + Exceeds the maximum length of the SKU field. + + https://developer.apple.com/documentation/advancedcommerceapi/skulengthexceedederror + """ + + DESCRIPTION_LENGTH_EXCEEDED = 4000088 + """ + Exceeds the maximum length of the description field. + + https://developer.apple.com/documentation/advancedcommerceapi/descriptionlengthexceedederror + """ + + DISPLAY_NAME_LENGTH_EXCEEDED = 4000089 + """ + Exceeds the maximum length of the displayName field. + + https://developer.apple.com/documentation/advancedcommerceapi/displaynamelengthexceedederror + """ + + NEGATIVE_PRORATED_PRICE = 4000091 + """ + The proratedPrice field must contain a positive number. + + https://developer.apple.com/documentation/advancedcommerceapi/negativeproratedpriceerror + """ + + NULL_OFFER_PERIOD = 4000092 + """ + The required field, offer period, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullofferperioderror + """ + + NULL_PERIOD_COUNT = 4000093 + """ + The required field, periodCount, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullperiodcounterror + """ + + PERIOD_COUNT_NOT_POSITIVE = 4000094 + """ + Period count must be a positive number. + + https://developer.apple.com/documentation/advancedcommerceapi/periodcountnotpositiveerror + """ + + NULL_REASON = 4000095 + """ + The required field, reason, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullreasonerror + """ + + INVALID_PREVIOUS_TRANSACTION_ID = 4000096 + """ + Previous original transaction id is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidprevioustransactioniderror + """ + + REPEATED_REQUEST_REFERENCE_ID = 4000097 + """ + The requestReferenceId was repeated. + + https://developer.apple.com/documentation/advancedcommerceapi/repeatedrequestreferenceiderror + """ + + NULL_CURRENCY = 4000098 + """ + The required field, currency, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullcurrencyerror + """ + + NULL_TAX_CODE = 4000099 + """ + The required field, taxCode, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nulltaxcodeerror + """ + + NULL_STOREFRONT = 4000100 + """ + The required field, storefront, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullstorefronterror + """ + + NULL_ITEMS = 4000101 + """ + The required field, items, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullitemserror + """ + + NULL_ITEM = 4000102 + """ + The required field, item, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullitemerror + """ + + NULL_DESCRIPTORS = 4000103 + """ + The required field, descriptors, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nulldescriptorserror + """ + + NULL_PERIOD = 4000104 + """ + The required field, period, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullperioderror + """ + + NULL_SKU = 4000105 + """ + The required field, SKU, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullskuerror + """ + + NULL_DISPLAY_NAME = 4000106 + """ + The required field, displayName, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nulldisplaynameerror + """ + + NULL_DESCRIPTION = 4000107 + """ + The required field, description, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nulldescriptionerror + """ + + NULL_PRICE = 4000109 + """ + The required field, price, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullpriceerror + """ + + NULL_RETAIN_BILLING_CYCLE = 4000110 + """ + The required field, retainBillingCycle, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullretainbillingcycleerror + """ + + NULL_EFFECTIVE = 4000111 + """ + The required field, effective, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nulleffectiveerror + """ + + NULL_NEW_SKU = 4000112 + """ + The required field, SKU in changeItems, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullnewskuerror + """ + + INVALID_PREVIOUS_SUBSCRIPTION = 4000113 + """ + The previous subscription targeted is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidprevioussubscriptionerror + """ + + PRODUCT_ALREADY_EXISTS = 4000114 + """ + Provided SKU is already owned. + + https://developer.apple.com/documentation/advancedcommerceapi/productalreadyexistserror + """ + + INVALID_PRODUCT_CHANGES = 4000115 + """ + Product changes are invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidproductchangeserror + """ + + INVALID_SALABLE = 4000116 + """ + The targeted salable isn't configured as a generic salable. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidsalableerror + """ + + INVALID_SALABLE_DURATION = 4000117 + """ + The salable duration is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidsalabledurationerror + """ + + INVALID_DISPLAY_NAME = 4000118 + """ + The displayName is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invaliddisplaynameerror + """ + + INVALID_DESCRIPTION = 4000119 + """ + The description is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invaliddescriptionerror + """ + + INVALID_PRODUCT = 4000121 + """ + The requested product to change doesn't exist. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidproducterror + """ + + INVALID_SKU = 4000122 + """ + The SKU was invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidskuerror + """ + + INVALID_REFUND_TYPE = 4000123 + """ + The refundType is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidrefundtypeerror + """ + + INVALID_REFUND_REASON = 4000124 + """ + The refundReason is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidrefundreasonerror + """ + + INVALID_CHANGE_REASON = 4000125 + """ + The change reason is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidchangereasonerror + """ + + INVALID_OFFER_REASON = 4000126 + """ + The offer reason is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidofferreasonerror + """ + + INVALID_TAX_PRODUCT_CODE = 4000127 + """ + The taxCode is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidtaxproductcodeerror + """ + + INVALID_OFFER_PERIOD = 4000128 + """ + The offer period is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidofferperioderror + """ + + INVALID_OFFER_PERIOD_COUNT = 4000129 + """ + The offer periodCount is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidofferperiodcounterror + """ + + INVALID_RENEWAL_PERIOD = 4000130 + """ + The renewal period is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidrenewalperioderror + """ + + INVALID_RENEWAL_PRICE = 4000131 + """ + The renewal price is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidrenewalpriceerror + """ + + INVALID_AMOUNT = 4000132 + """ + The amount is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidamounterror + """ + + MISMATCHED_STOREFRONT = 4000133 + """ + The storefronts mismatch. + + https://developer.apple.com/documentation/advancedcommerceapi/mismatchedstorefronterror + """ + + MISSING_PRICING_CONFIG_FOR_STOREFRONT = 4000134 + """ + Pricing isn't configured for the storefront. + + https://developer.apple.com/documentation/advancedcommerceapi/missingpricingconfigforstorefronterror + """ + + OPERATION_NOT_ALLOWED = 4000135 + """ + The operation isn't allowed. + + https://developer.apple.com/documentation/advancedcommerceapi/operationnotallowederror + """ + + MORE_ITEMS_THAN_ALLOWED = 4000136 + """ + More items were provided than allowed. + + https://developer.apple.com/documentation/advancedcommerceapi/moreitemsthanallowederror + """ + + MORE_OFFERS_THAN_ALLOWED = 4000137 + """ + More offers were provided than allowed. + + https://developer.apple.com/documentation/advancedcommerceapi/moreoffersthanallowederror + """ + + EMPTY_ADD_CHANGE_ITEMS = 4000139 + """ + The addItems and changeItems entries cannot be empty. + + https://developer.apple.com/documentation/advancedcommerceapi/emptyaddchangeitemserror + """ + + MISSING_UPDATED_ITEMS_WITH_PERIOD_CHANGE = 4000140 + """ + All items must be updated on a period change. + + https://developer.apple.com/documentation/advancedcommerceapi/missingupdateditemswithperiodchangeerror + """ + + PERIOD_RESET_WITH_RETAIN_BILLING_CYCLE = 4000141 + """ + Period reset conflicts with retaining billing cycle. + + https://developer.apple.com/documentation/advancedcommerceapi/periodresetwithretainbillingcycleerror + """ + + PERIOD_CHANGE_EFFECTIVE_CONFLICT = 4000142 + """ + A period change at next cycle conflicts with addition at the current period. + + https://developer.apple.com/documentation/advancedcommerceapi/periodchangeeffectiveconflicterror + """ + + MULTIPLE_OPERATIONS_ON_SINGLE_SKU = 4000143 + """ + Multiple operations on a single SKU isn't allowed. + + https://developer.apple.com/documentation/advancedcommerceapi/multipleoperationsonsingleskuerror + """ + + REMOVE_ITEMS_WITHOUT_ADD_OR_CHANGE_ITEMS = 4000144 + """ + The removeItems object was present without addItems or changeItems. + + https://developer.apple.com/documentation/advancedcommerceapi/removeitemswithoutaddorchangeitemserror + """ + + REMOVE_ITEM_NOT_FOUND = 4000145 + """ + A product in removeItems wasn't found for the given subscription. + + https://developer.apple.com/documentation/advancedcommerceapi/removeitemnotfounderror + """ + + CHANGE_ITEM_NOT_FOUND = 4000146 + """ + The targeted item in changeItems wasn't found. + + https://developer.apple.com/documentation/advancedcommerceapi/changeitemnotfounderror + """ + + MISALIGNED_BILLING_CYCLE = 4000147 + """ + The request contains a billing period that doesn't align with the subscription's billing cycle. + + https://developer.apple.com/documentation/advancedcommerceapi/misalignedbillingcycleerror + """ + + BILLING_CYCLE_RESET_WITH_EFFECTIVE_LATER = 4000148 + """ + Bill cycle reset with effective later. + + https://developer.apple.com/documentation/advancedcommerceapi/billingcycleresetwitheffectivelatererror + """ + + PERIOD_CHANGE_IMMEDIATE_WITH_EFFECTIVE_AT_NEXT_BILLING_CYCLE = 4000149 + """ + Period change immediately with effective later. + + https://developer.apple.com/documentation/advancedcommerceapi/periodchangeimmediatewitheffectiveatnextbillingcycleerror + """ + + MULTIPLE_PRICES = 4000150 + """ + Prorated price and offer price are mutually exclusive. + + https://developer.apple.com/documentation/advancedcommerceapi/multiplepriceserror + """ + + INVALID_PRORATED_PRICE = 4000151 + """ + The prorated price was invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidproratedpriceerror + """ + + INVALID_OFFER_PRICE = 4000152 + """ + The subscription offer price is higher than the regular subscription price. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidofferpriceerror + """ + + NULL_REFUND_AMOUNT = 4000153 + """ + The refundAmount value is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/nullrefundamounterror + """ + + NEGATIVE_REFUND_AMOUNT = 4000154 + """ + The refundAmount must be a positive number. + + https://developer.apple.com/documentation/advancedcommerceapi/negativerefundamounterror + """ + + REFUND_AMOUNT_WITHOUT_CUSTOM = 4000155 + """ + Can't provide the refund amount because the refundType isn't CUSTOM. + + https://developer.apple.com/documentation/advancedcommerceapi/refundamountwithoutcustomerror + """ + + NULL_REFUND_REASON = 4000156 + """ + The required field, refundReason, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullrefundreasonerror + """ + + NULL_REFUND_TYPE = 4000157 + """ + The required field, refundType, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullrefundtypeerror + """ + + SIMULATE_REFUND_DECLINE_ONLY_IN_SANDBOX = 4000158 + """ + The type SIMULATE_REFUND_DECLINE is only valid in Sandbox. + + https://developer.apple.com/documentation/advancedcommerceapi/simulaterefunddeclineonlyinsandboxerror + """ + + NULL_REFUND_RISKING = 4000159 + """ + The required field, refundRiskingPreference, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullrefundriskingerror + """ + + AT_LEAST_ONE_ITEM = 4000160 + """ + When included, provide at least one item in items. + + https://developer.apple.com/documentation/advancedcommerceapi/atleastoneitemerror + """ + + INVALID_IMAGE = 4000161 + """ + An error that indicates the image that’s uploading is invalid. + + https://developer.apple.com/documentation/retentionmessaging/invalidimageerror + """ + + HEADER_TOO_LONG = 4000162 + """ + An error that indicates the header text is too long. + + https://developer.apple.com/documentation/retentionmessaging/headertoolongerror + """ + + BODY_TOO_LONG = 4000163 + """ + An error that indicates the body text is too long. + + https://developer.apple.com/documentation/retentionmessaging/bodytoolongerror + """ + + INVALID_LOCALE = 4000164 + """ + An error that indicates the locale is invalid. + + https://developer.apple.com/documentation/retentionmessaging/invalidlocaleerror + """ + + AT_LEAST_ONE_OF_DISPLAY_NAME_OR_DESCRIPTION = 4000165 + """ + Provide either the displayName or a description. + + https://developer.apple.com/documentation/advancedcommerceapi/atleastoneofdisplaynameordescriptionerror + """ + + NULL_TARGET_PRODUCT_ID = 4000166 + """ + The required field, targetProductID, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nulltargetproductiderror + """ + + INVALID_TARGET_PRODUCT_ID = 4000167 + """ + The targetProductID value is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidtargetproductiderror + """ + + REMOVAL_ALL_NOT_ALLOWED = 4000168 + """ + The active subscription must contain at least one item and cannot be completely empty. + + https://developer.apple.com/documentation/advancedcommerceapi/removalallnotallowederror + """ + + NULL_CURRENT_SKU = 4000169 + """ + The required field, currentSKU, is missing. + + https://developer.apple.com/documentation/advancedcommerceapi/nullcurrentskuerror + """ + + CURRENT_SKU_LENGTH_EXCEEDED = 4000170 + """ + Exceeds the maximum length of the currentSKU field. + + https://developer.apple.com/documentation/advancedcommerceapi/currentskulengthexceedederror + """ + + NULL_ADVANCED_COMMERCE_DATA = 4000171 + """ + The required field, advancedCommerceData, was null. + + https://developer.apple.com/documentation/advancedcommerceapi/nulladvancedcommercedataerror + """ + + INVALID_OPERATION = 4000172 + """ + The operation is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidoperationerror + """ + + MALFORMED_PAYLOAD = 4000173 + """ + The payload is malformed. + + https://developer.apple.com/documentation/advancedcommerceapi/malformedpayloaderror + """ + + INVALID_SIGNATURE = 4000174 """ - An error that indicates the value of the account tenure field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invalidaccounttenureerror + The signature is invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidsignatureerror """ - INVALID_APP_ACCOUNT_TOKEN = 4000033 + ALT_TEXT_TOO_LONG = 4000175 """ - An error that indicates the value of the app account token field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invalidappaccounttokenerror + An error that indicates the alternative text for an image is too long. + + https://developer.apple.com/documentation/retentionmessaging/alttexttoolongerror """ - INVALID_CONSUMPTION_STATUS = 4000034 + SUBSCRIPTION_ALREADY_MIGRATED = 4000176 """ - An error that indicates the value of the consumption status field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invalidconsumptionstatuserror + The subscription was already migrated. + + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionalreadymigratederror """ - INVALID_CUSTOMER_CONSENTED = 4000035 + OFFER_PREVENTS_ITEM_MID_CYCLE_CHANGE = 4000177 """ - An error that indicates the customer consented field is invalid or doesn’t indicate that the customer consented. - - https://developer.apple.com/documentation/appstoreserverapi/invalidcustomerconsentederror + An active or pending offer prevents changes to the item mid-cycle. + + https://developer.apple.com/documentation/advancedcommerceapi/offerpreventsitemmidcyclechangeerror """ - INVALID_DELIVERY_STATUS = 4000036 + PRICE_CHANGE_NOT_SUPPORTED_THROUGH_MODIFY_ITEMS = 4000178 """ - An error that indicates the value in the delivery status field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invaliddeliverystatuserror + Price change is not allowed through plan modification. + + https://developer.apple.com/documentation/advancedcommerceapi/pricechangenotsupportedthroughmodifyitemserror """ - INVALID_LIFETIME_DOLLARS_PURCHASED = 4000037 + ITEM_LIMIT_EXCEEDED = 4000179 """ - An error that indicates the value in the lifetime dollars purchased field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invalidlifetimedollarspurchasederror + The number of items in subscription exceeds the limit. + + https://developer.apple.com/documentation/advancedcommerceapi/itemlimitexceedederror """ - INVALID_LIFETIME_DOLLARS_REFUNDED = 4000038 + PENDING_CHANGES_MISMATCH = 4000180 """ - An error that indicates the value in the lifetime dollars refunded field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invalidlifetimedollarsrefundederror + Pending subscription changes must specify a renewalItem, and if there are no pending changes, a renewalItem cannot be specified. + + https://developer.apple.com/documentation/advancedcommerceapi/pendingchangesmismatcherror """ - INVALID_PLATFORM = 4000039 + PENDING_REFUND = 4000181 """ - An error that indicates the value in the platform field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invalidplatformerror + The transaction has pending refunds. + + https://developer.apple.com/documentation/advancedcommerceapi/pendingrefunderror """ - INVALID_PLAY_TIME = 4000040 + PRORATED_ONLY_LATEST_TRANSACTION = 4000182 """ - An error that indicates the value in the playtime field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invalidplaytimeerror + Only requests against the latest transaction can have a PRORATED refundType value. + + https://developer.apple.com/documentation/advancedcommerceapi/proratedonlylatesttransactionerror """ - INVALID_SAMPLE_CONTENT_PROVIDED = 4000041 + INVALID_APP_ACCOUNT_TOKEN_UUID = 4000183 """ - An error that indicates the value in the sample content provided field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invalidsamplecontentprovidederror + An error that indicates the app account token value is not a valid UUID. + + https://developer.apple.com/documentation/appstoreserverapi/invalidappaccounttokenuuiderror """ - INVALID_USER_STATUS = 4000042 + PARTIAL_SIMULATE_REFUND_DECLINE = 4000184 """ - An error that indicates the value in the user status field is invalid. - - https://developer.apple.com/documentation/appstoreserverapi/invaliduserstatuserror + If one item has a refundReason value of SIMULATE_REFUND_DECLINE, all items must have a refundReason value of SIMULATE_REFUND_DECLINE. + + https://developer.apple.com/documentation/advancedcommerceapi/partialsimulaterefunddeclineerror """ - INVALID_TRANSACTION_NOT_CONSUMABLE = 4000043 + FAMILY_TRANSACTION_NOT_SUPPORTED = 4000185 """ - An error that indicates the transaction identifier doesn’t represent a consumable in-app purchase. - - https://developer.apple.com/documentation/appstoreserverapi/invalidtransactionnotconsumableerror + An error that indicates the transaction is for a product the customer obtains through Family Sharing, which the endpoint doesn’t support. - .. deprecated:: 1.11 + https://developer.apple.com/documentation/appstoreserverapi/familytransactionnotsupportederror """ - INVALID_TRANSACTION_TYPE_NOT_SUPPORTED = 4000047 + REVOKE_ON_INACTIVE_SUBSCRIPTION = 4000186 """ - An error that indicates the transaction identifier represents an unsupported in-app purchase type. - - https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror + Only active subscriptions are revocable. + + https://developer.apple.com/documentation/advancedcommerceapi/revokeoninactivesubscriptionerror """ - APP_TRANSACTION_ID_NOT_SUPPORTED_ERROR = 4000048 + TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID = 4000187 """ - An error that indicates the endpoint doesn't support an app transaction ID. + An error that indicates the endpoint expects an original transaction identifier. - https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror + https://developer.apple.com/documentation/appstoreserverapi/transactionidisnotoriginaltransactioniderror """ - INVALID_IMAGE = 4000161 + DEPENDENT_SKUS_CANNOT_BE_SHARED = 4000192 """ - An error that indicates the image that's uploading is invalid. + Invalid request. dependentSKUs can't be shared between multiple SKUs. - https://developer.apple.com/documentation/retentionmessaging/invalidimageerror + https://developer.apple.com/documentation/advancedcommerceapi/dependentskuscannotbesharederror """ - HEADER_TOO_LONG = 4000162 + DEPENDENT_SKUS_CANNOT_BE_CHAINED = 4000193 """ - An error that indicates the header text is too long. + Invalid request. dependentSKUs can't be chained. - https://developer.apple.com/documentation/retentionmessaging/headertoolongerror + https://developer.apple.com/documentation/advancedcommerceapi/dependentskuscannotbechainederror """ - BODY_TOO_LONG = 4000163 + ITEM_CANNOT_BE_SPECIFIED_MULTIPLE_TIMES = 4000194 """ - An error that indicates the body text is too long. + Invalid request. The same SKU can't be repeated in this request. - https://developer.apple.com/documentation/retentionmessaging/bodytoolongerror + https://developer.apple.com/documentation/advancedcommerceapi/itemcannotbespecifiedmultipletimeserror """ - INVALID_LOCALE = 4000164 + PRICE_CHANGE_CANNOT_BE_ISSUED_WHEN_ALREADY_COMMUNICATED = 4000205 """ - An error that indicates the locale is invalid. + Invalid request. A price change can't be issued when the price increase has already been communicated. - https://developer.apple.com/documentation/retentionmessaging/invalidlocaleerror + https://developer.apple.com/documentation/advancedcommerceapi/pricechangecannotbeissuedwhenalreadycommunicatederror """ - ALT_TEXT_TOO_LONG = 4000175 + CONSUMPTION_PERCENTAGE_OUT_OF_RANGE = 4000207 """ - An error that indicates the alternative text for an image is too long. + An error that indicates the consumption percentage is outside the valid range. - https://developer.apple.com/documentation/retentionmessaging/alttexttoolongerror + https://developer.apple.com/documentation/appstoreserverapi/consumptionpercentageoutofrangeerror """ - INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR = 4000183 + UNDELIVERED_CONSUMPTION_PERCENTAGE_NON_ZERO = 4000208 """ - An error that indicates the app account token value is not a valid UUID. - - https://developer.apple.com/documentation/appstoreserverapi/invalidappaccounttokenuuiderror + An error that indicates the consumption percentage must be zero for the provided delivery status. + + https://developer.apple.com/documentation/appstoreserverapi/undeliveredconsumptionpercentagenonzeroerror """ - FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR = 4000185 + CONSUMPTION_PERCENTAGE_AUTO_RENEWABLE_SUBSCRIPTION = 4000209 """ - An error that indicates the transaction is for a product the customer obtains through Family Sharing, - which the endpoint doesn’t support. + An error that indicates the consumption percentage field is unsupported for auto-renewable subscription transactions. - https://developer.apple.com/documentation/appstoreserverapi/familytransactionnotsupportederror + https://developer.apple.com/documentation/appstoreserverapi/consumptionpercentageautorenewablesubscriptionerror """ - TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR = 4000187 + ADVANCED_COMMERCE_TRANSACTION_NOT_SUPPORTED = 4000210 """ - An error that indicates the endpoint expects an original transaction identifier. + An error that indicates Advanced Commerce API transactions are not supported by the endpoint. - https://developer.apple.com/documentation/appstoreserverapi/transactionidisnotoriginaltransactioniderror + https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetransactionnotsupportederror """ INVALID_PERFORMANCE_TEST_REQUEST = 4000211 @@ -432,6 +1204,13 @@ class APIError(IntEnum): https://developer.apple.com/documentation/retentionmessaging/existingperformancetestrunerror """ + INVALID_PRICE_FOR_CHANGE_ITEM_IN_PRICE_INCREASE = 4000214 + """ + Invalid request. Price provided for a changed item with an offer while subject to a price increase was invalid. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidpriceforchangeiteminpriceincreaseerror + """ + BAD_REQUEST_REALTIME_URL = 4000215 """ An error that indicates the URL is invalid. @@ -460,6 +1239,34 @@ class APIError(IntEnum): https://developer.apple.com/documentation/retentionmessaging/badrequestbulletpointtexttoolongerror """ + INVALID_SKU_PROVIDED_MUST_BE_CURRENT_SKU_SET_TO_RENEW = 4000220 + """ + Invalid request. SKU values provided in request must be a SKU that will renew. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidskuprovidedmustbecurrentskusettorenewerror + """ + + ACA_PRICE_INCREASE_IS_NOT_CURRENTLY_SUPPORTED_IN_INDIA = 4000221 + """ + Advanced Commerce API Price Increase mechanism is not currently supported in India. + + https://developer.apple.com/documentation/advancedcommerceapi/acapriceincreaseisnotcurrentlysupportedinindiaerror + """ + + INVALID_PRORATED_PRICE_FOR_CHANGE_ITEM_WITH_EFFECTIVE_LATER = 4000222 + """ + Invalid request. A prorated price shouldn't be present to change items with the change taking effect later. + + https://developer.apple.com/documentation/advancedcommerceapi/invalidproratedpriceforchangeitemwitheffectivelatererror + """ + + FREE_TRIAL_OFFER_MUST_USE_PERIOD_COUNT_OF_ONE = 4000223 + """ + Invalid request. The subscription offer configuration is invalid; free trial offers must use a period count of 1. + + https://developer.apple.com/documentation/advancedcommerceapi/freetrialoffermustuseperiodcountofoneerror + """ + BAD_REQUEST_ABOVE_IMAGE_REQUIRES_AN_IMAGE = 4000224 """ An error that indicates that no image object is included, but the request indicates that the header should be placed above the image. @@ -467,27 +1274,83 @@ class APIError(IntEnum): https://developer.apple.com/documentation/retentionmessaging/badrequestaboveimagerequiresanimageerror """ + INVALID_GROUP_ID = 4000225 + """ + An error that indicates the group identifier is invalid. + + https://developer.apple.com/documentation/appstoreserverapi/invalidgroupiderror + """ + + INVALID_ASSIGNED_TRANSACTION_NOT_SUPPORTED = 4000227 + """ + An error that indicates the transaction is one that an organization or group assigns to the customer, which the endpoint doesn’t support. + + https://developer.apple.com/documentation/appstoreserverapi/invalidassignedtransactionnotsupportederror + """ + + UNAUTHORIZED = 4010000 + """ + Unauthorized. + + https://developer.apple.com/documentation/advancedcommerceapi/unauthorizederror + """ + SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030004 """ - An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state. - + An error that indicates the subscription doesn’t qualify for a renewal-date extension due to its subscription state. + https://developer.apple.com/documentation/appstoreserverapi/subscriptionextensionineligibleerror """ SUBSCRIPTION_MAX_EXTENSION = 4030005 """ An error that indicates the subscription doesn’t qualify for a renewal-date extension because it has already received the maximum extensions. - + https://developer.apple.com/documentation/appstoreserverapi/subscriptionmaxextensionerror """ FAMILY_SHARED_SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030007 """ - An error that indicates a subscription isn't directly eligible for a renewal date extension because the user obtained it through Family Sharing. - + An error that indicates a subscription isn’t directly eligible for a renewal date extension because the customer obtained it through Family Sharing. + https://developer.apple.com/documentation/appstoreserverapi/familysharedsubscriptionextensionineligibleerror """ + SUBSCRIPTION_DOES_NOT_EXIST = 4030008 + """ + The subscription doesn't exist. + + https://developer.apple.com/documentation/advancedcommerceapi/subscriptiondoesnotexisterror + """ + + SUBSCRIPTION_ALREADY_EXISTS = 4030009 + """ + The subscription already exists. + + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionalreadyexistserror + """ + + SUBSCRIPTION_NOT_ELIGIBLE = 4030010 + """ + The subscription isn't eligible for the requested changes. + + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionnoteligibleerror + """ + + SUBSCRIPTION_ALREADY_ACTIVE = 4030011 + """ + The subscription is already active, and cannot be reactivated or renewed at this time. + + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionalreadyactiveerror + """ + + PRODUCT_NOT_OWNED = 4030013 + """ + The customer doesn't own the product. + + https://developer.apple.com/documentation/advancedcommerceapi/productnotownederror + """ + MAXIMUM_NUMBER_OF_IMAGES_REACHED = 4030014 """ An error that indicates when you reach the maximum number of uploaded images. @@ -495,6 +1358,13 @@ class APIError(IntEnum): https://developer.apple.com/documentation/retentionmessaging/maximumnumberofimagesreachederror """ + INACTIVE_ACA_SUB = 4030015 + """ + The subscription is not active. + + https://developer.apple.com/documentation/advancedcommerceapi/inactiveacasuberror + """ + MAXIMUM_NUMBER_OF_MESSAGES_REACHED = 4030016 """ An error that indicates when you reach the maximum number of uploaded messages. @@ -504,25 +1374,67 @@ class APIError(IntEnum): MESSAGE_NOT_APPROVED = 4030017 """ - An error that indicates the message isn't in the approved state, so you can't configure it as a default message. + An error that indicates the message isn’t in the approved state, so you can’t configure it as a default message. https://developer.apple.com/documentation/retentionmessaging/messagenotapprovederror """ IMAGE_NOT_APPROVED = 4030018 """ - An error that indicates the image isn't in the approved state, so you can't configure it as part of a default message. + An error that indicates the image isn’t in the approved state, so you can’t configure it as part of a default message. https://developer.apple.com/documentation/retentionmessaging/imagenotapprovederror """ IMAGE_IN_USE = 4030019 """ - An error that indicates the image is currently in use as part of a message, so you can't delete it. + An error that indicates the image is currently in use as part of a message, so you can’t delete it. https://developer.apple.com/documentation/retentionmessaging/imageinuseerror """ + INSUFFICIENT_FUNDS = 4030020 + """ + Insufficient funds for refund. + + https://developer.apple.com/documentation/advancedcommerceapi/insufficientfundserror + """ + + ALREADY_REFUNDED = 4030021 + """ + The transaction was already refunded. + + https://developer.apple.com/documentation/advancedcommerceapi/alreadyrefundederror + """ + + STOREFRONT_CHANGE = 4030022 + """ + The storefront changed. + + https://developer.apple.com/documentation/advancedcommerceapi/storefrontchangeerror + """ + + PRODUCT_NOT_ELIGIBLE = 4030023 + """ + The product isn't eligible for the requested operation. + + https://developer.apple.com/documentation/advancedcommerceapi/productnoteligibleerror + """ + + TRANSACTION_NOT_REFUNDABLE = 4030024 + """ + The transaction is not refundable. + + https://developer.apple.com/documentation/advancedcommerceapi/transactionnotrefundableerror + """ + + TRANSACTION_CANNOT_BE_REFUNDED_CONTACT_SUPPORT = 4030025 + """ + The transaction can't be refunded; customer can contact Apple Support for assistance. + + https://developer.apple.com/documentation/advancedcommerceapi/transactioncannotberefundedcontactsupporterror + """ + FORBIDDEN_NO_PASSING_TEST = 4030026 """ An error that indicates that passing a performance test is required before you can set a URL for the production environment. @@ -530,101 +1442,122 @@ class APIError(IntEnum): https://developer.apple.com/documentation/retentionmessaging/forbiddennopassingtesterror """ + MIGRATION_NOT_ALLOWED_WHEN_PRICE_INCREASE_COMMUNICATED = 4030027 + """ + The subscription isn't eligible for migration because a price increase has already been communicated to the customer. + + https://developer.apple.com/documentation/advancedcommerceapi/migrationnotallowedwhenpriceincreasecommunicatederror + """ + + ASSIGNED_SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030028 + """ + An error that indicates a subscription isn’t eligible for a renewal date extension because the customer has access through an organization or group. + + https://developer.apple.com/documentation/appstoreserverapi/assignedsubscriptionextensionineligibleerror + """ + ACCOUNT_NOT_FOUND = 4040001 """ An error that indicates the App Store account wasn’t found. - + https://developer.apple.com/documentation/appstoreserverapi/accountnotfounderror """ ACCOUNT_NOT_FOUND_RETRYABLE = 4040002 """ An error response that indicates the App Store account wasn’t found, but you can try again. - + https://developer.apple.com/documentation/appstoreserverapi/accountnotfoundretryableerror """ APP_NOT_FOUND = 4040003 """ An error that indicates the app wasn’t found. - + https://developer.apple.com/documentation/appstoreserverapi/appnotfounderror """ APP_NOT_FOUND_RETRYABLE = 4040004 """ An error response that indicates the app wasn’t found, but you can try again. - + https://developer.apple.com/documentation/appstoreserverapi/appnotfoundretryableerror """ ORIGINAL_TRANSACTION_ID_NOT_FOUND = 4040005 """ - An error that indicates an original transaction identifier wasn't found. - + An error that indicates an original transaction identifier wasn’t found. + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionidnotfounderror """ ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE = 4040006 """ An error response that indicates the original transaction identifier wasn’t found, but you can try again. - + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionidnotfoundretryableerror """ SERVER_NOTIFICATION_URL_NOT_FOUND = 4040007 """ - An error that indicates that the App Store server couldn’t find a notifications URL for your app in this environment. - + An error that indicates the App Store server couldn’t find a notifications URL for your app in the environment. + https://developer.apple.com/documentation/appstoreserverapi/servernotificationurlnotfounderror """ TEST_NOTIFICATION_NOT_FOUND = 4040008 """ - An error that indicates that the test notification token is expired or the test notification status isn’t available. - + An error that indicates the test notification token is expired or the test notification status isn’t available. + https://developer.apple.com/documentation/appstoreserverapi/testnotificationnotfounderror """ STATUS_REQUEST_NOT_FOUND = 4040009 """ - An error that indicates the server didn't find a subscription-renewal-date extension request for the request identifier and product identifier you provided. - + An error that indicates the server didn’t find a subscription-renewal-date extension request for the request identifier and product identifier you provided. + https://developer.apple.com/documentation/appstoreserverapi/statusrequestnotfounderror """ TRANSACTION_ID_NOT_FOUND = 4040010 """ - An error that indicates a transaction identifier wasn't found. - + An error that indicates a transaction identifier wasn’t found. + https://developer.apple.com/documentation/appstoreserverapi/transactionidnotfounderror """ IMAGE_NOT_FOUND = 4040014 """ - An error that indicates the system can't find the image identifier. + An error that indicates the system can’t find the image identifier. https://developer.apple.com/documentation/retentionmessaging/imagenotfounderror """ MESSAGE_NOT_FOUND = 4040015 """ - An error that indicates the system can't find the message identifier. + An error that indicates the system can’t find the message identifier. https://developer.apple.com/documentation/retentionmessaging/messagenotfounderror """ + PRODUCT_NOT_FOUND = 4040016 + """ + Product not found. + + https://developer.apple.com/documentation/advancedcommerceapi/productnotfounderror + """ + PERFORMANCE_TEST_RUN_NOT_FOUND = 4040018 """ - An error the API returns if the service can't find the specified test run. + An error the API returns if the service can’t find the specified test run. https://developer.apple.com/documentation/retentionmessaging/performancetestrunnotfounderror """ - APP_TRANSACTION_DOES_NOT_EXIST_ERROR = 4040019 + APP_TRANSACTION_DOES_NOT_EXIST = 4040019 """ An error response that indicates an app transaction doesn’t exist for the specified customer. - + https://developer.apple.com/documentation/appstoreserverapi/apptransactiondoesnotexisterror """ @@ -642,6 +1575,13 @@ class APIError(IntEnum): https://developer.apple.com/documentation/retentionmessaging/realtimeurlnotfounderror """ + GROUP_NOT_FOUND = 4040022 + """ + An error that indicates the group wasn’t found. + + https://developer.apple.com/documentation/appstoreserverapi/groupnotfounderror + """ + IMAGE_ALREADY_EXISTS = 4090000 """ An error that indicates the image identifier already exists. @@ -658,22 +1598,22 @@ class APIError(IntEnum): RATE_LIMIT_EXCEEDED = 4290000 """ - An error that indicates that the request exceeded the rate limit. - + An error that indicates the request exceeded the rate limit. + https://developer.apple.com/documentation/appstoreserverapi/ratelimitexceedederror """ GENERAL_INTERNAL = 5000000 """ An error that indicates a general internal error. - + https://developer.apple.com/documentation/appstoreserverapi/generalinternalerror """ GENERAL_INTERNAL_RETRYABLE = 5000001 """ An error response that indicates an unknown error occurred, but you can try again. - + https://developer.apple.com/documentation/appstoreserverapi/generalinternalretryableerror """ @@ -721,6 +1661,18 @@ def _parse_retry_after(retry_after_values: Optional[List[str]]) -> Optional[int] return None return int(stripped_retry_after) +def _encode_path_segment(path_segment: str) -> str: + encoded = quote(str(path_segment), safe='') + if encoded in ('', '.', '..'): + raise ValueError() + return encoded + +def _path(template: str, *path_parameters: str) -> str: + parts = template.split('{}') + if len(parts) != len(path_parameters) + 1: + raise ValueError() + return parts[0] + ''.join(_encode_path_segment(path_parameter) + part for path_parameter, part in zip(path_parameters, parts[1:])) + class GetTransactionHistoryVersion(str, Enum): V1 = "v1" """ @@ -747,12 +1699,14 @@ def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: s self._bundle_id = bundle_id def _generate_token(self) -> str: - future_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=5) + current_time = datetime.datetime.now(datetime.timezone.utc) + future_time = current_time + datetime.timedelta(minutes=5) return jwt.encode( { "bid": self._bundle_id, "iss": self._issuer_id, "aud": "appstoreconnect-v1", + "iat": calendar.timegm(current_time.timetuple()), "exp": calendar.timegm(future_time.timetuple()), }, self._signing_key, @@ -790,11 +1744,11 @@ def _parse_response(self, status_code: int, headers: MutableMapping, json_suppli else: normalized_headers = self._normalize_headers(headers) # Best effort parsing of the response body - if not 'content-type' in headers or headers['content-type'] != 'application/json': + if not 'content-type' in headers or headers['content-type'].split(';')[0].strip().lower() != 'application/json': raise APIException(status_code, headers=normalized_headers) try: response_body = json_supplier() - raise APIException(status_code, response_body['errorCode'], response_body['errorMessage'], headers=normalized_headers) + raise APIException(status_code, response_body.get('errorCode'), response_body.get('errorMessage'), headers=normalized_headers) except APIException as e: raise e except Exception as e: @@ -825,7 +1779,7 @@ def _execute_request(self, method: str, url: str, params: Dict[str, Union[str, L def extend_renewal_date_for_all_active_subscribers(self, mass_extend_renewal_date_request: MassExtendRenewalDateRequest) -> MassExtendRenewalDateResponse: """ Uses a subscription's product identifier to extend the renewal date for all of its eligible active subscribers. - https://developer.apple.com/documentation/appstoreserverapi/extend_subscription_renewal_dates_for_all_active_subscribers + https://developer.apple.com/documentation/appstoreserverapi/extend-subscription-renewal-dates-for-all-active-subscribers :param mass_extend_renewal_date_request: The request body for extending a subscription renewal date for all of its active subscribers. :return: A response that indicates the server successfully received the subscription-renewal-date extension request. @@ -836,19 +1790,19 @@ def extend_renewal_date_for_all_active_subscribers(self, mass_extend_renewal_dat def extend_subscription_renewal_date(self, original_transaction_id: str, extend_renewal_date_request: ExtendRenewalDateRequest) -> ExtendRenewalDateResponse: """ Extends the renewal date of a customer's active subscription using the original transaction identifier. - https://developer.apple.com/documentation/appstoreserverapi/extend_a_subscription_renewal_date + https://developer.apple.com/documentation/appstoreserverapi/extend-a-subscription-renewal-date :param original_transaction_id: The original transaction identifier of the subscription receiving a renewal date extension. :param extend_renewal_date_request: The request body containing subscription-renewal-extension data. :return: A response that indicates whether an individual renewal-date extension succeeded, and related details. :throws APIException: If a response was returned indicating the request could not be processed """ - return self._make_request(f"/inApps/v1/subscriptions/extend/{original_transaction_id}", "PUT", {}, extend_renewal_date_request, ExtendRenewalDateResponse, None) + return self._make_request(_path("/inApps/v1/subscriptions/extend/{}", original_transaction_id), "PUT", {}, extend_renewal_date_request, ExtendRenewalDateResponse, None) def get_all_subscription_statuses(self, any_transaction_id: str, status: Optional[List[Status]] = None) -> StatusResponse: """ Get the statuses for all of a customer's auto-renewable subscriptions in your app. - https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses + https://developer.apple.com/documentation/appstoreserverapi/get-all-subscription-statuses :param any_transaction_id: Any transactionId, originalTransactionId, or appTransactionId that belongs to the customer for your app. :param status: An optional filter that indicates the status of subscriptions to include in the response. Your query may specify more than one status query parameter. @@ -859,16 +1813,16 @@ def get_all_subscription_statuses(self, any_transaction_id: str, status: Optiona if status is not None: queryParameters["status"] = [s.value for s in status] - return self._make_request(f"/inApps/v1/subscriptions/{any_transaction_id}", "GET", queryParameters, None, StatusResponse, None) + return self._make_request(_path("/inApps/v1/subscriptions/{}", any_transaction_id), "GET", queryParameters, None, StatusResponse, None) def get_refund_history(self, any_transaction_id: str, revision: Optional[str]) -> RefundHistoryResponse: """ Get a paginated list of all of a customer's refunded in-app purchases for your app. - https://developer.apple.com/documentation/appstoreserverapi/get_refund_history + https://developer.apple.com/documentation/appstoreserverapi/get-refund-history :param any_transaction_id: Any transactionId, originalTransactionId, or appTransactionId that belongs to the customer for your app. :param revision: A token you provide to get the next set of up to 20 transactions. All responses include a revision token. Use the revision token from the previous RefundHistoryResponse. - :return: A response that contains status information for all of a customer's auto-renewable subscriptions in your app. + :return: A response that contains an array of signed JSON Web Signature (JWS) refunded transactions, and paging information. :throws APIException: If a response was returned indicating the request could not be processed """ @@ -876,35 +1830,35 @@ def get_refund_history(self, any_transaction_id: str, revision: Optional[str]) - if revision is not None: queryParameters["revision"] = [revision] - return self._make_request(f"/inApps/v2/refund/lookup/{any_transaction_id}", "GET", queryParameters, None, RefundHistoryResponse, None) + return self._make_request(_path("/inApps/v2/refund/lookup/{}", any_transaction_id), "GET", queryParameters, None, RefundHistoryResponse, None) def get_status_of_subscription_renewal_date_extensions(self, request_identifier: str, product_id: str) -> MassExtendRenewalDateStatusResponse: """ Checks whether a renewal date extension request completed, and provides the final count of successful or failed extensions. - https://developer.apple.com/documentation/appstoreserverapi/get_status_of_subscription_renewal_date_extensions + https://developer.apple.com/documentation/appstoreserverapi/get-status-of-subscription-renewal-date-extensions :param request_identifier: The UUID that represents your request to the Extend Subscription Renewal Dates for All Active Subscribers endpoint. :param product_id: The product identifier of the auto-renewable subscription that you request a renewal-date extension for. :return: A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers. :throws APIException: If a response was returned indicating the request could not be processed """ - return self._make_request(f"/inApps/v1/subscriptions/extend/mass/{product_id}/{request_identifier}", "GET", {}, None, MassExtendRenewalDateStatusResponse, None) + return self._make_request(_path("/inApps/v1/subscriptions/extend/mass/{}/{}", product_id, request_identifier), "GET", {}, None, MassExtendRenewalDateStatusResponse, None) def get_test_notification_status(self, test_notification_token: str) -> CheckTestNotificationResponse: """ Check the status of the test App Store server notification sent to your server. - https://developer.apple.com/documentation/appstoreserverapi/get_test_notification_status + https://developer.apple.com/documentation/appstoreserverapi/get-test-notification-status :param test_notification_token: The test notification token received from the Request a Test Notification endpoint :return: A response that contains the contents of the test notification sent by the App Store server and the result from your server. :throws APIException: If a response was returned indicating the request could not be processed """ - return self._make_request(f"/inApps/v1/notifications/test/{test_notification_token}", "GET", {}, None, CheckTestNotificationResponse, None) + return self._make_request(_path("/inApps/v1/notifications/test/{}", test_notification_token), "GET", {}, None, CheckTestNotificationResponse, None) def get_notification_history(self, pagination_token: Optional[str], notification_history_request: NotificationHistoryRequest) -> NotificationHistoryResponse: """ Get a list of notifications that the App Store server attempted to send to your server. - https://developer.apple.com/documentation/appstoreserverapi/get_notification_history + https://developer.apple.com/documentation/appstoreserverapi/get-notification-history :param pagination_token: An optional token you use to get the next set of up to 20 notification history records. All responses that have more records available include a paginationToken. Omit this parameter the first time you call this endpoint. :param notification_history_request: The request body that includes the start and end dates, and optional query constraints. @@ -920,7 +1874,7 @@ def get_notification_history(self, pagination_token: Optional[str], notification def get_transaction_history(self, any_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 + https://developer.apple.com/documentation/appstoreserverapi/get-transaction-history :param any_transaction_id: Any transactionId, originalTransactionId, or appTransactionId that belongs to the customer for your app. :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. @@ -929,6 +1883,10 @@ def get_transaction_history(self, any_transaction_id: str, revision: Optional[st :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 """ + version = GetTransactionHistoryVersion(version) + if version == GetTransactionHistoryVersion.V1: + warnings.warn("GetTransactionHistoryVersion.V1 is deprecated, use GetTransactionHistoryVersion.V2 instead", DeprecationWarning, stacklevel=2) + queryParameters: Dict[str, List[str]] = dict() if revision is not None: queryParameters["revision"] = [revision] @@ -955,36 +1913,36 @@ def get_transaction_history(self, any_transaction_id: str, revision: Optional[st queryParameters["inAppOwnershipType"] = [transaction_history_request.inAppOwnershipType.value] if transaction_history_request.revoked is not None: - queryParameters["revoked"] = [str(transaction_history_request.revoked)] + queryParameters["revoked"] = ["true" if transaction_history_request.revoked else "false"] - return self._make_request("/inApps/{}/history/{}".format(version.value, any_transaction_id), "GET", queryParameters, None, HistoryResponse, None) + return self._make_request(_path("/inApps/{}/history/{}", version.value, any_transaction_id), "GET", queryParameters, None, HistoryResponse, None) def get_transaction_info(self, transaction_id: str) -> TransactionInfoResponse: """ Get information about a single transaction for your app. - https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info + https://developer.apple.com/documentation/appstoreserverapi/get-transaction-info - :param transaction_id The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. + :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. :return: A response that contains signed transaction information for a single transaction. :throws APIException: If a response was returned indicating the request could not be processed """ - return self._make_request(f"/inApps/v1/transactions/{transaction_id}", "GET", {}, None, TransactionInfoResponse, None) + return self._make_request(_path("/inApps/v1/transactions/{}", transaction_id), "GET", {}, None, TransactionInfoResponse, None) def look_up_order_id(self, order_id: str) -> OrderLookupResponse: """ Get a customer's in-app purchases from a receipt using the order ID. - https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id + https://developer.apple.com/documentation/appstoreserverapi/look-up-order-id :param order_id: The order ID for in-app purchases that belong to the customer. :return: A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order. :throws APIException: If a response was returned indicating the request could not be processed """ - return self._make_request(f"/inApps/v1/lookup/{order_id}", "GET", {}, None, OrderLookupResponse, None) + return self._make_request(_path("/inApps/v1/lookup/{}", order_id), "GET", {}, None, OrderLookupResponse, None) def request_test_notification(self) -> SendTestNotificationResponse: """ Ask App Store Server Notifications to send a test notification to your server. - https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification + https://developer.apple.com/documentation/appstoreserverapi/request-a-test-notification :return: A response that contains the test notification token. :throws APIException: If a response was returned indicating the request could not be processed @@ -1004,7 +1962,7 @@ def send_consumption_data(self, transaction_id: str, consumption_request: Consum :raises APIException: If a response was returned indicating the request could not be processed """ warnings.warn("send_consumption_data is deprecated, use send_consumption_information instead", DeprecationWarning, stacklevel=2) - self._make_request(f"/inApps/v1/transactions/consumption/{transaction_id}", "PUT", {}, consumption_request, None, None) + self._make_request(_path("/inApps/v1/transactions/consumption/{}", transaction_id), "PUT", {}, consumption_request, None, None) def send_consumption_information(self, transaction_id: str, consumption_request: ConsumptionRequest): """ @@ -1015,18 +1973,18 @@ def send_consumption_information(self, transaction_id: str, consumption_request: :param consumption_request: The request body containing consumption information. :raises APIException: If a response was returned indicating the request could not be processed """ - self._make_request(f"/inApps/v2/transactions/consumption/{transaction_id}", "PUT", {}, consumption_request, None, None) + self._make_request(_path("/inApps/v2/transactions/consumption/{}", transaction_id), "PUT", {}, consumption_request, None, 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. + :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(f"/inApps/v1/transactions/{original_transaction_id}/appAccountToken", "PUT", {}, update_app_account_token_request, None, None) + self._make_request(_path("/inApps/v1/transactions/{}/appAccountToken", original_transaction_id), "PUT", {}, update_app_account_token_request, None, None) def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optional[ImageSize] = None): """ @@ -1041,7 +1999,7 @@ def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optiona query_parameters = {} if image_size is not None: query_parameters["imageSize"] = [image_size.value] - self._make_request(f"/inApps/v1/messaging/image/{image_identifier}", "PUT", query_parameters, image, None, "image/png") + self._make_request(_path("/inApps/v1/messaging/image/{}", image_identifier), "PUT", query_parameters, image, None, "image/png") def delete_image(self, image_identifier: UUID): """ @@ -1051,7 +2009,7 @@ def delete_image(self, image_identifier: UUID): :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/delete-image """ - self._make_request(f"/inApps/v1/messaging/image/{image_identifier}", "DELETE", {}, None, None, None) + self._make_request(_path("/inApps/v1/messaging/image/{}", image_identifier), "DELETE", {}, None, None, None) def get_image_list(self) -> GetImageListResponse: """ @@ -1072,7 +2030,7 @@ def upload_message(self, message_identifier: UUID, upload_message_request_body: :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/upload-message """ - self._make_request(f"/inApps/v1/messaging/message/{message_identifier}", "PUT", {}, upload_message_request_body, None, None) + self._make_request(_path("/inApps/v1/messaging/message/{}", message_identifier), "PUT", {}, upload_message_request_body, None, None) def delete_message(self, message_identifier: UUID): """ @@ -1082,7 +2040,7 @@ def delete_message(self, message_identifier: UUID): :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/delete-message """ - self._make_request(f"/inApps/v1/messaging/message/{message_identifier}", "DELETE", {}, None, None, None) + self._make_request(_path("/inApps/v1/messaging/message/{}", message_identifier), "DELETE", {}, None, None, None) def get_message_list(self) -> GetMessageListResponse: """ @@ -1104,7 +2062,7 @@ def configure_default_message(self, product_id: str, locale: str, default_config :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/configure-default-message """ - self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "PUT", {}, default_configuration_request, None, None) + self._make_request(_path("/inApps/v1/messaging/default/{}/{}", product_id, locale), "PUT", {}, default_configuration_request, None, None) def delete_default_message(self, product_id: str, locale: str): """ @@ -1115,7 +2073,7 @@ def delete_default_message(self, product_id: str, locale: str): :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/delete-default-message """ - self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "DELETE", {}, None, None, None) + self._make_request(_path("/inApps/v1/messaging/default/{}/{}", product_id, locale), "DELETE", {}, None, None, None) def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurationResponse: """ @@ -1127,7 +2085,7 @@ def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurat :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-default-message """ - return self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "GET", {}, None, DefaultConfigurationResponse, None) + return self._make_request(_path("/inApps/v1/messaging/default/{}/{}", product_id, locale), "GET", {}, None, DefaultConfigurationResponse, None) def configure_realtime_url(self, realtime_url_request: RealtimeUrlRequest): """ @@ -1169,7 +2127,7 @@ def initiate_performance_test(self, performance_test_request: PerformanceTestReq """ return self._make_request("/inApps/v1/messaging/performanceTest", "POST", {}, performance_test_request, PerformanceTestResponse, None) - def get_performance_test_results(self, request_id: str) -> PerformanceTestResultResponse: + def get_performance_test_results(self, request_id: UUID) -> PerformanceTestResultResponse: """ Gets the results of the performance test for the specified identifier. @@ -1178,7 +2136,7 @@ def get_performance_test_results(self, request_id: str) -> PerformanceTestResult :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-performance-test-results """ - return self._make_request(f"/inApps/v1/messaging/performanceTest/result/{request_id}", "GET", {}, None, PerformanceTestResultResponse, None) + return self._make_request(_path("/inApps/v1/messaging/performanceTest/result/{}", str(request_id).lower()), "GET", {}, None, PerformanceTestResultResponse, None) def get_app_transaction_info(self, any_transaction_id: str) -> AppTransactionInfoResponse: """ @@ -1189,17 +2147,17 @@ def get_app_transaction_info(self, any_transaction_id: str) -> AppTransactionInf :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/appstoreserverapi/get-app-transaction-info """ - return self._make_request(f"/inApps/v1/transactions/appTransactions/{any_transaction_id}", "GET", {}, None, AppTransactionInfoResponse, None) + return self._make_request(_path("/inApps/v1/transactions/appTransactions/{}", any_transaction_id), "GET", {}, None, AppTransactionInfoResponse, None) def finish_transaction(self, transaction_id: str): """ Notifies the App Store server that your system has finished processing the customer's transaction. https://developer.apple.com/documentation/appstoreserverapi/finish-transaction - :param transaction_id The transaction identifier of the transaction to mark as finished. + :param transaction_id: The transaction identifier of the transaction to mark as finished. :raises APIException: If a response was returned indicating the request could not be processed """ - self._make_request(f"/inApps/v1/transactions/{transaction_id}/finish", "POST", {}, None, None, None) + self._make_request(_path("/inApps/v1/transactions/{}/finish", transaction_id), "POST", {}, None, None, None) def change_subscription_price(self, transaction_id: str, subscription_price_change_request: AdvancedCommerceSubscriptionPriceChangeRequest) -> AdvancedCommerceSubscriptionPriceChangeResponse: """ @@ -1211,7 +2169,7 @@ def change_subscription_price(self, transaction_id: str, subscription_price_chan :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/change-subscription-price """ - return self._make_request(f"/advancedCommerce/v1/subscription/changePrice/{transaction_id}", "POST", {}, subscription_price_change_request, AdvancedCommerceSubscriptionPriceChangeResponse, None) + return self._make_request(_path("/advancedCommerce/v1/subscription/changePrice/{}", transaction_id), "POST", {}, subscription_price_change_request, AdvancedCommerceSubscriptionPriceChangeResponse, None) def cancel_subscription(self, transaction_id: str, subscription_cancel_request: AdvancedCommerceSubscriptionCancelRequest) -> AdvancedCommerceSubscriptionCancelResponse: """ @@ -1223,7 +2181,7 @@ def cancel_subscription(self, transaction_id: str, subscription_cancel_request: :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/cancel-a-subscription """ - return self._make_request(f"/advancedCommerce/v1/subscription/cancel/{transaction_id}", "POST", {}, subscription_cancel_request, AdvancedCommerceSubscriptionCancelResponse, None) + return self._make_request(_path("/advancedCommerce/v1/subscription/cancel/{}", transaction_id), "POST", {}, subscription_cancel_request, AdvancedCommerceSubscriptionCancelResponse, None) def revoke_subscription(self, transaction_id: str, subscription_revoke_request: AdvancedCommerceSubscriptionRevokeRequest) -> AdvancedCommerceSubscriptionRevokeResponse: """ @@ -1235,7 +2193,7 @@ def revoke_subscription(self, transaction_id: str, subscription_revoke_request: :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/revoke-subscription """ - return self._make_request(f"/advancedCommerce/v1/subscription/revoke/{transaction_id}", "POST", {}, subscription_revoke_request, AdvancedCommerceSubscriptionRevokeResponse, None) + return self._make_request(_path("/advancedCommerce/v1/subscription/revoke/{}", transaction_id), "POST", {}, subscription_revoke_request, AdvancedCommerceSubscriptionRevokeResponse, None) def request_transaction_refund(self, transaction_id: str, request_refund_request: AdvancedCommerceRequestRefundRequest) -> AdvancedCommerceRequestRefundResponse: """ @@ -1247,7 +2205,7 @@ def request_transaction_refund(self, transaction_id: str, request_refund_request :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/request-transaction-refund """ - return self._make_request(f"/advancedCommerce/v1/transaction/requestRefund/{transaction_id}", "POST", {}, request_refund_request, AdvancedCommerceRequestRefundResponse, None) + return self._make_request(_path("/advancedCommerce/v1/transaction/requestRefund/{}", transaction_id), "POST", {}, request_refund_request, AdvancedCommerceRequestRefundResponse, None) def change_subscription_metadata(self, transaction_id: str, subscription_change_metadata_request: AdvancedCommerceSubscriptionChangeMetadataRequest) -> AdvancedCommerceSubscriptionChangeMetadataResponse: """ @@ -1259,7 +2217,7 @@ def change_subscription_metadata(self, transaction_id: str, subscription_change_ :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/change-subscription-metadata """ - return self._make_request(f"/advancedCommerce/v1/subscription/changeMetadata/{transaction_id}", "POST", {}, subscription_change_metadata_request, AdvancedCommerceSubscriptionChangeMetadataResponse, None) + return self._make_request(_path("/advancedCommerce/v1/subscription/changeMetadata/{}", transaction_id), "POST", {}, subscription_change_metadata_request, AdvancedCommerceSubscriptionChangeMetadataResponse, None) def migrate_subscription_to_advanced_commerce_api(self, transaction_id: str, subscription_migrate_request: AdvancedCommerceSubscriptionMigrateRequest) -> AdvancedCommerceSubscriptionMigrateResponse: """ @@ -1271,7 +2229,37 @@ def migrate_subscription_to_advanced_commerce_api(self, transaction_id: str, sub :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/migrate-subscription-to-advanced-commerce-api """ - return self._make_request(f"/advancedCommerce/v1/subscription/migrate/{transaction_id}", "POST", {}, subscription_migrate_request, AdvancedCommerceSubscriptionMigrateResponse, None) + return self._make_request(_path("/advancedCommerce/v1/subscription/migrate/{}", transaction_id), "POST", {}, subscription_migrate_request, AdvancedCommerceSubscriptionMigrateResponse, None) + + def get_customer_groups(self, any_transaction_id: str) -> GetCustomerGroupsResponse: + """ + Get the groups that a customer belongs to, and their role in each group. + + :param any_transaction_id: Any originalTransactionId, transactionId, or appTransactionId that belongs to the customer for your app. + :return: A response that contains the groups a customer belongs to, and their role in each group. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/appstoreserverapi/get-customer-groups + """ + return self._make_request(_path("/groups/v1/currentGroups/{}", any_transaction_id), "GET", {}, None, GetCustomerGroupsResponse, None) + + def get_group_members(self, group_id: str, pagination_token: Optional[str] = None, limit: Optional[int] = None) -> GetGroupMembersResponse: + """ + Get a paginated list of the customers that belong to a group. + + :param group_id: The identifier of a group in your app, which the Get Customer Groups endpoint returns. + :param pagination_token: An optional token you use to get the next set of members. Responses that have more members available include a paginationToken. Omit this parameter the first time you call this endpoint. + :param limit: An optional maximum number of members to return in a single response. + :return: A response that contains a page of the customers that belong to a group. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/appstoreserverapi/get-group-members + """ + queryParameters: Dict[str, List[str]] = dict() + if pagination_token is not None: + queryParameters["paginationToken"] = [pagination_token] + if limit is not None: + queryParameters["limit"] = [str(limit)] + + return self._make_request(_path("/groups/v1/group/{}", group_id), "GET", queryParameters, None, GetGroupMembersResponse, None) class AsyncAppStoreServerAPIClient(BaseAppStoreServerAPIClient): def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment): @@ -1313,7 +2301,7 @@ async def _execute_request(self, method: str, url: str, params: Dict[str, Union[ async def extend_renewal_date_for_all_active_subscribers(self, mass_extend_renewal_date_request: MassExtendRenewalDateRequest) -> MassExtendRenewalDateResponse: """ Uses a subscription's product identifier to extend the renewal date for all of its eligible active subscribers. - https://developer.apple.com/documentation/appstoreserverapi/extend_subscription_renewal_dates_for_all_active_subscribers + https://developer.apple.com/documentation/appstoreserverapi/extend-subscription-renewal-dates-for-all-active-subscribers :param mass_extend_renewal_date_request: The request body for extending a subscription renewal date for all of its active subscribers. :return: A response that indicates the server successfully received the subscription-renewal-date extension request. @@ -1324,19 +2312,19 @@ async def extend_renewal_date_for_all_active_subscribers(self, mass_extend_renew async def extend_subscription_renewal_date(self, original_transaction_id: str, extend_renewal_date_request: ExtendRenewalDateRequest) -> ExtendRenewalDateResponse: """ Extends the renewal date of a customer's active subscription using the original transaction identifier. - https://developer.apple.com/documentation/appstoreserverapi/extend_a_subscription_renewal_date + https://developer.apple.com/documentation/appstoreserverapi/extend-a-subscription-renewal-date :param original_transaction_id: The original transaction identifier of the subscription receiving a renewal date extension. :param extend_renewal_date_request: The request body containing subscription-renewal-extension data. :return: A response that indicates whether an individual renewal-date extension succeeded, and related details. :throws APIException: If a response was returned indicating the request could not be processed """ - return await self._make_request(f"/inApps/v1/subscriptions/extend/{original_transaction_id}", "PUT", {}, extend_renewal_date_request, ExtendRenewalDateResponse, None) + return await self._make_request(_path("/inApps/v1/subscriptions/extend/{}", original_transaction_id), "PUT", {}, extend_renewal_date_request, ExtendRenewalDateResponse, None) async def get_all_subscription_statuses(self, any_transaction_id: str, status: Optional[List[Status]] = None) -> StatusResponse: """ Get the statuses for all of a customer's auto-renewable subscriptions in your app. - https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses + https://developer.apple.com/documentation/appstoreserverapi/get-all-subscription-statuses :param any_transaction_id: Any transactionId, originalTransactionId, or appTransactionId that belongs to the customer for your app. :param status: An optional filter that indicates the status of subscriptions to include in the response. Your query may specify more than one status query parameter. @@ -1347,16 +2335,16 @@ async def get_all_subscription_statuses(self, any_transaction_id: str, status: O if status is not None: queryParameters["status"] = [s.value for s in status] - return await self._make_request(f"/inApps/v1/subscriptions/{any_transaction_id}", "GET", queryParameters, None, StatusResponse, None) + return await self._make_request(_path("/inApps/v1/subscriptions/{}", any_transaction_id), "GET", queryParameters, None, StatusResponse, None) async def get_refund_history(self, any_transaction_id: str, revision: Optional[str]) -> RefundHistoryResponse: """ Get a paginated list of all of a customer's refunded in-app purchases for your app. - https://developer.apple.com/documentation/appstoreserverapi/get_refund_history + https://developer.apple.com/documentation/appstoreserverapi/get-refund-history :param any_transaction_id: Any transactionId, originalTransactionId, or appTransactionId that belongs to the customer for your app. :param revision: A token you provide to get the next set of up to 20 transactions. All responses include a revision token. Use the revision token from the previous RefundHistoryResponse. - :return: A response that contains status information for all of a customer's auto-renewable subscriptions in your app. + :return: A response that contains an array of signed JSON Web Signature (JWS) refunded transactions, and paging information. :throws APIException: If a response was returned indicating the request could not be processed """ @@ -1364,35 +2352,35 @@ async def get_refund_history(self, any_transaction_id: str, revision: Optional[s if revision is not None: queryParameters["revision"] = [revision] - return await self._make_request(f"/inApps/v2/refund/lookup/{any_transaction_id}", "GET", queryParameters, None, RefundHistoryResponse, None) + return await self._make_request(_path("/inApps/v2/refund/lookup/{}", any_transaction_id), "GET", queryParameters, None, RefundHistoryResponse, None) async def get_status_of_subscription_renewal_date_extensions(self, request_identifier: str, product_id: str) -> MassExtendRenewalDateStatusResponse: """ Checks whether a renewal date extension request completed, and provides the final count of successful or failed extensions. - https://developer.apple.com/documentation/appstoreserverapi/get_status_of_subscription_renewal_date_extensions + https://developer.apple.com/documentation/appstoreserverapi/get-status-of-subscription-renewal-date-extensions :param request_identifier: The UUID that represents your request to the Extend Subscription Renewal Dates for All Active Subscribers endpoint. :param product_id: The product identifier of the auto-renewable subscription that you request a renewal-date extension for. :return: A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers. :throws APIException: If a response was returned indicating the request could not be processed """ - return await self._make_request(f"/inApps/v1/subscriptions/extend/mass/{product_id}/{request_identifier}", "GET", {}, None, MassExtendRenewalDateStatusResponse, None) + return await self._make_request(_path("/inApps/v1/subscriptions/extend/mass/{}/{}", product_id, request_identifier), "GET", {}, None, MassExtendRenewalDateStatusResponse, None) async def get_test_notification_status(self, test_notification_token: str) -> CheckTestNotificationResponse: """ Check the status of the test App Store server notification sent to your server. - https://developer.apple.com/documentation/appstoreserverapi/get_test_notification_status + https://developer.apple.com/documentation/appstoreserverapi/get-test-notification-status :param test_notification_token: The test notification token received from the Request a Test Notification endpoint :return: A response that contains the contents of the test notification sent by the App Store server and the result from your server. :throws APIException: If a response was returned indicating the request could not be processed """ - return await self._make_request(f"/inApps/v1/notifications/test/{test_notification_token}", "GET", {}, None, CheckTestNotificationResponse, None) + return await self._make_request(_path("/inApps/v1/notifications/test/{}", test_notification_token), "GET", {}, None, CheckTestNotificationResponse, None) async def get_notification_history(self, pagination_token: Optional[str], notification_history_request: NotificationHistoryRequest) -> NotificationHistoryResponse: """ Get a list of notifications that the App Store server attempted to send to your server. - https://developer.apple.com/documentation/appstoreserverapi/get_notification_history + https://developer.apple.com/documentation/appstoreserverapi/get-notification-history :param pagination_token: An optional token you use to get the next set of up to 20 notification history records. All responses that have more records available include a paginationToken. Omit this parameter the first time you call this endpoint. :param notification_history_request: The request body that includes the start and end dates, and optional query constraints. @@ -1408,7 +2396,7 @@ async def get_notification_history(self, pagination_token: Optional[str], notifi async def get_transaction_history(self, any_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 + https://developer.apple.com/documentation/appstoreserverapi/get-transaction-history :param any_transaction_id: Any transactionId, originalTransactionId, or appTransactionId that belongs to the customer for your app. :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. @@ -1417,6 +2405,10 @@ async def get_transaction_history(self, any_transaction_id: str, revision: Optio :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 """ + version = GetTransactionHistoryVersion(version) + if version == GetTransactionHistoryVersion.V1: + warnings.warn("GetTransactionHistoryVersion.V1 is deprecated, use GetTransactionHistoryVersion.V2 instead", DeprecationWarning, stacklevel=2) + queryParameters: Dict[str, List[str]] = dict() if revision is not None: queryParameters["revision"] = [revision] @@ -1443,35 +2435,35 @@ async def get_transaction_history(self, any_transaction_id: str, revision: Optio queryParameters["inAppOwnershipType"] = [transaction_history_request.inAppOwnershipType.value] if transaction_history_request.revoked is not None: - queryParameters["revoked"] = [str(transaction_history_request.revoked)] + queryParameters["revoked"] = ["true" if transaction_history_request.revoked else "false"] - return await self._make_request("/inApps/" + version + "/history/" + any_transaction_id, "GET", queryParameters, None, HistoryResponse, None) + return await self._make_request(_path("/inApps/{}/history/{}", version.value, any_transaction_id), "GET", queryParameters, None, HistoryResponse, None) async def get_transaction_info(self, transaction_id: str) -> TransactionInfoResponse: """ Get information about a single transaction for your app. - https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info + https://developer.apple.com/documentation/appstoreserverapi/get-transaction-info - :param transaction_id The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. + :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. :return: A response that contains signed transaction information for a single transaction. :throws APIException: If a response was returned indicating the request could not be processed """ - return await self._make_request(f"/inApps/v1/transactions/{transaction_id}", "GET", {}, None, TransactionInfoResponse, None) + return await self._make_request(_path("/inApps/v1/transactions/{}", transaction_id), "GET", {}, None, TransactionInfoResponse, None) async def look_up_order_id(self, order_id: str) -> OrderLookupResponse: """ Get a customer's in-app purchases from a receipt using the order ID. - https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id + https://developer.apple.com/documentation/appstoreserverapi/look-up-order-id :param order_id: The order ID for in-app purchases that belong to the customer. :return: A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order. :throws APIException: If a response was returned indicating the request could not be processed """ - return await self._make_request(f"/inApps/v1/lookup/{order_id}", "GET", {}, None, OrderLookupResponse, None) + return await self._make_request(_path("/inApps/v1/lookup/{}", order_id), "GET", {}, None, OrderLookupResponse, None) async def request_test_notification(self) -> SendTestNotificationResponse: """ Ask App Store Server Notifications to send a test notification to your server. - https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification + https://developer.apple.com/documentation/appstoreserverapi/request-a-test-notification :return: A response that contains the test notification token. :throws APIException: If a response was returned indicating the request could not be processed @@ -1491,7 +2483,7 @@ 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 """ warnings.warn("send_consumption_data is deprecated, use send_consumption_information instead", DeprecationWarning, stacklevel=2) - await self._make_request(f"/inApps/v1/transactions/consumption/{transaction_id}", "PUT", {}, consumption_request, None, None) + await self._make_request(_path("/inApps/v1/transactions/consumption/{}", transaction_id), "PUT", {}, consumption_request, None, None) async def send_consumption_information(self, transaction_id: str, consumption_request: ConsumptionRequest): """ @@ -1502,18 +2494,18 @@ async def send_consumption_information(self, transaction_id: str, consumption_re :param consumption_request: The request body containing consumption information. :raises APIException: If a response was returned indicating the request could not be processed """ - await self._make_request(f"/inApps/v2/transactions/consumption/{transaction_id}", "PUT", {}, consumption_request, None, None) + await self._make_request(_path("/inApps/v2/transactions/consumption/{}", transaction_id), "PUT", {}, consumption_request, None, 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. + :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(f"/inApps/v1/transactions/{original_transaction_id}/appAccountToken", "PUT", {}, update_app_account_token_request, None, None) + await self._make_request(_path("/inApps/v1/transactions/{}/appAccountToken", original_transaction_id), "PUT", {}, update_app_account_token_request, None, None) async def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optional[ImageSize] = None): """ @@ -1528,7 +2520,7 @@ async def upload_image(self, image_identifier: UUID, image: bytes, image_size: O query_parameters = {} if image_size is not None: query_parameters["imageSize"] = [image_size.value] - await self._make_request(f"/inApps/v1/messaging/image/{image_identifier}", "PUT", query_parameters, image, None, "image/png") + await self._make_request(_path("/inApps/v1/messaging/image/{}", image_identifier), "PUT", query_parameters, image, None, "image/png") async def delete_image(self, image_identifier: UUID): """ @@ -1538,7 +2530,7 @@ async def delete_image(self, image_identifier: UUID): :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/delete-image """ - await self._make_request(f"/inApps/v1/messaging/image/{image_identifier}", "DELETE", {}, None, None, None) + await self._make_request(_path("/inApps/v1/messaging/image/{}", image_identifier), "DELETE", {}, None, None, None) async def get_image_list(self) -> GetImageListResponse: """ @@ -1559,7 +2551,7 @@ async def upload_message(self, message_identifier: UUID, upload_message_request_ :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/upload-message """ - await self._make_request(f"/inApps/v1/messaging/message/{message_identifier}", "PUT", {}, upload_message_request_body, None, None) + await self._make_request(_path("/inApps/v1/messaging/message/{}", message_identifier), "PUT", {}, upload_message_request_body, None, None) async def delete_message(self, message_identifier: UUID): """ @@ -1569,7 +2561,7 @@ async def delete_message(self, message_identifier: UUID): :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/delete-message """ - await self._make_request(f"/inApps/v1/messaging/message/{message_identifier}", "DELETE", {}, None, None, None) + await self._make_request(_path("/inApps/v1/messaging/message/{}", message_identifier), "DELETE", {}, None, None, None) async def get_message_list(self) -> GetMessageListResponse: """ @@ -1591,7 +2583,7 @@ async def configure_default_message(self, product_id: str, locale: str, default_ :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/configure-default-message """ - await self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "PUT", {}, default_configuration_request, None, None) + await self._make_request(_path("/inApps/v1/messaging/default/{}/{}", product_id, locale), "PUT", {}, default_configuration_request, None, None) async def delete_default_message(self, product_id: str, locale: str): """ @@ -1602,7 +2594,7 @@ async def delete_default_message(self, product_id: str, locale: str): :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/delete-default-message """ - await self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "DELETE", {}, None, None, None) + await self._make_request(_path("/inApps/v1/messaging/default/{}/{}", product_id, locale), "DELETE", {}, None, None, None) async def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurationResponse: """ @@ -1614,7 +2606,7 @@ async def get_default_message(self, product_id: str, locale: str) -> DefaultConf :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-default-message """ - return await self._make_request(f"/inApps/v1/messaging/default/{product_id}/{locale}", "GET", {}, None, DefaultConfigurationResponse, None) + return await self._make_request(_path("/inApps/v1/messaging/default/{}/{}", product_id, locale), "GET", {}, None, DefaultConfigurationResponse, None) async def configure_realtime_url(self, realtime_url_request: RealtimeUrlRequest): """ @@ -1656,7 +2648,7 @@ async def initiate_performance_test(self, performance_test_request: PerformanceT """ return await self._make_request("/inApps/v1/messaging/performanceTest", "POST", {}, performance_test_request, PerformanceTestResponse, None) - async def get_performance_test_results(self, request_id: str) -> PerformanceTestResultResponse: + async def get_performance_test_results(self, request_id: UUID) -> PerformanceTestResultResponse: """ Gets the results of the performance test for the specified identifier. @@ -1665,7 +2657,7 @@ async def get_performance_test_results(self, request_id: str) -> PerformanceTest :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-performance-test-results """ - return await self._make_request(f"/inApps/v1/messaging/performanceTest/result/{request_id}", "GET", {}, None, PerformanceTestResultResponse, None) + return await self._make_request(_path("/inApps/v1/messaging/performanceTest/result/{}", str(request_id).lower()), "GET", {}, None, PerformanceTestResultResponse, None) async def get_app_transaction_info(self, any_transaction_id: str) -> AppTransactionInfoResponse: """ @@ -1676,17 +2668,17 @@ async def get_app_transaction_info(self, any_transaction_id: str) -> AppTransact :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/appstoreserverapi/get-app-transaction-info """ - return await self._make_request(f"/inApps/v1/transactions/appTransactions/{any_transaction_id}", "GET", {}, None, AppTransactionInfoResponse, None) + return await self._make_request(_path("/inApps/v1/transactions/appTransactions/{}", any_transaction_id), "GET", {}, None, AppTransactionInfoResponse, None) async def finish_transaction(self, transaction_id: str): """ Notifies the App Store server that your system has finished processing the customer's transaction. https://developer.apple.com/documentation/appstoreserverapi/finish-transaction - :param transaction_id The transaction identifier of the transaction to mark as finished. + :param transaction_id: The transaction identifier of the transaction to mark as finished. :raises APIException: If a response was returned indicating the request could not be processed """ - await self._make_request(f"/inApps/v1/transactions/{transaction_id}/finish", "POST", {}, None, None, None) + await self._make_request(_path("/inApps/v1/transactions/{}/finish", transaction_id), "POST", {}, None, None, None) async def change_subscription_price(self, transaction_id: str, subscription_price_change_request: AdvancedCommerceSubscriptionPriceChangeRequest) -> AdvancedCommerceSubscriptionPriceChangeResponse: """ @@ -1698,7 +2690,7 @@ async def change_subscription_price(self, transaction_id: str, subscription_pric :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/change-subscription-price """ - return await self._make_request(f"/advancedCommerce/v1/subscription/changePrice/{transaction_id}", "POST", {}, subscription_price_change_request, AdvancedCommerceSubscriptionPriceChangeResponse, None) + return await self._make_request(_path("/advancedCommerce/v1/subscription/changePrice/{}", transaction_id), "POST", {}, subscription_price_change_request, AdvancedCommerceSubscriptionPriceChangeResponse, None) async def cancel_subscription(self, transaction_id: str, subscription_cancel_request: AdvancedCommerceSubscriptionCancelRequest) -> AdvancedCommerceSubscriptionCancelResponse: """ @@ -1710,7 +2702,7 @@ async def cancel_subscription(self, transaction_id: str, subscription_cancel_req :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/cancel-a-subscription """ - return await self._make_request(f"/advancedCommerce/v1/subscription/cancel/{transaction_id}", "POST", {}, subscription_cancel_request, AdvancedCommerceSubscriptionCancelResponse, None) + return await self._make_request(_path("/advancedCommerce/v1/subscription/cancel/{}", transaction_id), "POST", {}, subscription_cancel_request, AdvancedCommerceSubscriptionCancelResponse, None) async def revoke_subscription(self, transaction_id: str, subscription_revoke_request: AdvancedCommerceSubscriptionRevokeRequest) -> AdvancedCommerceSubscriptionRevokeResponse: """ @@ -1722,7 +2714,7 @@ async def revoke_subscription(self, transaction_id: str, subscription_revoke_req :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/revoke-subscription """ - return await self._make_request(f"/advancedCommerce/v1/subscription/revoke/{transaction_id}", "POST", {}, subscription_revoke_request, AdvancedCommerceSubscriptionRevokeResponse, None) + return await self._make_request(_path("/advancedCommerce/v1/subscription/revoke/{}", transaction_id), "POST", {}, subscription_revoke_request, AdvancedCommerceSubscriptionRevokeResponse, None) async def request_transaction_refund(self, transaction_id: str, request_refund_request: AdvancedCommerceRequestRefundRequest) -> AdvancedCommerceRequestRefundResponse: """ @@ -1734,7 +2726,7 @@ async def request_transaction_refund(self, transaction_id: str, request_refund_r :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/request-transaction-refund """ - return await self._make_request(f"/advancedCommerce/v1/transaction/requestRefund/{transaction_id}", "POST", {}, request_refund_request, AdvancedCommerceRequestRefundResponse, None) + return await self._make_request(_path("/advancedCommerce/v1/transaction/requestRefund/{}", transaction_id), "POST", {}, request_refund_request, AdvancedCommerceRequestRefundResponse, None) async def change_subscription_metadata(self, transaction_id: str, subscription_change_metadata_request: AdvancedCommerceSubscriptionChangeMetadataRequest) -> AdvancedCommerceSubscriptionChangeMetadataResponse: """ @@ -1746,7 +2738,7 @@ async def change_subscription_metadata(self, transaction_id: str, subscription_c :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/change-subscription-metadata """ - return await self._make_request(f"/advancedCommerce/v1/subscription/changeMetadata/{transaction_id}", "POST", {}, subscription_change_metadata_request, AdvancedCommerceSubscriptionChangeMetadataResponse, None) + return await self._make_request(_path("/advancedCommerce/v1/subscription/changeMetadata/{}", transaction_id), "POST", {}, subscription_change_metadata_request, AdvancedCommerceSubscriptionChangeMetadataResponse, None) async def migrate_subscription_to_advanced_commerce_api(self, transaction_id: str, subscription_migrate_request: AdvancedCommerceSubscriptionMigrateRequest) -> AdvancedCommerceSubscriptionMigrateResponse: """ @@ -1758,4 +2750,34 @@ async def migrate_subscription_to_advanced_commerce_api(self, transaction_id: st :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/advancedcommerceapi/migrate-subscription-to-advanced-commerce-api """ - return await self._make_request(f"/advancedCommerce/v1/subscription/migrate/{transaction_id}", "POST", {}, subscription_migrate_request, AdvancedCommerceSubscriptionMigrateResponse, None) + return await self._make_request(_path("/advancedCommerce/v1/subscription/migrate/{}", transaction_id), "POST", {}, subscription_migrate_request, AdvancedCommerceSubscriptionMigrateResponse, None) + + async def get_customer_groups(self, any_transaction_id: str) -> GetCustomerGroupsResponse: + """ + Get the groups that a customer belongs to, and their role in each group. + + :param any_transaction_id: Any originalTransactionId, transactionId, or appTransactionId that belongs to the customer for your app. + :return: A response that contains the groups a customer belongs to, and their role in each group. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/appstoreserverapi/get-customer-groups + """ + return await self._make_request(_path("/groups/v1/currentGroups/{}", any_transaction_id), "GET", {}, None, GetCustomerGroupsResponse, None) + + async def get_group_members(self, group_id: str, pagination_token: Optional[str] = None, limit: Optional[int] = None) -> GetGroupMembersResponse: + """ + Get a paginated list of the customers that belong to a group. + + :param group_id: The identifier of a group in your app, which the Get Customer Groups endpoint returns. + :param pagination_token: An optional token you use to get the next set of members. Responses that have more members available include a paginationToken. Omit this parameter the first time you call this endpoint. + :param limit: An optional maximum number of members to return in a single response. + :return: A response that contains a page of the customers that belong to a group. + :raises APIException: If a response was returned indicating the request could not be processed + :see: https://developer.apple.com/documentation/appstoreserverapi/get-group-members + """ + queryParameters: Dict[str, List[str]] = dict() + if pagination_token is not None: + queryParameters["paginationToken"] = [pagination_token] + if limit is not None: + queryParameters["limit"] = [str(limit)] + + return await self._make_request(_path("/groups/v1/group/{}", group_id), "GET", queryParameters, None, GetGroupMembersResponse, None) diff --git a/appstoreserverlibrary/jws_signature_creator.py b/appstoreserverlibrary/jws_signature_creator.py index 491e9f8b..e20b386f 100644 --- a/appstoreserverlibrary/jws_signature_creator.py +++ b/appstoreserverlibrary/jws_signature_creator.py @@ -12,7 +12,7 @@ from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter -class AdvancedCommerceAPIInAppRequest: +class AdvancedCommerceInAppRequest: def __init__(self): pass @@ -52,7 +52,7 @@ def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: s """ super().__init__(audience="promotional-offer", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id) - def create_signature(self, product_id: str, offer_identifier: str, transaction_id: Optional[str]) -> str: + def create_signature(self, product_id: str, offer_identifier: str, transaction_id: Optional[str] = None) -> str: """ Create a promotional offer V2 signature. https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests @@ -109,10 +109,10 @@ def create_signature(self, product_id: str, allow_introductory_offer: bool, tran } return self._create_signature(feature_specific_claims=feature_specific_claims) -class AdvancedCommerceAPIInAppSignatureCreator(JWSSignatureCreator): +class AdvancedCommerceInAppSignatureCreator(JWSSignatureCreator): def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): """ - Create an AdvancedCommerceAPIInAppSignatureCreator + Create an AdvancedCommerceInAppSignatureCreator :param signing_key: Your private key downloaded from App Store Connect :param key_id: Your private key ID from App Store Connect @@ -121,7 +121,7 @@ def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: s """ super().__init__(audience="advanced-commerce-api", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id) - def create_signature(self, advanced_commerce_in_app_request: AdvancedCommerceAPIInAppRequest) -> str: + def create_signature(self, advanced_commerce_in_app_request: AdvancedCommerceInAppRequest) -> str: """ Create an Advanced Commerce in-app signed request. https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests @@ -137,4 +137,4 @@ def create_signature(self, advanced_commerce_in_app_request: AdvancedCommerceAPI feature_specific_claims = { "request": encoded_request } - return self._create_signature(feature_specific_claims=feature_specific_claims) \ No newline at end of file + return self._create_signature(feature_specific_claims=feature_specific_claims) diff --git a/appstoreserverlibrary/models/AbstractAdvancedCommerceInAppRequest.py b/appstoreserverlibrary/models/AbstractAdvancedCommerceInAppRequest.py index 01295251..a949a4af 100644 --- a/appstoreserverlibrary/models/AbstractAdvancedCommerceInAppRequest.py +++ b/appstoreserverlibrary/models/AbstractAdvancedCommerceInAppRequest.py @@ -6,9 +6,9 @@ import attr from .AdvancedCommerceRequest import AdvancedCommerceRequest -from ..jws_signature_creator import AdvancedCommerceAPIInAppRequest +from ..jws_signature_creator import AdvancedCommerceInAppRequest @define -class AbstractAdvancedCommerceInAppRequest(AdvancedCommerceRequest, AdvancedCommerceAPIInAppRequest, ABC): +class AbstractAdvancedCommerceInAppRequest(AdvancedCommerceRequest, AdvancedCommerceInAppRequest, ABC): operation: str = attr.ib() version: str = attr.ib() \ No newline at end of file diff --git a/appstoreserverlibrary/models/AccountTenure.py b/appstoreserverlibrary/models/AccountTenure.py index fbe947aa..74893fbf 100644 --- a/appstoreserverlibrary/models/AccountTenure.py +++ b/appstoreserverlibrary/models/AccountTenure.py @@ -6,15 +6,46 @@ class AccountTenure(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ - The age of the customer's account. - + The age of the customer’s account. + https://developer.apple.com/documentation/appstoreserverapi/accounttenure """ UNDECLARED = 0 + """ + Account age is undeclared. + """ + ZERO_TO_THREE_DAYS = 1 + """ + Account age is between 0–3 days. + """ + THREE_DAYS_TO_TEN_DAYS = 2 + """ + Account age is between 3–10 days. + """ + TEN_DAYS_TO_THIRTY_DAYS = 3 + """ + Account age is between 10–30 days. + """ + THIRTY_DAYS_TO_NINETY_DAYS = 4 + """ + Account age is between 30–90 days. + """ + NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS = 5 + """ + Account age is between 90–180 days. + """ + ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS = 6 + """ + Account age is between 180–365 days. + """ + GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS = 7 + """ + Account age is over 365 days. + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceDescriptors.py b/appstoreserverlibrary/models/AdvancedCommerceDescriptors.py index ff5a5eb4..8489d3bd 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceDescriptors.py +++ b/appstoreserverlibrary/models/AdvancedCommerceDescriptors.py @@ -12,16 +12,17 @@ class AdvancedCommerceDescriptors: https://developer.apple.com/documentation/advancedcommerceapi/descriptors """ - description: str = attr.ib(validator=HelperValidationUtils.description_validator) + + description: str = attr.ib(kw_only=True, validator=HelperValidationUtils.description_validator) """ - A string you provide that describes a SKU. - + A string that contains a description of the product. + https://developer.apple.com/documentation/advancedcommerceapi/description """ - displayName: str = attr.ib(validator=HelperValidationUtils.display_name_validator) + displayName: str = attr.ib(kw_only=True, validator=HelperValidationUtils.display_name_validator) """ - A string with a product name that you can localize and is suitable for display to customers. - + A string that contains the name of the product, suitable for display to customers. + https://developer.apple.com/documentation/advancedcommerceapi/displayname - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceEffective.py b/appstoreserverlibrary/models/AdvancedCommerceEffective.py index b694fa74..b9d89fbc 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceEffective.py +++ b/appstoreserverlibrary/models/AdvancedCommerceEffective.py @@ -7,8 +7,15 @@ class AdvancedCommerceEffective(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ A string value that indicates when a requested change to an auto-renewable subscription goes into effect. - + https://developer.apple.com/documentation/advancedcommerceapi/effective """ IMMEDIATELY = "IMMEDIATELY" - NEXT_BILL_CYCLE = "NEXT_BILL_CYCLE" \ No newline at end of file + """ + The requested change goes into effect immediately. + """ + + NEXT_BILL_CYCLE = "NEXT_BILL_CYCLE" + """ + The requested change goes into effect at the start of the next billing cycle. + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceInfo.py b/appstoreserverlibrary/models/AdvancedCommerceInfo.py index e09d945f..e3c6f77d 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceInfo.py +++ b/appstoreserverlibrary/models/AdvancedCommerceInfo.py @@ -14,14 +14,14 @@ class AdvancedCommerceInfo: https://developer.apple.com/documentation/retentionmessaging/advancedcommerceinfo """ - messageIdentifier: Optional[UUID] = attr.ib(default=None) + messageIdentifier: Optional[UUID] = attr.ib(default=None, kw_only=True) """ The identifier of the message to display to the customer, along with the offer or switch-plan recommendation provided in advancedCommerceData. https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ - advancedCommerceData: Optional[str] = attr.ib(default=None) + advancedCommerceData: Optional[str] = attr.ib(default=None, kw_only=True) """ A Base64-encoded JSON object which contains a JWS describing an offer or switch-plan recommendation. diff --git a/appstoreserverlibrary/models/AdvancedCommerceOffer.py b/appstoreserverlibrary/models/AdvancedCommerceOffer.py index 0b18fba3..621f364c 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceOffer.py +++ b/appstoreserverlibrary/models/AdvancedCommerceOffer.py @@ -1,5 +1,4 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import Optional from attr import define import attr @@ -7,7 +6,7 @@ from .AdvancedCommerceOfferPeriod import AdvancedCommerceOfferPeriod from .AdvancedCommerceOfferReason import AdvancedCommerceOfferReason from .HelperValidationUtils import HelperValidationUtils -from .LibraryUtility import AttrsRawValueAware +from .LibraryUtility import AttrsRawValueAware, validate_not_none @define class AdvancedCommerceOffer(AttrsRawValueAware): @@ -17,34 +16,40 @@ class AdvancedCommerceOffer(AttrsRawValueAware): https://developer.apple.com/documentation/advancedcommerceapi/offer """ - periodCount: int = attr.ib(validator=HelperValidationUtils.period_count_validator) - """ - The number of periods the offer is active. + period: AdvancedCommerceOfferPeriod = AdvancedCommerceOfferPeriod.create_main_attr('rawPeriod', raw_required=True, kw_only=True) """ + The period of the offer. - price: int = attr.ib() + https://developer.apple.com/documentation/advancedcommerceapi/offer """ - The offer price, in milliunits. - https://developer.apple.com/documentation/advancedcommerceapi/price + rawPeriod: str = AdvancedCommerceOfferPeriod.create_raw_attr('period', required=True) + """ + See period """ - period: AdvancedCommerceOfferPeriod = AdvancedCommerceOfferPeriod.create_main_attr('rawPeriod', raw_required=True) + periodCount: int = attr.ib(kw_only=True, validator=HelperValidationUtils.period_count_validator) """ - The period of the offer. + The number of periods the offer is active. + + https://developer.apple.com/documentation/advancedcommerceapi/offer """ - rawPeriod: str = AdvancedCommerceOfferPeriod.create_raw_attr('period', required=True) + price: int = attr.ib(kw_only=True, validator=validate_not_none) """ - See period + The offer price, in milliunits. + + https://developer.apple.com/documentation/advancedcommerceapi/price """ - reason: AdvancedCommerceOfferReason = AdvancedCommerceOfferReason.create_main_attr('rawReason', raw_required=True) + reason: AdvancedCommerceOfferReason = AdvancedCommerceOfferReason.create_main_attr('rawReason', raw_required=True, kw_only=True) """ The reason for the offer. + + https://developer.apple.com/documentation/advancedcommerceapi/offer """ rawReason: str = AdvancedCommerceOfferReason.create_raw_attr('reason', required=True) """ See reason - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceOfferPeriod.py b/appstoreserverlibrary/models/AdvancedCommerceOfferPeriod.py index 0946b48f..0bedb697 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceOfferPeriod.py +++ b/appstoreserverlibrary/models/AdvancedCommerceOfferPeriod.py @@ -7,7 +7,7 @@ class AdvancedCommerceOfferPeriod(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The period of the offer. - + https://developer.apple.com/documentation/advancedcommerceapi/offer """ P3D = "P3D" @@ -18,4 +18,4 @@ class AdvancedCommerceOfferPeriod(str, Enum, metaclass=AppStoreServerLibraryEnum P3M = "P3M" P6M = "P6M" P9M = "P9M" - P1Y = "P1Y" \ No newline at end of file + P1Y = "P1Y" diff --git a/appstoreserverlibrary/models/AdvancedCommerceOfferReason.py b/appstoreserverlibrary/models/AdvancedCommerceOfferReason.py index 0d4286e3..386c60a0 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceOfferReason.py +++ b/appstoreserverlibrary/models/AdvancedCommerceOfferReason.py @@ -7,9 +7,9 @@ class AdvancedCommerceOfferReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The reason for the offer. - + https://developer.apple.com/documentation/advancedcommerceapi/offer """ ACQUISITION = "ACQUISITION" WIN_BACK = "WIN_BACK" - RETENTION = "RETENTION" \ No newline at end of file + RETENTION = "RETENTION" diff --git a/appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeCreateRequest.py b/appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeCreateRequest.py index 6c30527a..72d4e256 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeCreateRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeCreateRequest.py @@ -7,6 +7,8 @@ from .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest from .AdvancedCommerceOneTimeChargeItem import AdvancedCommerceOneTimeChargeItem +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo +from .LibraryUtility import validate_not_none @define class AdvancedCommerceOneTimeChargeCreateRequest(AbstractAdvancedCommerceInAppRequest): @@ -16,40 +18,51 @@ class AdvancedCommerceOneTimeChargeCreateRequest(AbstractAdvancedCommerceInAppRe https://developer.apple.com/documentation/advancedcommerceapi/onetimechargecreaterequest """ - currency: str = attr.ib() + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) """ - The currency of the price of the product. + The metadata of the request. - https://developer.apple.com/documentation/advancedcommerceapi/currency + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ - item: AdvancedCommerceOneTimeChargeItem = attr.ib() + operation: str = attr.ib(init=False, default="CREATE_ONE_TIME_CHARGE", on_setattr=attr.setters.frozen) """ - The details of the product for purchase. + The constant that represents the operation of this request. - https://developer.apple.com/documentation/advancedcommerceapi/onetimechargeitem + https://developer.apple.com/documentation/advancedcommerceapi/onetimechargecreaterequest """ - taxCode: str = attr.ib() + version: str = attr.ib(init=False, default="1", on_setattr=attr.setters.frozen) """ - The tax code for this product. + The version number of the API. - https://developer.apple.com/documentation/advancedcommerceapi/taxCode + https://developer.apple.com/documentation/advancedcommerceapi/version """ - operation: str = attr.ib(init=False, default="CREATE_ONE_TIME_CHARGE", on_setattr=attr.setters.frozen) + currency: str = attr.ib(kw_only=True, validator=validate_not_none) """ - The constant that represents the operation of this request. + The currency of the price of the product. + + https://developer.apple.com/documentation/advancedcommerceapi/currency """ - version: str = attr.ib(init=False, default="1", on_setattr=attr.setters.frozen) + item: AdvancedCommerceOneTimeChargeItem = attr.ib(kw_only=True, validator=validate_not_none) """ - The version number of the API. + The details of the product for purchase. + + https://developer.apple.com/documentation/advancedcommerceapi/onetimechargeitem """ - storefront: Optional[str] = attr.ib(default=None) + taxCode: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + The tax code for this product. + + https://developer.apple.com/documentation/advancedcommerceapi/taxcode + """ + + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ The storefront for the transaction. https://developer.apple.com/documentation/advancedcommerceapi/storefront - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeItem.py b/appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeItem.py index 514e8771..a811a930 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeItem.py @@ -4,18 +4,41 @@ import attr from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceOneTimeChargeItem(AbstractAdvancedCommerceItem): """ The details of a one-time charge product, including its display name, price, SKU, and metadata. - + https://developer.apple.com/documentation/advancedcommerceapi/onetimechargeitem """ - price: int = attr.ib() + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The product identifier. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ + + description: str = attr.ib(kw_only=True, validator=HelperValidationUtils.description_validator) + """ + A description of the product that doesn’t display to customers. + + https://developer.apple.com/documentation/advancedcommerceapi/description + """ + + displayName: str = attr.ib(kw_only=True, validator=HelperValidationUtils.display_name_validator) + """ + The product name, suitable for display to customers. + + https://developer.apple.com/documentation/advancedcommerceapi/displayname + """ + + price: int = attr.ib(kw_only=True, validator=validate_not_none) """ The price, in milliunits of the currency, of the one-time charge product. https://developer.apple.com/documentation/advancedcommerceapi/price - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommercePeriod.py b/appstoreserverlibrary/models/AdvancedCommercePeriod.py index 4b0a623e..eb06f759 100644 --- a/appstoreserverlibrary/models/AdvancedCommercePeriod.py +++ b/appstoreserverlibrary/models/AdvancedCommercePeriod.py @@ -7,13 +7,35 @@ class AdvancedCommercePeriod(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The duration of a single cycle of an auto-renewable subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/period """ - P1W = "P1W" + """ + A period that is 1 week in duration. + """ + P1M = "P1M" + """ + A period that is 1 month in duration. + """ + P2M = "P2M" + """ + A period that is 2 months in duration. + """ + P3M = "P3M" + """ + A period that is 3 months in duration. + """ + P6M = "P6M" - P1Y = "P1Y" \ No newline at end of file + """ + A period that is 6 months in duration. + """ + + P1Y = "P1Y" + """ + A period that is 1 year in duration. + """ diff --git a/appstoreserverlibrary/models/AdvancedCommercePriceIncreaseInfo.py b/appstoreserverlibrary/models/AdvancedCommercePriceIncreaseInfo.py index e301786e..e9a6dbec 100644 --- a/appstoreserverlibrary/models/AdvancedCommercePriceIncreaseInfo.py +++ b/appstoreserverlibrary/models/AdvancedCommercePriceIncreaseInfo.py @@ -1,6 +1,6 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import List, Optional +from typing import Optional, List from attr import define import attr @@ -14,17 +14,17 @@ class AdvancedCommercePriceIncreaseInfo(AttrsRawValueAware): https://developer.apple.com/documentation/appstoreserverapi/advancedcommercepriceincreaseinfo """ - dependentSKUs: Optional[List[str]] = attr.ib(default=None) + dependentSKUs: Optional[List[str]] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercepriceincreaseinfodependentsku """ - price: Optional[int] = attr.ib(default=None) + price: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercepriceincreaseinfoprice """ - status: Optional[AdvancedCommercePriceIncreaseInfoStatus] = AdvancedCommercePriceIncreaseInfoStatus.create_main_attr('rawStatus') + status: Optional[AdvancedCommercePriceIncreaseInfoStatus] = AdvancedCommercePriceIncreaseInfoStatus.create_main_attr('rawStatus', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercepriceincreaseinfostatus """ diff --git a/appstoreserverlibrary/models/AdvancedCommercePriceIncreaseInfoStatus.py b/appstoreserverlibrary/models/AdvancedCommercePriceIncreaseInfoStatus.py index dc55e831..d4ed307a 100644 --- a/appstoreserverlibrary/models/AdvancedCommercePriceIncreaseInfoStatus.py +++ b/appstoreserverlibrary/models/AdvancedCommercePriceIncreaseInfoStatus.py @@ -6,7 +6,7 @@ class AdvancedCommercePriceIncreaseInfoStatus(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - https://developer.apple.com/documentation/appstoreservernotifications/advancedcommercepriceincreaseinfostatus + https://developer.apple.com/documentation/appstoreserverapi/advancedcommercepriceincreaseinfostatus """ SCHEDULED = "SCHEDULED" PENDING = "PENDING" diff --git a/appstoreserverlibrary/models/AdvancedCommerceReason.py b/appstoreserverlibrary/models/AdvancedCommerceReason.py index cb41c2c1..5692a686 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceReason.py +++ b/appstoreserverlibrary/models/AdvancedCommerceReason.py @@ -6,10 +6,8 @@ class AdvancedCommerceReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - The data your app provides to change an item of an auto-renewable subscription. - https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem """ UPGRADE = "UPGRADE" DOWNGRADE = "DOWNGRADE" - APPLY_OFFER = "APPLY_OFFER" \ No newline at end of file + APPLY_OFFER = "APPLY_OFFER" diff --git a/appstoreserverlibrary/models/AdvancedCommerceRefund.py b/appstoreserverlibrary/models/AdvancedCommerceRefund.py index c30e7e8a..949266f7 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRefund.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRefund.py @@ -15,17 +15,17 @@ class AdvancedCommerceRefund(AttrsRawValueAware): https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerefund """ - refundAmount: Optional[int] = attr.ib(default=None) + refundAmount: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerefundamount """ - refundDate: Optional[int] = attr.ib(default=None) + refundDate: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerefunddate """ - refundReason: Optional[AdvancedCommerceRefundReason] = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason') + refundReason: Optional[AdvancedCommerceRefundReason] = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerefundreason """ @@ -35,7 +35,7 @@ class AdvancedCommerceRefund(AttrsRawValueAware): See refundReason """ - refundType: Optional[AdvancedCommerceRefundType] = AdvancedCommerceRefundType.create_main_attr('rawRefundType') + refundType: Optional[AdvancedCommerceRefundType] = AdvancedCommerceRefundType.create_main_attr('rawRefundType', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerefundtype """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceRefundReason.py b/appstoreserverlibrary/models/AdvancedCommerceRefundReason.py index 975db70d..8e6d7039 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRefundReason.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRefundReason.py @@ -7,14 +7,40 @@ class AdvancedCommerceRefundReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ A reason to request a refund. - + https://developer.apple.com/documentation/advancedcommerceapi/refundreason """ - UNINTENDED_PURCHASE = "UNINTENDED_PURCHASE" + """ + The customer didn’t intend to make the Apple In-App Purchase. + """ + FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE" + """ + The customer had issues with receiving or using the Apple In-App Purchase. + """ + UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE" + """ + The customer wasn’t satisfied with the Apple In-App Purchase. + """ + LEGAL = "LEGAL" + """ + The customer requested a refund based on a legal reason. + """ + OTHER = "OTHER" + """ + The customer requested a refund for other reasons. + """ + MODIFY_ITEMS_REFUND = "MODIFY_ITEMS_REFUND" - SIMULATE_REFUND_DECLINE = "SIMULATE_REFUND_DECLINE" \ No newline at end of file + """ + The customer modified one or more items in the subscription, which results in a refund. + """ + + SIMULATE_REFUND_DECLINE = "SIMULATE_REFUND_DECLINE" + """ + For testing purposes only, a simulation of a refund that the App Store declines. + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceRefundType.py b/appstoreserverlibrary/models/AdvancedCommerceRefundType.py index 445a72d9..63bfdac5 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRefundType.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRefundType.py @@ -6,10 +6,10 @@ class AdvancedCommerceRefundType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - Information about the refund request for an item, such as its SKU, the refund amount, reason, and type. + The type of refund requested. https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem """ FULL = "FULL" PRORATED = "PRORATED" - CUSTOM = "CUSTOM" \ No newline at end of file + CUSTOM = "CUSTOM" diff --git a/appstoreserverlibrary/models/AdvancedCommerceRenewalInfo.py b/appstoreserverlibrary/models/AdvancedCommerceRenewalInfo.py index 591d96b9..1f97fb18 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRenewalInfo.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRenewalInfo.py @@ -1,6 +1,6 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import List, Optional +from typing import Optional, List from attr import define import attr @@ -16,22 +16,22 @@ class AdvancedCommerceRenewalInfo(AttrsRawValueAware): https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerenewalinfo """ - consistencyToken: Optional[str] = attr.ib(default=None) + consistencyToken: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommerceconsistencytoken """ - descriptors: Optional[AdvancedCommerceDescriptors] = attr.ib(default=None) + descriptors: Optional[AdvancedCommerceDescriptors] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercedescriptors """ - items: Optional[List[AdvancedCommerceRenewalItem]] = attr.ib(default=None) + items: Optional[List[AdvancedCommerceRenewalItem]] = attr.ib(default=None, kw_only=True) """ - https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerenewalitem + https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerenewalitems """ - period: Optional[AdvancedCommercePeriod] = AdvancedCommercePeriod.create_main_attr('rawPeriod') + period: Optional[AdvancedCommercePeriod] = AdvancedCommercePeriod.create_main_attr('rawPeriod', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommerceperiod """ @@ -41,12 +41,12 @@ class AdvancedCommerceRenewalInfo(AttrsRawValueAware): See period """ - requestReferenceId: Optional[str] = attr.ib(default=None) + requestReferenceId: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerequestreferenceid """ - taxCode: Optional[str] = attr.ib(default=None) + taxCode: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetaxcode """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceRenewalItem.py b/appstoreserverlibrary/models/AdvancedCommerceRenewalItem.py index be435f8d..650c8f02 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRenewalItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRenewalItem.py @@ -14,32 +14,32 @@ class AdvancedCommerceRenewalItem: https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerenewalitem """ - SKU: Optional[str] = attr.ib(default=None) + SKU: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercesku """ - description: Optional[str] = attr.ib(default=None) + description: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercedescription """ - displayName: Optional[str] = attr.ib(default=None) + displayName: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercedisplayname """ - offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None) + offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommerceoffer """ - price: Optional[int] = attr.ib(default=None) + price: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommerceprice """ - priceIncreaseInfo: Optional[AdvancedCommercePriceIncreaseInfo] = attr.ib(default=None) + priceIncreaseInfo: Optional[AdvancedCommercePriceIncreaseInfo] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercepriceincreaseinfo """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceRequestInfo.py b/appstoreserverlibrary/models/AdvancedCommerceRequestInfo.py index 98386e18..8d70861a 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRequestInfo.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRequestInfo.py @@ -6,27 +6,33 @@ from attr import define import attr +from .LibraryUtility import validate_not_none + @define class AdvancedCommerceRequestInfo: """ The metadata to include in server requests. - + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ - requestReferenceId: UUID = attr.ib() + requestReferenceId: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ - A UUID that you provide to uniquely identify each request. If the request times out, you can use the same requestReferenceId value to retry the request. Otherwise, provide a unique value. + A UUID that you provide to uniquely identify each request. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ - appAccountToken: Optional[UUID] = attr.ib(default=None) + appAccountToken: Optional[UUID] = attr.ib(default=None, kw_only=True) """ A UUID that represents an app account token, to associate with the transaction in the request. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo + """ + + consistencyToken: Optional[str] = attr.ib(default=None, kw_only=True) """ + The value of the advancedCommerceConsistencyToken that you receive in the JWSRenewalInfo renewal information for a subscription. - consistencyToken: Optional[str] = attr.ib(default=None) + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ - The value of the advancedCommerceConsistencyToken that you receive in the JWSRenewalInfo renewal information for a subscription. Don’t generate this value. - - https://developer.apple.com/documentation/AppStoreServerAPI/advancedCommerceConsistencyToken - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/AdvancedCommerceRequestRefundItem.py b/appstoreserverlibrary/models/AdvancedCommerceRequestRefundItem.py index 07a62205..19879172 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRequestRefundItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRequestRefundItem.py @@ -8,6 +8,8 @@ from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem from .AdvancedCommerceRefundReason import AdvancedCommerceRefundReason from .AdvancedCommerceRefundType import AdvancedCommerceRefundType +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceRequestRefundItem(AbstractAdvancedCommerceBaseItem): @@ -17,9 +19,14 @@ class AdvancedCommerceRequestRefundItem(AbstractAdvancedCommerceBaseItem): https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem """ - revoke: bool = attr.ib() + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The product identifier. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ - refundReason: AdvancedCommerceRefundReason = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', raw_required=True) + refundReason: AdvancedCommerceRefundReason = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', raw_required=True, kw_only=True) """ The reason for the refund request. @@ -31,9 +38,11 @@ class AdvancedCommerceRequestRefundItem(AbstractAdvancedCommerceBaseItem): See refundReason """ - refundType: AdvancedCommerceRefundType = AdvancedCommerceRefundType.create_main_attr('rawRefundType', raw_required=True) + refundType: AdvancedCommerceRefundType = AdvancedCommerceRefundType.create_main_attr('rawRefundType', raw_required=True, kw_only=True) """ The type of refund requested. + + https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem """ rawRefundType: str = AdvancedCommerceRefundType.create_raw_attr('refundType', required=True) @@ -41,9 +50,14 @@ class AdvancedCommerceRequestRefundItem(AbstractAdvancedCommerceBaseItem): See refundType """ - refundAmount: Optional[int] = attr.ib(default=None) + revoke: bool = attr.ib(kw_only=True, validator=validate_not_none) + """ + https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem """ - The refund amount you're requesting for the SKU, in milliunits of the currency. + + refundAmount: Optional[int] = attr.ib(default=None, kw_only=True) + """ + The refund amount you’re requesting for the SKU, in milliunits of the currency. https://developer.apple.com/documentation/advancedcommerceapi/refundamount - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceRequestRefundRequest.py b/appstoreserverlibrary/models/AdvancedCommerceRequestRefundRequest.py index d2fb55cc..99556d80 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRequestRefundRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRequestRefundRequest.py @@ -6,35 +6,50 @@ import attr from .AdvancedCommerceRequest import AdvancedCommerceRequest +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo from .AdvancedCommerceRequestRefundItem import AdvancedCommerceRequestRefundItem from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceRequestRefundRequest(AdvancedCommerceRequest): """ The request body for requesting a refund for a transaction. - + https://developer.apple.com/documentation/advancedcommerceapi/requestrefundrequest """ - items: List[AdvancedCommerceRequestRefundItem] = attr.ib(validator=HelperValidationUtils.items_validator) + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) + """ + The metadata to include in server requests. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo + """ + + items: List[AdvancedCommerceRequestRefundItem] = attr.ib(kw_only=True, validator=HelperValidationUtils.items_validator) """ + Information about the refund request for an item, such as its SKU, the refund amount, reason, and type. + https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem """ - refundRiskingPreference: bool = attr.ib() + refundRiskingPreference: bool = attr.ib(kw_only=True, validator=validate_not_none) """ + A Boolean value that indicates whether the App Store asks you for consumption data to help inform the refund decision. + https://developer.apple.com/documentation/advancedcommerceapi/refundriskingpreference """ - currency: Optional[str] = attr.ib(default=None) + currency: Optional[str] = attr.ib(default=None, kw_only=True) """ The currency of the transaction. - + https://developer.apple.com/documentation/advancedcommerceapi/currency """ - storefront: Optional[str] = attr.ib(default=None) + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ + A three-letter code that represents the country or region associated with the App Store storefront. + https://developer.apple.com/documentation/advancedcommerceapi/storefront - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceRequestRefundResponse.py b/appstoreserverlibrary/models/AdvancedCommerceRequestRefundResponse.py index 03e9a1ff..c881233b 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceRequestRefundResponse.py +++ b/appstoreserverlibrary/models/AdvancedCommerceRequestRefundResponse.py @@ -1,7 +1,14 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. +from typing import Optional + +from attr import define +import attr + from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +from .LibraryUtility import validate_not_none +@define class AdvancedCommerceRequestRefundResponse(AbstractAdvancedCommerceResponse): """ The response body for a transaction refund request. @@ -9,5 +16,19 @@ class AdvancedCommerceRequestRefundResponse(AbstractAdvancedCommerceResponse): https://developer.apple.com/documentation/advancedcommerceapi/requestrefundresponse """ - def __init__(self, signedTransactionInfo: str): - super().__init__(signedRenewalInfo=None, signedTransactionInfo=signedTransactionInfo) \ No newline at end of file + signedRenewalInfo: Optional[str] = attr.ib(default=None, kw_only=True) + """ + Subscription renewal information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwsrenewalinfo + """ + + signedTransactionInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Transaction information signed by the App Store, in JWS Compact Serialization format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwstransaction + """ + + def __init__(self, *, signedTransactionInfo: str, signedRenewalInfo: Optional[str] = None): + super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelRequest.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelRequest.py index db6e8317..5de80ffa 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelRequest.py @@ -6,16 +6,27 @@ import attr from .AdvancedCommerceRequest import AdvancedCommerceRequest +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionCancelRequest(AdvancedCommerceRequest): """ The request body for turning off automatic renewal of a subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncancelrequest """ - storefront: Optional[str] = attr.ib(default=None) + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) + """ + The metadata to include in server requests. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ + + storefront: Optional[str] = attr.ib(default=None, kw_only=True) + """ + A three-letter code that represents the country or region associated with the App Store storefront. + https://developer.apple.com/documentation/advancedcommerceapi/storefront - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelResponse.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelResponse.py index b0f7a246..37afe9cc 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelResponse.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelResponse.py @@ -1,14 +1,32 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define +import attr from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionCancelResponse(AbstractAdvancedCommerceResponse): """ The response body for a successful subscription cancellation. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncancelresponse """ - pass \ No newline at end of file + + signedRenewalInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Subscription renewal information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwsrenewalinfo + """ + + signedTransactionInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Transaction information signed by the App Store, in JWS Compact Serialization format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwstransaction + """ + + def __init__(self, *, signedRenewalInfo: str, signedTransactionInfo: str): + super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataDescriptors.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataDescriptors.py index 99e0c782..905ac251 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataDescriptors.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataDescriptors.py @@ -1,4 +1,5 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -6,19 +7,20 @@ from .AdvancedCommerceEffective import AdvancedCommerceEffective from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import AttrsRawValueAware @define -class AdvancedCommerceSubscriptionChangeMetadataDescriptors(): +class AdvancedCommerceSubscriptionChangeMetadataDescriptors(AttrsRawValueAware): """ The subscription metadata to change, specifically the description and display name. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadatadescriptors """ - effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True) + effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True, kw_only=True) """ The string that determines when the metadata change goes into effect. - + https://developer.apple.com/documentation/advancedcommerceapi/effective """ @@ -27,22 +29,16 @@ class AdvancedCommerceSubscriptionChangeMetadataDescriptors(): See effective """ - description: Optional[str] = attr.ib( - default=None, - validator=attr.validators.optional(HelperValidationUtils.description_validator) - ) + description: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.description_validator)) """ The new description for the subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/description """ - displayName: Optional[str] = attr.ib( - default=None, - validator=attr.validators.optional(HelperValidationUtils.display_name_validator) - ) + displayName: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.display_name_validator)) """ The new display name for the subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/displayname - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataItem.py index 5d7e83d6..c10b99ac 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataItem.py @@ -1,4 +1,5 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -6,21 +7,24 @@ from .AdvancedCommerceEffective import AdvancedCommerceEffective from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import AttrsRawValueAware @define -class AdvancedCommerceSubscriptionChangeMetadataItem(): +class AdvancedCommerceSubscriptionChangeMetadataItem(AttrsRawValueAware): """ The metadata to change for an item, specifically its SKU, description, and display name. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadataitem """ - currentSKU: str = attr.ib(validator=HelperValidationUtils.sku_validator) + currentSKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) """ The original SKU of the item. + + https://developer.apple.com/documentation/advancedcommerceapi/sku """ - effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True) + effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True, kw_only=True) """ The string that determines when the metadata change goes into effect. @@ -32,23 +36,23 @@ class AdvancedCommerceSubscriptionChangeMetadataItem(): See effective """ - description: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.description_validator)) + SKU: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.sku_validator)) + """ + The new SKU of the item. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ + + description: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.description_validator)) """ The new description for the item. https://developer.apple.com/documentation/advancedcommerceapi/description """ - displayName: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.display_name_validator)) + displayName: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.display_name_validator)) """ The new display name for the item. https://developer.apple.com/documentation/advancedcommerceapi/displayname - """ - - SKU: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.sku_validator)) """ - The new SKU of the item. - - https://developer.apple.com/documentation/advancedcommerceapi/sku - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataRequest.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataRequest.py index 69fb57fc..481c2855 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataRequest.py @@ -6,34 +6,51 @@ import attr from .AdvancedCommerceRequest import AdvancedCommerceRequest +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo from .AdvancedCommerceSubscriptionChangeMetadataDescriptors import AdvancedCommerceSubscriptionChangeMetadataDescriptors from .AdvancedCommerceSubscriptionChangeMetadataItem import AdvancedCommerceSubscriptionChangeMetadataItem from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionChangeMetadataRequest(AdvancedCommerceRequest): """ The request body you provide to change the metadata of a subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadatarequest """ - descriptors: Optional[AdvancedCommerceSubscriptionChangeMetadataDescriptors] = attr.ib(default=None) + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) + """ + The metadata to include in server requests. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo + """ + + descriptors: Optional[AdvancedCommerceSubscriptionChangeMetadataDescriptors] = attr.ib(default=None, kw_only=True) """ + The subscription metadata to change, specifically the description and display name. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadatadescriptors """ - items: Optional[List[AdvancedCommerceSubscriptionChangeMetadataItem]] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.items_validator)) + items: Optional[List[AdvancedCommerceSubscriptionChangeMetadataItem]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.items_validator)) """ + The metadata to change for an item, specifically its SKU, description, and display name. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadataitem """ - storefront: Optional[str] = attr.ib(default=None) + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ + A three-letter code that represents the country or region associated with the App Store storefront. + https://developer.apple.com/documentation/advancedcommerceapi/storefront """ - taxCode: Optional[str] = attr.ib(default=None) + taxCode: Optional[str] = attr.ib(default=None, kw_only=True) """ + A tax code that applies to a SKU. + https://developer.apple.com/documentation/advancedcommerceapi/taxcode - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataResponse.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataResponse.py index b7c157d6..330bdb80 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataResponse.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataResponse.py @@ -1,7 +1,12 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. +from attr import define +import attr + from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +from .LibraryUtility import validate_not_none +@define class AdvancedCommerceSubscriptionChangeMetadataResponse(AbstractAdvancedCommerceResponse): """ The response body for a successful subscription metadata change. @@ -9,5 +14,19 @@ class AdvancedCommerceSubscriptionChangeMetadataResponse(AbstractAdvancedCommerc https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadataresponse """ - def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str): - super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) \ No newline at end of file + signedRenewalInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Subscription renewal information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwsrenewalinfo + """ + + signedTransactionInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Transaction information signed by the App Store, in JWS Compact Serialization format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwstransaction + """ + + def __init__(self, *, signedRenewalInfo: str, signedTransactionInfo: str): + super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateItem.py index aab39cd5..fc472a4a 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateItem.py @@ -1,11 +1,14 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional -import attr + from attr import define +import attr + from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem from .AdvancedCommerceOffer import AdvancedCommerceOffer - +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionCreateItem(AbstractAdvancedCommerceItem): @@ -15,12 +18,37 @@ class AdvancedCommerceSubscriptionCreateItem(AbstractAdvancedCommerceItem): https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncreateitem """ - price: int = attr.ib() + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The item’s product identifier, which you define. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ + + description: str = attr.ib(kw_only=True, validator=HelperValidationUtils.description_validator) + """ + A string you provide that describes a SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/description + """ + + displayName: str = attr.ib(kw_only=True, validator=HelperValidationUtils.display_name_validator) + """ + A string with a product name that you can localize and is suitable for display to customers. + + https://developer.apple.com/documentation/advancedcommerceapi/displayname """ + + price: int = attr.ib(kw_only=True, validator=validate_not_none) + """ + A price, in milliunits of a currency, for an Advanced Commerce API SKU. + https://developer.apple.com/documentation/advancedcommerceapi/price """ - offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None) + offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None, kw_only=True) """ + A discount offer for an auto-renewable subscription. + https://developer.apple.com/documentation/advancedcommerceapi/offer - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateRequest.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateRequest.py index 85bb8d6a..f487d410 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateRequest.py @@ -1,48 +1,70 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List -import attr + from attr import define +import attr + from .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest from .AdvancedCommerceDescriptors import AdvancedCommerceDescriptors -from .AdvancedCommerceSubscriptionCreateItem import AdvancedCommerceSubscriptionCreateItem from .AdvancedCommercePeriod import AdvancedCommercePeriod - +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo +from .AdvancedCommerceSubscriptionCreateItem import AdvancedCommerceSubscriptionCreateItem +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionCreateRequest(AbstractAdvancedCommerceInAppRequest): """ The request data your app provides when a customer purchases an auto-renewable subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncreaterequest """ + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) + """ + The metadata to include in server requests. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo + """ + operation: str = attr.ib(init=False, default="CREATE_SUBSCRIPTION", on_setattr=attr.setters.frozen) + """ + https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncreaterequest + """ version: str = attr.ib(init=False, default="1", on_setattr=attr.setters.frozen) + """ + A number that represents the version of the data structure or Advanced Commerce API. + + https://developer.apple.com/documentation/advancedcommerceapi/version + """ - currency: str = attr.ib() + currency: str = attr.ib(kw_only=True, validator=validate_not_none) """ + The three-letter ISO 4217 currency code for the price of a product. + https://developer.apple.com/documentation/advancedcommerceapi/currency """ - descriptors: AdvancedCommerceDescriptors = attr.ib() + descriptors: AdvancedCommerceDescriptors = attr.ib(kw_only=True, validator=validate_not_none) """ + The display name and description of a subscription product. + https://developer.apple.com/documentation/advancedcommerceapi/descriptors """ - items: List[AdvancedCommerceSubscriptionCreateItem] = attr.ib() + items: List[AdvancedCommerceSubscriptionCreateItem] = attr.ib(kw_only=True, validator=HelperValidationUtils.items_validator) """ + The data that describes a subscription item. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncreateitem """ - taxCode: str = attr.ib() - """ - https://developer.apple.com/documentation/advancedcommerceapi/taxCode + period: AdvancedCommercePeriod = AdvancedCommercePeriod.create_main_attr('rawPeriod', raw_required=True, kw_only=True) """ + The duration of a single cycle of an auto-renewable subscription. - period: AdvancedCommercePeriod = AdvancedCommercePeriod.create_main_attr('rawPeriod', raw_required=True) - """ https://developer.apple.com/documentation/advancedcommerceapi/period """ @@ -51,12 +73,23 @@ class AdvancedCommerceSubscriptionCreateRequest(AbstractAdvancedCommerceInAppReq See period """ - previousTransactionId: Optional[str] = attr.ib(default=None) + taxCode: str = attr.ib(kw_only=True, validator=validate_not_none) """ + A tax code that applies to a SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/taxcode + """ + + previousTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) + """ + A unique identifier that the App Store generates for a transaction. + https://developer.apple.com/documentation/advancedcommerceapi/transactionid """ - storefront: Optional[str] = attr.ib(default=None) + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ + A three-letter code that represents the country or region associated with the App Store storefront. + https://developer.apple.com/documentation/advancedcommerceapi/storefront - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateDescriptors.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateDescriptors.py index 1402cc27..25c31609 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateDescriptors.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateDescriptors.py @@ -1,8 +1,10 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define -from .AdvancedCommerceDescriptors import AdvancedCommerceDescriptors +import attr +from .AdvancedCommerceDescriptors import AdvancedCommerceDescriptors +from .HelperValidationUtils import HelperValidationUtils @define class AdvancedCommerceSubscriptionMigrateDescriptors(AdvancedCommerceDescriptors): @@ -10,4 +12,18 @@ class AdvancedCommerceSubscriptionMigrateDescriptors(AdvancedCommerceDescriptors The description and display name of the subscription to migrate to that you manage. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigratedescriptors - """ \ No newline at end of file + """ + + description: str = attr.ib(kw_only=True, validator=HelperValidationUtils.description_validator) + """ + The description of the subscription to migrate to. + + https://developer.apple.com/documentation/advancedcommerceapi/description + """ + + displayName: str = attr.ib(kw_only=True, validator=HelperValidationUtils.display_name_validator) + """ + The display name of the subscription to migrate to. + + https://developer.apple.com/documentation/advancedcommerceapi/displayname + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateItem.py index 49838a41..5dcbb425 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateItem.py @@ -1,13 +1,36 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define -from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem +import attr +from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem +from .HelperValidationUtils import HelperValidationUtils @define class AdvancedCommerceSubscriptionMigrateItem(AbstractAdvancedCommerceItem): """ The SKU, description, and display name to use for a migrated subscription item. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigrateitem - """ \ No newline at end of file + """ + + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The SKU to use for the migrated item. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ + + description: str = attr.ib(kw_only=True, validator=HelperValidationUtils.description_validator) + """ + The description of the SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/description + """ + + displayName: str = attr.ib(kw_only=True, validator=HelperValidationUtils.display_name_validator) + """ + The display name of the SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/displayname + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRenewalItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRenewalItem.py index 0c4af397..aabcd5aa 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRenewalItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRenewalItem.py @@ -1,8 +1,10 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define -from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem +import attr +from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem +from .HelperValidationUtils import HelperValidationUtils @define class AdvancedCommerceSubscriptionMigrateRenewalItem(AbstractAdvancedCommerceItem): @@ -10,4 +12,25 @@ class AdvancedCommerceSubscriptionMigrateRenewalItem(AbstractAdvancedCommerceIte The item information that replaces a migrated subscription item when the subscription renews. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigraterenewalitem - """ \ No newline at end of file + """ + + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The SKU that the subscription item renews to at the next renewal period. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ + + description: str = attr.ib(kw_only=True, validator=HelperValidationUtils.description_validator) + """ + The description of the renewing SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/description + """ + + displayName: str = attr.ib(kw_only=True, validator=HelperValidationUtils.display_name_validator) + """ + The display name of the renewing SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/displayname + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRequest.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRequest.py index da54738c..2d0cf325 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRequest.py @@ -1,54 +1,71 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List -import attr + from attr import define +import attr + from .AdvancedCommerceRequest import AdvancedCommerceRequest -from .HelperValidationUtils import HelperValidationUtils +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo from .AdvancedCommerceSubscriptionMigrateDescriptors import AdvancedCommerceSubscriptionMigrateDescriptors from .AdvancedCommerceSubscriptionMigrateItem import AdvancedCommerceSubscriptionMigrateItem from .AdvancedCommerceSubscriptionMigrateRenewalItem import AdvancedCommerceSubscriptionMigrateRenewalItem +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionMigrateRequest(AdvancedCommerceRequest): """ - The subscription details you provide to migrate a subscription from In-App Purchase to the Advanced Commerce API, such as descriptors, items, storefront, and more. - + The subscription details you provide to migrate a subscription from Apple In-App Purchase to the Advanced Commerce API, such as descriptors, items, storefront, and more. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigraterequest """ - descriptors: AdvancedCommerceSubscriptionMigrateDescriptors = attr.ib() + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) + """ + The metadata to include in server requests. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ + + descriptors: AdvancedCommerceSubscriptionMigrateDescriptors = attr.ib(kw_only=True, validator=validate_not_none) + """ + The description and display name of the subscription to migrate to that you manage. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigratedescriptors """ - items: List[AdvancedCommerceSubscriptionMigrateItem] = attr.ib(validator=HelperValidationUtils.items_validator) + items: List[AdvancedCommerceSubscriptionMigrateItem] = attr.ib(kw_only=True, validator=HelperValidationUtils.items_validator) """ An array of one or more SKUs, along with descriptions and display names, that are included in the subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigrateitem """ - targetProductId: str = attr.ib() + targetProductId: str = attr.ib(kw_only=True, validator=validate_not_none) """ Your generic product ID for an auto-renewable subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/targetproductid """ - taxCode: str = attr.ib() + taxCode: str = attr.ib(kw_only=True, validator=validate_not_none) """ + A tax code that applies to a SKU. + https://developer.apple.com/documentation/advancedcommerceapi/taxcode """ - renewalItems: Optional[List[AdvancedCommerceSubscriptionMigrateRenewalItem]] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.items_validator)) + renewalItems: Optional[List[AdvancedCommerceSubscriptionMigrateRenewalItem]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.items_validator)) """ An optional array of subscription items that represents the items that renew at the next renewal period, if they differ from items. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigraterenewalitem """ - storefront: Optional[str] = attr.ib(default=None) + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ + A three-letter code that represents the country or region associated with the App Store storefront. + https://developer.apple.com/documentation/advancedcommerceapi/storefront - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateResponse.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateResponse.py index 152b0d22..40519a38 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateResponse.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateResponse.py @@ -1,15 +1,32 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define -from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +import attr +from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionMigrateResponse(AbstractAdvancedCommerceResponse): """ A response that contains signed renewal and transaction information after a subscription successfully migrates to the Advanced Commerce API. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigrateresponse """ - def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str): - super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) \ No newline at end of file + + signedRenewalInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Subscription renewal information signed by the App Store, in JSON Web Signature (JWS) format, for the migrated subscription. + + https://developer.apple.com/documentation/advancedcommerceapi/jwsrenewalinfo + """ + + signedTransactionInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Transaction information signed by the App Store, in JSON Web Signature (JWS) Compact Serialization format, for the migrated subscription. + + https://developer.apple.com/documentation/advancedcommerceapi/jwstransaction + """ + + def __init__(self, *, signedRenewalInfo: str, signedTransactionInfo: str): + super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyAddItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyAddItem.py index a1470659..92903f4b 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyAddItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyAddItem.py @@ -1,33 +1,61 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional -import attr + from attr import define +import attr + from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem from .AdvancedCommerceOffer import AdvancedCommerceOffer - +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionModifyAddItem(AbstractAdvancedCommerceItem): """ The data your app provides to add items when it makes changes to an auto-renewable subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyadditem """ - price: int = attr.ib() + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) """ + The product identifier of an Apple In-App Purchase product you manage in your own system. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ + + description: str = attr.ib(kw_only=True, validator=HelperValidationUtils.description_validator) + """ + A string you provide that describes a SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/description + """ + + displayName: str = attr.ib(kw_only=True, validator=HelperValidationUtils.display_name_validator) + """ + A string with a product name that you can localize and is suitable for display to customers. + + https://developer.apple.com/documentation/advancedcommerceapi/displayname + """ + + price: int = attr.ib(kw_only=True, validator=validate_not_none) + """ + A price, in milliunits of a currency, for an Advanced Commerce API SKU. + https://developer.apple.com/documentation/advancedcommerceapi/price """ - offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None) + offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None, kw_only=True) """ A discount offer for an auto-renewable subscription. https://developer.apple.com/documentation/advancedcommerceapi/offer """ - proratedPrice: Optional[int] = attr.ib(default=None) + proratedPrice: Optional[int] = attr.ib(default=None, kw_only=True) """ + A prorated price, in milliunits of a currency, for an Advanced Commerce API SKU. + https://developer.apple.com/documentation/advancedcommerceapi/proratedprice - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyChangeItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyChangeItem.py index 532ce30b..a3572c6c 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyChangeItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyChangeItem.py @@ -1,14 +1,16 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional -import attr + from attr import define +import attr + from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem from .AdvancedCommerceEffective import AdvancedCommerceEffective from .AdvancedCommerceOffer import AdvancedCommerceOffer from .AdvancedCommerceReason import AdvancedCommerceReason from .HelperValidationUtils import HelperValidationUtils - +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionModifyChangeItem(AbstractAdvancedCommerceItem): @@ -18,25 +20,38 @@ class AdvancedCommerceSubscriptionModifyChangeItem(AbstractAdvancedCommerceItem) https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem """ - currentSKU: str = attr.ib(validator=HelperValidationUtils.sku_validator) + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) """ + The product identifier of an Apple In-App Purchase product you manage in your own system. + https://developer.apple.com/documentation/advancedcommerceapi/sku """ - price: int = attr.ib() + description: str = attr.ib(kw_only=True, validator=HelperValidationUtils.description_validator) """ - https://developer.apple.com/documentation/advancedcommerceapi/price + A string you provide that describes a SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/description """ - reason: AdvancedCommerceReason = AdvancedCommerceReason.create_main_attr('rawReason', raw_required=True) + displayName: str = attr.ib(kw_only=True, validator=HelperValidationUtils.display_name_validator) + """ + A string with a product name that you can localize and is suitable for display to customers. - rawReason: str = AdvancedCommerceReason.create_raw_attr('reason', required=True) + https://developer.apple.com/documentation/advancedcommerceapi/displayname """ - See reason + + currentSKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The product identifier of an Apple In-App Purchase product you manage in your own system. + + https://developer.apple.com/documentation/advancedcommerceapi/sku """ - effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True) + effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True, kw_only=True) """ + A string value that indicates when a requested change to an auto-renewable subscription goes into effect. + https://developer.apple.com/documentation/advancedcommerceapi/effective """ @@ -45,12 +60,33 @@ class AdvancedCommerceSubscriptionModifyChangeItem(AbstractAdvancedCommerceItem) See effective """ - offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None) + price: int = attr.ib(kw_only=True, validator=validate_not_none) """ + A price, in milliunits of a currency, for an Advanced Commerce API SKU. + + https://developer.apple.com/documentation/advancedcommerceapi/price + """ + + reason: AdvancedCommerceReason = AdvancedCommerceReason.create_main_attr('rawReason', raw_required=True, kw_only=True) + """ + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem + """ + + rawReason: str = AdvancedCommerceReason.create_raw_attr('reason', required=True) + """ + See reason + """ + + offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None, kw_only=True) + """ + A discount offer for an auto-renewable subscription. + https://developer.apple.com/documentation/advancedcommerceapi/offer """ - proratedPrice: Optional[int] = attr.ib(default=None) + proratedPrice: Optional[int] = attr.ib(default=None, kw_only=True) """ + A prorated price, in milliunits of a currency, for an Advanced Commerce API SKU. + https://developer.apple.com/documentation/advancedcommerceapi/proratedprice - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyDescriptors.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyDescriptors.py index b46edde0..5550ef41 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyDescriptors.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyDescriptors.py @@ -1,21 +1,26 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. + from typing import Optional -import attr from attr import define +import attr + from .AdvancedCommerceEffective import AdvancedCommerceEffective from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import AttrsRawValueAware @define -class AdvancedCommerceSubscriptionModifyDescriptors(): +class AdvancedCommerceSubscriptionModifyDescriptors(AttrsRawValueAware): """ The data your app provides to change the description and display name of an auto-renewable subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifydescriptors """ - effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True) + effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True, kw_only=True) """ + A string value that indicates when a requested change to an auto-renewable subscription goes into effect. + https://developer.apple.com/documentation/advancedcommerceapi/effective """ @@ -24,13 +29,16 @@ class AdvancedCommerceSubscriptionModifyDescriptors(): See effective """ - description: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.description_validator)) + description: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.description_validator)) """ + A string you provide that describes a SKU. + https://developer.apple.com/documentation/advancedcommerceapi/description """ - displayName: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.display_name_validator) - ) + displayName: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.display_name_validator)) """ + A string with a product name that you can localize and is suitable for display to customers. + https://developer.apple.com/documentation/advancedcommerceapi/displayname - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyInAppRequest.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyInAppRequest.py index e1ac2611..0a4c4ec6 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyInAppRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyInAppRequest.py @@ -1,16 +1,19 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List -import attr + from attr import define +import attr + from .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest -from .HelperValidationUtils import HelperValidationUtils +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo from .AdvancedCommerceSubscriptionModifyAddItem import AdvancedCommerceSubscriptionModifyAddItem from .AdvancedCommerceSubscriptionModifyChangeItem import AdvancedCommerceSubscriptionModifyChangeItem from .AdvancedCommerceSubscriptionModifyDescriptors import AdvancedCommerceSubscriptionModifyDescriptors from .AdvancedCommerceSubscriptionModifyPeriodChange import AdvancedCommerceSubscriptionModifyPeriodChange from .AdvancedCommerceSubscriptionModifyRemoveItem import AdvancedCommerceSubscriptionModifyRemoveItem - +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionModifyInAppRequest(AbstractAdvancedCommerceInAppRequest): @@ -20,56 +23,91 @@ class AdvancedCommerceSubscriptionModifyInAppRequest(AbstractAdvancedCommerceInA https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyinapprequest """ - operation: str = attr.ib(default="MODIFY_SUBSCRIPTION", init=False, on_setattr=attr.setters.frozen) + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) + """ + The metadata to include in server requests. - version: str = attr.ib(default="1", init=False, on_setattr=attr.setters.frozen) + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo + """ - transactionId: str = attr.ib() + operation: str = attr.ib(init=False, default="MODIFY_SUBSCRIPTION", on_setattr=attr.setters.frozen) """ - https://developer.apple.com/documentation/advancedcommerceapi/transactionid + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyinapprequest + """ + + version: str = attr.ib(init=False, default="1", on_setattr=attr.setters.frozen) + """ + A number that represents the version of the data structure or Advanced Commerce API. + + https://developer.apple.com/documentation/advancedcommerceapi/version """ - retainBillingCycle: bool = attr.ib() + retainBillingCycle: bool = attr.ib(kw_only=True, validator=validate_not_none) """ + A Boolean value that determines whether to keep the existing billing cycle with the change you request. + https://developer.apple.com/documentation/advancedcommerceapi/retainbillingcycle """ - addItems: Optional[List[AdvancedCommerceSubscriptionModifyAddItem]] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.items_validator)) + transactionId: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + A unique identifier that the App Store generates for a transaction. + + https://developer.apple.com/documentation/advancedcommerceapi/transactionid + """ + + addItems: Optional[List[AdvancedCommerceSubscriptionModifyAddItem]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.items_validator)) """ + The data your app provides to add items when it makes changes to an auto-renewable subscription. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyadditem """ - changeItems: Optional[List[AdvancedCommerceSubscriptionModifyChangeItem]] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.items_validator)) + changeItems: Optional[List[AdvancedCommerceSubscriptionModifyChangeItem]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.items_validator)) """ + The data your app provides to change an item of an auto-renewable subscription. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem """ - currency: Optional[str] = attr.ib(default=None) + currency: Optional[str] = attr.ib(default=None, kw_only=True) """ + The three-letter ISO 4217 currency code for the price of a product. + https://developer.apple.com/documentation/advancedcommerceapi/currency """ - descriptors: Optional[AdvancedCommerceSubscriptionModifyDescriptors] = attr.ib(default=None) + descriptors: Optional[AdvancedCommerceSubscriptionModifyDescriptors] = attr.ib(default=None, kw_only=True) """ + The data your app provides to change the description and display name of an auto-renewable subscription. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifydescriptors """ - periodChange: Optional[AdvancedCommerceSubscriptionModifyPeriodChange] = attr.ib(default=None) + periodChange: Optional[AdvancedCommerceSubscriptionModifyPeriodChange] = attr.ib(default=None, kw_only=True) """ + The data your app provides to change the period of an auto-renewable subscription. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyperiodchange """ - removeItems: Optional[List[AdvancedCommerceSubscriptionModifyRemoveItem]] = attr.ib(default=None) + removeItems: Optional[List[AdvancedCommerceSubscriptionModifyRemoveItem]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.items_validator)) """ + The data your app provides to remove an item from an auto-renewable subscription. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyremoveitem """ - storefront: Optional[str] = attr.ib(default=None) + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ + A three-letter code that represents the country or region associated with the App Store storefront. + https://developer.apple.com/documentation/advancedcommerceapi/storefront """ - taxCode: Optional[str] = attr.ib(default=None) + taxCode: Optional[str] = attr.ib(default=None, kw_only=True) """ + A tax code that applies to a SKU. + https://developer.apple.com/documentation/advancedcommerceapi/taxcode - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyPeriodChange.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyPeriodChange.py index a2f33177..d9e1a7c7 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyPeriodChange.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyPeriodChange.py @@ -1,12 +1,11 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -import attr from attr import define + from .AdvancedCommerceEffective import AdvancedCommerceEffective from .AdvancedCommercePeriod import AdvancedCommercePeriod from .LibraryUtility import AttrsRawValueAware - @define class AdvancedCommerceSubscriptionModifyPeriodChange(AttrsRawValueAware): """ @@ -15,8 +14,10 @@ class AdvancedCommerceSubscriptionModifyPeriodChange(AttrsRawValueAware): https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyperiodchange """ - effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True) + effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True, kw_only=True) """ + A string value that indicates when a requested change to an auto-renewable subscription goes into effect. + https://developer.apple.com/documentation/advancedcommerceapi/effective """ @@ -25,12 +26,14 @@ class AdvancedCommerceSubscriptionModifyPeriodChange(AttrsRawValueAware): See effective """ - period: AdvancedCommercePeriod = AdvancedCommercePeriod.create_main_attr('rawPeriod', raw_required=True) + period: AdvancedCommercePeriod = AdvancedCommercePeriod.create_main_attr('rawPeriod', raw_required=True, kw_only=True) """ + The duration of a single cycle of an auto-renewable subscription. + https://developer.apple.com/documentation/advancedcommerceapi/period """ rawPeriod: str = AdvancedCommercePeriod.create_raw_attr('period', required=True) """ See period - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyRemoveItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyRemoveItem.py index f978e7d3..fd3e306f 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyRemoveItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyRemoveItem.py @@ -1,13 +1,22 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define -from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem +import attr +from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem +from .HelperValidationUtils import HelperValidationUtils @define class AdvancedCommerceSubscriptionModifyRemoveItem(AbstractAdvancedCommerceBaseItem): """ The data your app provides to remove an item from an auto-renewable subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyremoveitem - """ \ No newline at end of file + """ + + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The product identifier of an Apple In-App Purchase product you manage in your own system. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeItem.py index 77889374..14e3e690 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeItem.py @@ -1,25 +1,39 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -import attr +from typing import Optional, List + from attr import define -from typing import List, Optional +import attr + from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionPriceChangeItem(AbstractAdvancedCommerceBaseItem): """ The data your app provides to change a subscription price. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionpricechangeitem """ - price: int = attr.ib() + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The product identifier of an Apple In-App Purchase product you manage in your own system. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ + + price: int = attr.ib(kw_only=True, validator=validate_not_none) """ + A price, in milliunits of a currency, for an Advanced Commerce API SKU. + https://developer.apple.com/documentation/advancedcommerceapi/price """ - dependentSKUs: Optional[List[str]] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.dependent_skus_validator)) + dependentSKUs: Optional[List[str]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.dependent_skus_validator)) """ + The product identifier of a dependent SKU in a subscription price change. + https://developer.apple.com/documentation/advancedcommerceapi/dependentsku - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeRequest.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeRequest.py index 13612b9a..0fa7d809 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeRequest.py @@ -1,36 +1,48 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List -import attr + from attr import define +import attr + from .AdvancedCommerceRequest import AdvancedCommerceRequest +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo from .AdvancedCommerceSubscriptionPriceChangeItem import AdvancedCommerceSubscriptionPriceChangeItem +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionPriceChangeRequest(AdvancedCommerceRequest): """ The request body you use to change the price of an auto-renewable subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionpricechangerequest """ - items: List[AdvancedCommerceSubscriptionPriceChangeItem] = attr.ib() + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) + """ + Metadata that identifies the request. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo + """ + + items: List[AdvancedCommerceSubscriptionPriceChangeItem] = attr.ib(kw_only=True, validator=HelperValidationUtils.items_validator) """ An array that contains one or more SKUs and the changed price for each SKU. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionpricechangeitem """ - currency: Optional[str] = attr.ib(default=None) + currency: Optional[str] = attr.ib(default=None, kw_only=True) """ The currency of the prices. - + https://developer.apple.com/documentation/advancedcommerceapi/currency """ - storefront: Optional[str] = attr.ib(default=None) + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ The App Store storefront of the subscription. - + https://developer.apple.com/documentation/advancedcommerceapi/storefront - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeResponse.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeResponse.py index 264b1d67..7fb8bd04 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeResponse.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeResponse.py @@ -1,15 +1,32 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define -from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +import attr +from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionPriceChangeResponse(AbstractAdvancedCommerceResponse): """ A response that contains signed JWS renewal and JWS transaction information after a subscription price change request. - + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionpricechangeresponse """ - def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str): - super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) \ No newline at end of file + + signedRenewalInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Subscription renewal information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwsrenewalinfo + """ + + signedTransactionInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Transaction information signed by the App Store, in JWS Compact Serialization format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwstransaction + """ + + def __init__(self, *, signedRenewalInfo: str, signedTransactionInfo: str): + super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateInAppRequest.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateInAppRequest.py index ee7cdb78..20af3050 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateInAppRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateInAppRequest.py @@ -1,33 +1,60 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from __future__ import annotations -from typing import List, Optional +from typing import Optional, List + +from attr import define import attr -from appstoreserverlibrary.models.AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest -from appstoreserverlibrary.models.AdvancedCommerceSubscriptionReactivateItem import AdvancedCommerceSubscriptionReactivateItem -@attr.define +from .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo +from .AdvancedCommerceSubscriptionReactivateItem import AdvancedCommerceSubscriptionReactivateItem +from .HelperValidationUtils import HelperValidationUtils +from .LibraryUtility import validate_not_none + +@define class AdvancedCommerceSubscriptionReactivateInAppRequest(AbstractAdvancedCommerceInAppRequest): """ The request your app provides to reactivate a subscription that has automatic renewal turned off. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateinapprequest """ + + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) + """ + The metadata to include in server requests. + + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo + """ + operation: str = attr.ib(init=False, default="REACTIVATE_SUBSCRIPTION", on_setattr=attr.setters.frozen) + """ + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateinapprequest + """ version: str = attr.ib(init=False, default="1", on_setattr=attr.setters.frozen) + """ + A number that represents the version of the data structure or Advanced Commerce API. - transactionId: str = attr.ib() + https://developer.apple.com/documentation/advancedcommerceapi/version """ - https://developer.apple.com/documentation/appstoreserverapi/transactionid + + transactionId: str = attr.ib(kw_only=True, validator=validate_not_none) """ - - items: Optional[List[AdvancedCommerceSubscriptionReactivateItem]] = attr.ib(default=None) + A unique identifier that the App Store generates for a transaction. + + https://developer.apple.com/documentation/advancedcommerceapi/transactionid + """ + + items: Optional[List[AdvancedCommerceSubscriptionReactivateItem]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(HelperValidationUtils.items_validator)) """ + An item in a subscription to reactivate. + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateitem """ - - storefront: Optional[str] = attr.ib(default=None) + + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ + A three-letter code that represents the country or region associated with the App Store storefront. + https://developer.apple.com/documentation/advancedcommerceapi/storefront - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateItem.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateItem.py index 7561a994..9fe32679 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateItem.py @@ -1,13 +1,22 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define -from appstoreserverlibrary.models.AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem +import attr +from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem +from .HelperValidationUtils import HelperValidationUtils @define class AdvancedCommerceSubscriptionReactivateItem(AbstractAdvancedCommerceBaseItem): """ - An item in a subscription to reactive. + An item in a subscription to reactivate. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateitem - """ \ No newline at end of file + """ + + SKU: str = attr.ib(kw_only=True, validator=HelperValidationUtils.sku_validator) + """ + The SKU of the item to reactivate. + + https://developer.apple.com/documentation/advancedcommerceapi/sku + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeRequest.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeRequest.py index f8d32f70..7889657b 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeRequest.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeRequest.py @@ -1,14 +1,17 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from __future__ import annotations from typing import Optional + +from attr import define import attr -from appstoreserverlibrary.models.AdvancedCommerceRequest import AdvancedCommerceRequest -from appstoreserverlibrary.models.AdvancedCommerceRefundReason import AdvancedCommerceRefundReason -from appstoreserverlibrary.models.AdvancedCommerceRefundType import AdvancedCommerceRefundType +from .AdvancedCommerceRefundReason import AdvancedCommerceRefundReason +from .AdvancedCommerceRefundType import AdvancedCommerceRefundType +from .AdvancedCommerceRequest import AdvancedCommerceRequest +from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo +from .LibraryUtility import validate_not_none -@attr.define +@define class AdvancedCommerceSubscriptionRevokeRequest(AdvancedCommerceRequest): """ The request body you provide to terminate a subscription and all its items immediately. @@ -16,20 +19,17 @@ class AdvancedCommerceSubscriptionRevokeRequest(AdvancedCommerceRequest): https://developer.apple.com/documentation/advancedcommerceapi/subscriptionrevokerequest """ - refundRiskingPreference: bool = attr.ib() - """ - https://developer.apple.com/documentation/advancedcommerceapi/refundriskingpreference + requestInfo: AdvancedCommerceRequestInfo = attr.ib(kw_only=True, validator=validate_not_none) """ + The metadata to include in server requests. - refundType: AdvancedCommerceRefundType = AdvancedCommerceRefundType.create_main_attr('rawRefundType', raw_required=True) - - rawRefundType: str = AdvancedCommerceRefundType.create_raw_attr('refundType', required=True) - """ - See refundType + https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ - refundReason: AdvancedCommerceRefundReason = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', raw_required=True) + refundReason: AdvancedCommerceRefundReason = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', raw_required=True, kw_only=True) """ + A reason to request a refund. + https://developer.apple.com/documentation/advancedcommerceapi/refundreason """ @@ -38,7 +38,26 @@ class AdvancedCommerceSubscriptionRevokeRequest(AdvancedCommerceRequest): See refundReason """ - storefront: Optional[str] = attr.ib(default=None) + refundRiskingPreference: bool = attr.ib(kw_only=True, validator=validate_not_none) + """ + A Boolean value that indicates whether the App Store asks you for consumption data to help inform the refund decision. + + https://developer.apple.com/documentation/advancedcommerceapi/refundriskingpreference + """ + + refundType: AdvancedCommerceRefundType = AdvancedCommerceRefundType.create_main_attr('rawRefundType', raw_required=True, kw_only=True) + """ + https://developer.apple.com/documentation/advancedcommerceapi/subscriptionrevokerequest + """ + + rawRefundType: str = AdvancedCommerceRefundType.create_raw_attr('refundType', required=True) + """ + See refundType + """ + + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ + A three-letter code that represents the country or region associated with the App Store storefront. + https://developer.apple.com/documentation/advancedcommerceapi/storefront - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeResponse.py b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeResponse.py index e7066078..e8293218 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeResponse.py +++ b/appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeResponse.py @@ -1,8 +1,10 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define -from appstoreserverlibrary.models.AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +import attr +from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse +from .LibraryUtility import validate_not_none @define class AdvancedCommerceSubscriptionRevokeResponse(AbstractAdvancedCommerceResponse): @@ -11,5 +13,20 @@ class AdvancedCommerceSubscriptionRevokeResponse(AbstractAdvancedCommerceRespons https://developer.apple.com/documentation/advancedcommerceapi/subscriptionrevokeresponse """ - def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str): - super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) \ No newline at end of file + + signedRenewalInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Subscription renewal information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwsrenewalinfo + """ + + signedTransactionInfo: str = attr.ib(kw_only=True, validator=validate_not_none) + """ + Transaction information signed by the App Store, in JWS Compact Serialization format. + + https://developer.apple.com/documentation/advancedcommerceapi/jwstransaction + """ + + def __init__(self, *, signedRenewalInfo: str, signedTransactionInfo: str): + super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) diff --git a/appstoreserverlibrary/models/AdvancedCommerceTransactionInfo.py b/appstoreserverlibrary/models/AdvancedCommerceTransactionInfo.py index 5f628d4b..05f660c4 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceTransactionInfo.py +++ b/appstoreserverlibrary/models/AdvancedCommerceTransactionInfo.py @@ -1,6 +1,6 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import List, Optional +from typing import Optional, List from attr import define import attr @@ -16,22 +16,22 @@ class AdvancedCommerceTransactionInfo(AttrsRawValueAware): https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetransactioninfo """ - descriptors: Optional[AdvancedCommerceDescriptors] = attr.ib(default=None) + descriptors: Optional[AdvancedCommerceDescriptors] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercedescriptors """ - estimatedTax: Optional[int] = attr.ib(default=None) + estimatedTax: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommerceestimatedtax """ - items: Optional[List[AdvancedCommerceTransactionItem]] = attr.ib(default=None) + items: Optional[List[AdvancedCommerceTransactionItem]] = attr.ib(default=None, kw_only=True) """ - https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetransactionitem + https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetransactionitems """ - period: Optional[AdvancedCommercePeriod] = AdvancedCommercePeriod.create_main_attr('rawPeriod') + period: Optional[AdvancedCommercePeriod] = AdvancedCommercePeriod.create_main_attr('rawPeriod', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommerceperiod """ @@ -41,22 +41,22 @@ class AdvancedCommerceTransactionInfo(AttrsRawValueAware): See period """ - requestReferenceId: Optional[str] = attr.ib(default=None) + requestReferenceId: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerequestreferenceid """ - taxCode: Optional[str] = attr.ib(default=None) + taxCode: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetaxcode """ - taxExclusivePrice: Optional[int] = attr.ib(default=None) + taxExclusivePrice: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetaxexclusiveprice """ - taxRate: Optional[str] = attr.ib(default=None) + taxRate: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetaxrate """ diff --git a/appstoreserverlibrary/models/AdvancedCommerceTransactionItem.py b/appstoreserverlibrary/models/AdvancedCommerceTransactionItem.py index 97a64b23..aa35a49e 100644 --- a/appstoreserverlibrary/models/AdvancedCommerceTransactionItem.py +++ b/appstoreserverlibrary/models/AdvancedCommerceTransactionItem.py @@ -1,6 +1,6 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import List, Optional +from typing import Optional, List from attr import define import attr @@ -14,37 +14,39 @@ class AdvancedCommerceTransactionItem: https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetransactionitem """ - SKU: Optional[str] = attr.ib(default=None) + SKU: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercesku """ - description: Optional[str] = attr.ib(default=None) + description: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercedescription """ - displayName: Optional[str] = attr.ib(default=None) + displayName: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercedisplayname """ - offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None) + offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommerceoffer """ - price: Optional[int] = attr.ib(default=None) + price: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommerceprice """ - refunds: Optional[List[AdvancedCommerceRefund]] = attr.ib(default=None) + refunds: Optional[List[AdvancedCommerceRefund]] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerefunds """ - revocationDate: Optional[int] = attr.ib(default=None) + revocationDate: Optional[int] = attr.ib(default=None, kw_only=True) """ + The UNIX time, in milliseconds, that the App Store refunded the transaction or revoked it from Family Sharing. + https://developer.apple.com/documentation/appstoreserverapi/revocationdate """ diff --git a/appstoreserverlibrary/models/AlternateProduct.py b/appstoreserverlibrary/models/AlternateProduct.py index d2c67c68..11e92091 100644 --- a/appstoreserverlibrary/models/AlternateProduct.py +++ b/appstoreserverlibrary/models/AlternateProduct.py @@ -1,4 +1,5 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. + from typing import Optional from uuid import UUID @@ -6,7 +7,7 @@ import attr from .BillingPlanType import BillingPlanType -from .LibraryUtility import AttrsRawValueAware +from .LibraryUtility import AttrsRawValueAware, validate_not_none @define class AlternateProduct(AttrsRawValueAware): @@ -16,21 +17,21 @@ class AlternateProduct(AttrsRawValueAware): https://developer.apple.com/documentation/retentionmessaging/alternateproduct """ - messageIdentifier: Optional[UUID] = attr.ib(default=None) + messageIdentifier: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ The message identifier of the text to display in the switch-plan retention message. https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ - productId: Optional[str] = attr.ib(default=None) + productId: str = attr.ib(kw_only=True, validator=validate_not_none) """ The product identifier of the subscription the retention message suggests for your customer to switch to. https://developer.apple.com/documentation/retentionmessaging/productid """ - billingPlanType: Optional[BillingPlanType] = BillingPlanType.create_main_attr('rawBillingPlanType') + billingPlanType: Optional[BillingPlanType] = BillingPlanType.create_main_attr('rawBillingPlanType', kw_only=True) """ https://developer.apple.com/documentation/retentionmessaging/billingplantype """ diff --git a/appstoreserverlibrary/models/AppData.py b/appstoreserverlibrary/models/AppData.py index e2a26a24..21e850e0 100644 --- a/appstoreserverlibrary/models/AppData.py +++ b/appstoreserverlibrary/models/AppData.py @@ -1,4 +1,5 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -15,21 +16,21 @@ class AppData(AttrsRawValueAware): https://developer.apple.com/documentation/appstoreservernotifications/appdata """ - appAppleId: Optional[int] = attr.ib(default=None) + appAppleId: Optional[int] = attr.ib(default=None, kw_only=True) """ The unique identifier of the app that the notification applies to. https://developer.apple.com/documentation/appstoreservernotifications/appappleid """ - bundleId: Optional[str] = attr.ib(default=None) + bundleId: Optional[str] = attr.ib(default=None, kw_only=True) """ The bundle identifier of the app. https://developer.apple.com/documentation/appstoreservernotifications/bundleid """ - environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') + environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', kw_only=True) """ The server environment that the notification applies to, either sandbox or production. @@ -41,7 +42,7 @@ class AppData(AttrsRawValueAware): See environment """ - signedAppTransactionInfo: Optional[str] = attr.ib(default=None) + signedAppTransactionInfo: Optional[str] = attr.ib(default=None, kw_only=True) """ App transaction information signed by the App Store, in JSON Web Signature (JWS) format. diff --git a/appstoreserverlibrary/models/AppTransaction.py b/appstoreserverlibrary/models/AppTransaction.py index b8d31b6a..9d53ba36 100644 --- a/appstoreserverlibrary/models/AppTransaction.py +++ b/appstoreserverlibrary/models/AppTransaction.py @@ -1,13 +1,15 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional +from uuid import UUID from attr import define import attr -from .LibraryUtility import AttrsRawValueAware - from .Environment import Environment +from .LibraryUtility import AttrsRawValueAware from .PurchasePlatform import PurchasePlatform +from .StoreType import StoreType @define class AppTransaction(AttrsRawValueAware): @@ -18,103 +20,115 @@ class AppTransaction(AttrsRawValueAware): https://developer.apple.com/documentation/appstoreserverapi/jwsapptransactiondecodedpayload """ - receiptType: Optional[Environment] = Environment.create_main_attr('rawReceiptType') + appAppleId: Optional[int] = attr.ib(default=None, kw_only=True) """ - The date that the App Store signed the JWS app transaction. + The unique identifier the App Store uses to identify the app. - https://developer.apple.com/documentation/appstoreserverapi/environment + https://developer.apple.com/documentation/appstoreserverapi/appappleid """ - rawReceiptType: Optional[str] = Environment.create_raw_attr('receiptType') - """ - See receiptType + appTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) """ + The unique identifier of the app download transaction. - appAppleId: Optional[int] = attr.ib(default=None) - """ - The unique identifier the App Store uses to identify the app. - - https://developer.apple.com/documentation/appstoreserverapi/appappleid + https://developer.apple.com/documentation/appstoreserverapi/apptransactionid """ - bundleId: Optional[str] = attr.ib(default=None) + bundleId: Optional[str] = attr.ib(default=None, kw_only=True) """ The bundle identifier that the app transaction applies to. - + https://developer.apple.com/documentation/appstoreserverapi/bundleid """ - applicationVersion: Optional[str] = attr.ib(default=None) + originalApplicationVersion: Optional[str] = attr.ib(default=None, kw_only=True) """ - The app version that the app transaction applies to. - - https://developer.apple.com/documentation/storekit/apptransaction/appversion + The app version that the customer originally purchased from the App Store. + + https://developer.apple.com/documentation/appstoreserverapi/originalapplicationversion """ - versionExternalIdentifier: Optional[int] = attr.ib(default=None) + originalPlatform: Optional[PurchasePlatform] = PurchasePlatform.create_main_attr('rawOriginalPlatform', kw_only=True) """ - The version external identifier of the app - - https://developer.apple.com/documentation/storekit/apptransaction/appversionid + The platform on which the customer originally purchased the app. + + https://developer.apple.com/documentation/appstoreserverapi/originalplatform """ - receiptCreationDate: Optional[int] = attr.ib(default=None) + rawOriginalPlatform: Optional[str] = PurchasePlatform.create_raw_attr('originalPlatform') """ - The date that the App Store signed the JWS app transaction. - - https://developer.apple.com/documentation/appstoreserverapi/receiptcreationdate + See originalPlatform """ - originalPurchaseDate: Optional[int] = attr.ib(default=None) + originalPurchaseDate: Optional[int] = attr.ib(default=None, kw_only=True) """ The date the customer originally purchased the app from the App Store. https://developer.apple.com/documentation/appstoreserverapi/originalpurchasedate """ - originalApplicationVersion: Optional[str] = attr.ib(default=None) + preorderDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The app version that the user originally purchased from the App Store. - - https://developer.apple.com/documentation/appstoreserverapi/originalapplicationversion + The date the customer placed an order for the app before it’s available in the App Store. + + https://developer.apple.com/documentation/appstoreserverapi/preorderdate """ - deviceVerification: Optional[str] = attr.ib(default=None) + receiptCreationDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The Base64 device verification value to use to verify whether the app transaction belongs to the device. - - https://developer.apple.com/documentation/storekit/apptransaction/deviceverification + The date that the App Store signed the JWS app transaction. + + https://developer.apple.com/documentation/appstoreserverapi/receiptcreationdate """ - deviceVerificationNonce: Optional[str] = attr.ib(default=None) + receiptType: Optional[Environment] = Environment.create_main_attr('rawReceiptType', kw_only=True) """ - The UUID used to compute the device verification value. - - https://developer.apple.com/documentation/storekit/apptransaction/deviceverificationnonce + The server environment, production or sandbox, that signed the app transaction. + + https://developer.apple.com/documentation/appstoreserverapi/environment """ - preorderDate: Optional[int] = attr.ib(default=None) + rawReceiptType: Optional[str] = Environment.create_raw_attr('receiptType') """ - The date the customer placed an order for the app before it's available in the App Store. - - https://developer.apple.com/documentation/appstoreserverapi/preorderdate + See receiptType """ - appTransactionId: Optional[str] = attr.ib(default=None) + storeType: Optional[StoreType] = StoreType.create_main_attr('rawStoreType', kw_only=True) """ - The unique identifier of the app download transaction. - - https://developer.apple.com/documentation/appstoreserverapi/apptransactionid + The store the customer obtained the app from. + + https://developer.apple.com/documentation/appstoreserverapi/storetype """ - originalPlatform: Optional[PurchasePlatform] = PurchasePlatform.create_main_attr('rawOriginalPlatform') + rawStoreType: Optional[str] = StoreType.create_raw_attr('storeType') """ - The platform on which the customer originally purchased the app. - - https://developer.apple.com/documentation/appstoreserverapi/originalplatform + See storeType """ - rawOriginalPlatform: Optional[str] = PurchasePlatform.create_raw_attr('originalPlatform') + applicationVersion: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The app version that the app transaction applies to. + + https://developer.apple.com/documentation/storekit/apptransaction/appversion + """ + + versionExternalIdentifier: Optional[int] = attr.ib(default=None, kw_only=True) + """ + The number that the App Store uses to uniquely identify the version of the app. + + https://developer.apple.com/documentation/storekit/apptransaction/appversionid + """ + + deviceVerification: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The device verification value to use to verify whether the app transaction belongs to the device. + + https://developer.apple.com/documentation/storekit/apptransaction/deviceverification + """ + + deviceVerificationNonce: Optional[UUID] = attr.ib(default=None, kw_only=True) + """ + The UUID used to compute the device verification value. + + https://developer.apple.com/documentation/storekit/apptransaction/deviceverificationnonce """ - See originalPlatform - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/AppTransactionInfoResponse.py b/appstoreserverlibrary/models/AppTransactionInfoResponse.py index 5deb7fde..fac2f604 100644 --- a/appstoreserverlibrary/models/AppTransactionInfoResponse.py +++ b/appstoreserverlibrary/models/AppTransactionInfoResponse.py @@ -1,4 +1,5 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -8,13 +9,13 @@ class AppTransactionInfoResponse: """ A response that contains signed app transaction information for a customer. - + https://developer.apple.com/documentation/appstoreserverapi/apptransactioninforesponse """ - - signedAppTransactionInfo: Optional[str] = attr.ib(default=None) + + signedAppTransactionInfo: Optional[str] = attr.ib(default=None, kw_only=True) """ A customer’s app transaction information, signed by Apple, in JSON Web Signature (JWS) format. - + https://developer.apple.com/documentation/appstoreserverapi/jwsapptransaction - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/AutoRenewStatus.py b/appstoreserverlibrary/models/AutoRenewStatus.py index c12deff4..bdbf0aa1 100644 --- a/appstoreserverlibrary/models/AutoRenewStatus.py +++ b/appstoreserverlibrary/models/AutoRenewStatus.py @@ -7,8 +7,15 @@ class AutoRenewStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The renewal status for an auto-renewable subscription. - + https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus """ OFF = 0 + """ + Automatic renewal is off. + """ + ON = 1 + """ + Automatic renewal is on. + """ diff --git a/appstoreserverlibrary/models/BulletPoint.py b/appstoreserverlibrary/models/BulletPoint.py index 1380c34f..1c6e106c 100644 --- a/appstoreserverlibrary/models/BulletPoint.py +++ b/appstoreserverlibrary/models/BulletPoint.py @@ -5,6 +5,8 @@ from attr import define import attr +from .LibraryUtility import validate_not_none + @define class BulletPoint: """ @@ -13,23 +15,23 @@ class BulletPoint: https://developer.apple.com/documentation/retentionmessaging/bulletpoint """ - text: str = attr.ib(validator=attr.validators.max_len(66)) + altText: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(150)]) """ - The text of the individual bullet point. + The alternative text you provide for the corresponding image of the bullet point. - https://developer.apple.com/documentation/retentionmessaging/bulletpointtext + https://developer.apple.com/documentation/retentionmessaging/alttext """ - imageIdentifier: UUID = attr.ib() + imageIdentifier: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ The identifier of the image to use as the bullet point. https://developer.apple.com/documentation/retentionmessaging/imageidentifier """ - altText: str = attr.ib(validator=attr.validators.max_len(150)) + text: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(66)]) """ - The alternative text you provide for the corresponding image of the bullet point. + The text of the individual bullet point. - https://developer.apple.com/documentation/retentionmessaging/alttext + https://developer.apple.com/documentation/retentionmessaging/bulletpointtext """ diff --git a/appstoreserverlibrary/models/CheckTestNotificationResponse.py b/appstoreserverlibrary/models/CheckTestNotificationResponse.py index e6bc1476..0c154625 100644 --- a/appstoreserverlibrary/models/CheckTestNotificationResponse.py +++ b/appstoreserverlibrary/models/CheckTestNotificationResponse.py @@ -1,5 +1,6 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. -from typing import List, Optional + +from typing import Optional, List from attr import define import attr @@ -7,23 +8,23 @@ from .SendAttemptItem import SendAttemptItem @define -class CheckTestNotificationResponse: +class CheckTestNotificationResponse: """ - A response that contains the contents of the test notification sent by the App Store server and the result from your server. - + A response that contains the contents of the App Store server’s test notification and the result from your server. + https://developer.apple.com/documentation/appstoreserverapi/checktestnotificationresponse """ - signedPayload: Optional[str] = attr.ib(default=None) + sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None, kw_only=True) """ - A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. - - https://developer.apple.com/documentation/appstoreservernotifications/signedpayload + An array of information the App Store server records for its attempts to send the TEST notification to your server. + + https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem """ - sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None) + signedPayload: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The signed payload, in JWS format, that contains the TEST notification that the App Store server sent to your server. + + https://developer.apple.com/documentation/appstoreserverapi/signedpayload """ - An array of information the App Store server records for its attempts to send the TEST notification to your server. The array may contain a maximum of six sendAttemptItem objects. - - https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/ConsumptionRequest.py b/appstoreserverlibrary/models/ConsumptionRequest.py index ec83073a..a0565a69 100644 --- a/appstoreserverlibrary/models/ConsumptionRequest.py +++ b/appstoreserverlibrary/models/ConsumptionRequest.py @@ -1,11 +1,12 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define import attr from .DeliveryStatus import DeliveryStatus -from .LibraryUtility import AttrsRawValueAware +from .LibraryUtility import AttrsRawValueAware, validate_not_none from .RefundPreference import RefundPreference @define @@ -16,42 +17,42 @@ class ConsumptionRequest(AttrsRawValueAware): https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest """ - customerConsented: bool = attr.ib() + customerConsented: bool = attr.ib(kw_only=True, validator=validate_not_none) """ - A Boolean value that indicates whether the customer consented to provide consumption data to the App Store. + A Boolean value of true or false that indicates whether the customer consented to provide consumption data. https://developer.apple.com/documentation/appstoreserverapi/customerconsented """ - sampleContentProvided: bool = attr.ib() + deliveryStatus: DeliveryStatus = DeliveryStatus.create_main_attr('rawDeliveryStatus', raw_required=True, kw_only=True) """ - A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. + A value that indicates whether the app successfully delivered an In-App Purchase that works properly. - https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided + https://developer.apple.com/documentation/appstoreserverapi/deliverystatus """ - deliveryStatus: Optional[DeliveryStatus] = DeliveryStatus.create_main_attr('rawDeliveryStatus', raw_required=True) + rawDeliveryStatus: str = DeliveryStatus.create_raw_attr('deliveryStatus', required=True) """ - A value that indicates whether the app successfully delivered an in-app purchase that works properly. - - https://developer.apple.com/documentation/appstoreserverapi/deliverystatus + See deliveryStatus """ - - consumptionPercentage: Optional[int] = attr.ib(default=None) + + sampleContentProvided: bool = attr.ib(kw_only=True, validator=validate_not_none) """ - An integer that indicates the percentage, in milliunits, of the In-App Purchase the customer consumed. + A Boolean value of true or false that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. - https://developer.apple.com/documentation/appstoreserverapi/consumptionpercentage + https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided """ - rawDeliveryStatus: str = DeliveryStatus.create_raw_attr('deliveryStatus', required=True) + consumptionPercentage: Optional[int] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(attr.validators.and_(attr.validators.ge(0), attr.validators.le(100000)))) """ - See deliveryStatus + An integer that indicates the percentage of the In-App Purchase the customer consumed, in milliunits. + + https://developer.apple.com/documentation/appstoreserverapi/consumptionpercentage """ - refundPreference: Optional[RefundPreference] = RefundPreference.create_main_attr('rawRefundPreference') + refundPreference: Optional[RefundPreference] = RefundPreference.create_main_attr('rawRefundPreference', kw_only=True) """ - A value that indicates your preferred outcome for the refund request. + A value that indicates your preference, based on your operational logic, as to whether the App Store should grant the refund. https://developer.apple.com/documentation/appstoreserverapi/refundpreference """ diff --git a/appstoreserverlibrary/models/ConsumptionRequestReason.py b/appstoreserverlibrary/models/ConsumptionRequestReason.py index 9907f117..79334bc9 100644 --- a/appstoreserverlibrary/models/ConsumptionRequestReason.py +++ b/appstoreserverlibrary/models/ConsumptionRequestReason.py @@ -7,11 +7,30 @@ class ConsumptionRequestReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The customer-provided reason for a refund request. - + https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason """ UNINTENDED_PURCHASE = "UNINTENDED_PURCHASE" + """ + The customer didn’t intend to make the in-app purchase. + """ + FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE" + """ + The customer had issues with receiving or using the in-app purchase. + """ + UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE" + """ + The customer wasn’t satisfied with the in-app purchase. + """ + LEGAL = "LEGAL" + """ + The customer requested a refund based on a legal reason. + """ + OTHER = "OTHER" + """ + The customer requested a refund for other reasons. + """ diff --git a/appstoreserverlibrary/models/ConsumptionRequestV1.py b/appstoreserverlibrary/models/ConsumptionRequestV1.py index 43da60a6..3b2ad90b 100644 --- a/appstoreserverlibrary/models/ConsumptionRequestV1.py +++ b/appstoreserverlibrary/models/ConsumptionRequestV1.py @@ -1,5 +1,7 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional +from uuid import UUID from attr import define import attr @@ -7,7 +9,7 @@ from .AccountTenure import AccountTenure from .ConsumptionStatus import ConsumptionStatus from .DeliveryStatusV1 import DeliveryStatusV1 -from .LibraryUtility import AttrsRawValueAware +from .LibraryUtility import AttrsRawValueAware, metadata_none_value_key, validate_not_none from .LifetimeDollarsPurchased import LifetimeDollarsPurchased from .LifetimeDollarsRefunded import LifetimeDollarsRefunded from .Platform import Platform @@ -20,134 +22,134 @@ class ConsumptionRequestV1(AttrsRawValueAware): """ The request body containing consumption information. - .. deprecated:: + .. deprecated:: 3.0.0 Use :class:`ConsumptionRequest` instead. https://developer.apple.com/documentation/appstoreserverapi/consumptionrequestv1 """ - customerConsented: Optional[bool] = attr.ib(default=None) + accountTenure: AccountTenure = AccountTenure.create_main_attr('rawAccountTenure', raw_required=True, kw_only=True) """ - A Boolean value that indicates whether the customer consented to provide consumption data to the App Store. + The age of the customer’s account. - https://developer.apple.com/documentation/appstoreserverapi/customerconsented + https://developer.apple.com/documentation/appstoreserverapi/accounttenure """ - consumptionStatus: Optional[ConsumptionStatus] = ConsumptionStatus.create_main_attr('rawConsumptionStatus') + rawAccountTenure: int = AccountTenure.create_raw_attr('accountTenure', required=True) """ - A value that indicates the extent to which the customer consumed the in-app purchase. - - https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus + See accountTenure """ - rawConsumptionStatus: Optional[int] = ConsumptionStatus.create_raw_attr('consumptionStatus') + appAccountToken: Optional[UUID] = attr.ib(kw_only=True, metadata={metadata_none_value_key: ''}) """ - See consumptionStatus + The UUID of the in-app user account that completed the in-app purchase transaction. + + https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken """ - platform: Optional[Platform] = Platform.create_main_attr('rawPlatform') + consumptionStatus: ConsumptionStatus = ConsumptionStatus.create_main_attr('rawConsumptionStatus', raw_required=True, kw_only=True) """ - A value that indicates the platform on which the customer consumed the in-app purchase. + A value that indicates the extent to which the customer consumed the in-app purchase. - https://developer.apple.com/documentation/appstoreserverapi/platform + https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus """ - rawPlatform: Optional[int] = Platform.create_raw_attr('platform') + rawConsumptionStatus: int = ConsumptionStatus.create_raw_attr('consumptionStatus', required=True) """ - See platform + See consumptionStatus """ - sampleContentProvided: Optional[bool] = attr.ib(default=None) + customerConsented: bool = attr.ib(kw_only=True, validator=validate_not_none) """ - A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. + A Boolean value of true or false that indicates whether the customer consented to provide consumption data. - https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided + https://developer.apple.com/documentation/appstoreserverapi/customerconsented """ - deliveryStatus: Optional[DeliveryStatusV1] = DeliveryStatusV1.create_main_attr('rawDeliveryStatus') + deliveryStatus: DeliveryStatusV1 = DeliveryStatusV1.create_main_attr('rawDeliveryStatus', raw_required=True, kw_only=True) """ A value that indicates whether the app successfully delivered an in-app purchase that works properly. - https://developer.apple.com/documentation/appstoreserverapi/deliverystatus + https://developer.apple.com/documentation/appstoreserverapi/deliverystatusv1 """ - rawDeliveryStatus: Optional[int] = DeliveryStatusV1.create_raw_attr('deliveryStatus') + rawDeliveryStatus: int = DeliveryStatusV1.create_raw_attr('deliveryStatus', required=True) """ See deliveryStatus """ - appAccountToken: Optional[str] = attr.ib(default=None) + lifetimeDollarsPurchased: LifetimeDollarsPurchased = LifetimeDollarsPurchased.create_main_attr('rawLifetimeDollarsPurchased', raw_required=True, kw_only=True) """ - The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. + A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. - https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken + https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased """ - accountTenure: Optional[AccountTenure] = AccountTenure.create_main_attr('rawAccountTenure') + rawLifetimeDollarsPurchased: int = LifetimeDollarsPurchased.create_raw_attr('lifetimeDollarsPurchased', required=True) + """ + See lifetimeDollarsPurchased """ - The age of the customer's account. - https://developer.apple.com/documentation/appstoreserverapi/accounttenure + lifetimeDollarsRefunded: LifetimeDollarsRefunded = LifetimeDollarsRefunded.create_main_attr('rawLifetimeDollarsRefunded', raw_required=True, kw_only=True) """ + A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. - rawAccountTenure: Optional[int] = AccountTenure.create_raw_attr('accountTenure') + https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded """ - See accountTenure + + rawLifetimeDollarsRefunded: int = LifetimeDollarsRefunded.create_raw_attr('lifetimeDollarsRefunded', required=True) + """ + See lifetimeDollarsRefunded """ - playTime: Optional[PlayTime] = PlayTime.create_main_attr('rawPlayTime') + platform: Platform = Platform.create_main_attr('rawPlatform', raw_required=True, kw_only=True) """ - A value that indicates the amount of time that the customer used the app. + A value that indicates the platform on which the customer consumed the in-app purchase. - https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest + https://developer.apple.com/documentation/appstoreserverapi/platform """ - rawPlayTime: Optional[int] = PlayTime.create_raw_attr('playTime') + rawPlatform: int = Platform.create_raw_attr('platform', required=True) """ - See playTime + See platform """ - lifetimeDollarsRefunded: Optional[LifetimeDollarsRefunded] = LifetimeDollarsRefunded.create_main_attr('rawLifetimeDollarsRefunded') + playTime: PlayTime = PlayTime.create_main_attr('rawPlayTime', raw_required=True, kw_only=True) """ - A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. + A value that indicates the amount of time that the customer used the app. - https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded + https://developer.apple.com/documentation/appstoreserverapi/playtime """ - rawLifetimeDollarsRefunded: Optional[int] = LifetimeDollarsRefunded.create_raw_attr('lifetimeDollarsRefunded') + rawPlayTime: int = PlayTime.create_raw_attr('playTime', required=True) """ - See lifetimeDollarsRefunded + See playTime """ - lifetimeDollarsPurchased: Optional[LifetimeDollarsPurchased] = LifetimeDollarsPurchased.create_main_attr('rawLifetimeDollarsPurchased') + sampleContentProvided: bool = attr.ib(kw_only=True, validator=validate_not_none) """ - A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. + A Boolean value of true or false that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. - https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased - """ - - rawLifetimeDollarsPurchased: Optional[int] = LifetimeDollarsPurchased.create_raw_attr('lifetimeDollarsPurchased') - """ - See lifetimeDollarsPurchased + https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided """ - userStatus: Optional[UserStatus] = UserStatus.create_main_attr('rawUserStatus') + userStatus: UserStatus = UserStatus.create_main_attr('rawUserStatus', raw_required=True, kw_only=True) """ - The status of the customer's account. + The status of the customer’s account. https://developer.apple.com/documentation/appstoreserverapi/userstatus """ - rawUserStatus: Optional[int] = UserStatus.create_raw_attr('userStatus') + rawUserStatus: int = UserStatus.create_raw_attr('userStatus', required=True) """ See userStatus """ - refundPreference: Optional[RefundPreferenceV1] = RefundPreferenceV1.create_main_attr('rawRefundPreference') + refundPreference: Optional[RefundPreferenceV1] = RefundPreferenceV1.create_main_attr('rawRefundPreference', kw_only=True) """ A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund. - https://developer.apple.com/documentation/appstoreserverapi/refundpreference + https://developer.apple.com/documentation/appstoreserverapi/refundpreferencev1 """ rawRefundPreference: Optional[int] = RefundPreferenceV1.create_raw_attr('refundPreference') diff --git a/appstoreserverlibrary/models/ConsumptionStatus.py b/appstoreserverlibrary/models/ConsumptionStatus.py index 0a738cee..c7c92b6f 100644 --- a/appstoreserverlibrary/models/ConsumptionStatus.py +++ b/appstoreserverlibrary/models/ConsumptionStatus.py @@ -6,11 +6,26 @@ class ConsumptionStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ - A value that indicates the extent to which the customer consumed the in-app purchase. - + A value that indicates the extent to which the customer consumed the In-App Purchase. + https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus """ UNDECLARED = 0 + """ + The consumption status is undeclared. + """ + NOT_CONSUMED = 1 + """ + The In-App Purchase is not consumed. + """ + PARTIALLY_CONSUMED = 2 + """ + The In-App Purchase is partially consumed. + """ + FULLY_CONSUMED = 3 + """ + The In-App Purchase is fully consumed. + """ diff --git a/appstoreserverlibrary/models/Data.py b/appstoreserverlibrary/models/Data.py index c69e5a02..862a11d6 100644 --- a/appstoreserverlibrary/models/Data.py +++ b/appstoreserverlibrary/models/Data.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -6,82 +7,84 @@ from .ConsumptionRequestReason import ConsumptionRequestReason from .Environment import Environment -from .Status import Status from .LibraryUtility import AttrsRawValueAware +from .Status import Status @define class Data(AttrsRawValueAware): """ - The app metadata and the signed renewal and transaction information. - + The payload data that contains app metadata and the signed renewal and transaction information. + https://developer.apple.com/documentation/appstoreservernotifications/data """ - environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') + + appAppleId: Optional[int] = attr.ib(default=None, kw_only=True) """ - The server environment that the notification applies to, either sandbox or production. - - https://developer.apple.com/documentation/appstoreservernotifications/environment + The unique identifier of the app that the notification applies to. + + https://developer.apple.com/documentation/appstoreservernotifications/appappleid """ - rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') + + bundleId: Optional[str] = attr.ib(default=None, kw_only=True) """ - See environment + The bundle identifier of the app. + + https://developer.apple.com/documentation/appstoreservernotifications/bundleid """ - appAppleId: Optional[int] = attr.ib(default=None) + bundleVersion: Optional[str] = attr.ib(default=None, kw_only=True) """ - The unique identifier of an app in the App Store. - - https://developer.apple.com/documentation/appstoreservernotifications/appappleid + The version of the build that identifies an iteration of the bundle. + + https://developer.apple.com/documentation/appstoreservernotifications/bundleversion """ - bundleId: Optional[str] = attr.ib(default=None) + consumptionRequestReason: Optional[ConsumptionRequestReason] = ConsumptionRequestReason.create_main_attr('rawConsumptionRequestReason', kw_only=True) """ - The bundle identifier of an app. - - https://developer.apple.com/documentation/appstoreserverapi/bundleid + The reason the customer requested the refund. + + https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason """ - bundleVersion: Optional[str] = attr.ib(default=None) + rawConsumptionRequestReason: Optional[str] = ConsumptionRequestReason.create_raw_attr('consumptionRequestReason') """ - The version of the build that identifies an iteration of the bundle. - - https://developer.apple.com/documentation/appstoreservernotifications/bundleversion + See consumptionRequestReason """ - signedTransactionInfo: Optional[str] = attr.ib(default=None) + environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', kw_only=True) """ - Transaction information signed by the App Store, in JSON Web Signature (JWS) format. - - https://developer.apple.com/documentation/appstoreserverapi/jwstransaction + The server environment that the notification applies to, either sandbox or production. + + https://developer.apple.com/documentation/appstoreservernotifications/environment """ - signedRenewalInfo: Optional[str] = attr.ib(default=None) + rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') """ - Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. - - https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo + See environment """ - - status: Optional[Status] = Status.create_main_attr('rawStatus') + + signedRenewalInfo: Optional[str] = attr.ib(default=None, kw_only=True) """ - The status of an auto-renewable subscription as of the signedDate in the responseBodyV2DecodedPayload. - - https://developer.apple.com/documentation/appstoreservernotifications/status + Subscription renewal information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/appstoreservernotifications/jwsrenewalinfo """ - rawStatus: Optional[int] = Status.create_raw_attr('status') + signedTransactionInfo: Optional[str] = attr.ib(default=None, kw_only=True) """ - See status + Transaction information signed by the App Store, in JSON Web Signature (JWS) format. + + https://developer.apple.com/documentation/appstoreservernotifications/jwstransaction """ - consumptionRequestReason: Optional[ConsumptionRequestReason] = ConsumptionRequestReason.create_main_attr('rawConsumptionRequestReason') + status: Optional[Status] = Status.create_main_attr('rawStatus', kw_only=True) """ - The reason the customer requested the refund. - - https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason + The status of an auto-renewable subscription as of the signedDate in the responseBodyV2DecodedPayload. + + https://developer.apple.com/documentation/appstoreservernotifications/status """ - rawConsumptionRequestReason: Optional[str] = ConsumptionRequestReason.create_raw_attr('consumptionRequestReason') + rawStatus: Optional[int] = Status.create_raw_attr('status') + """ + See status """ - See consumptionRequestReason - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/DecodedRealtimeRequestBody.py b/appstoreserverlibrary/models/DecodedRealtimeRequestBody.py index 45812b48..8c4fa034 100644 --- a/appstoreserverlibrary/models/DecodedRealtimeRequestBody.py +++ b/appstoreserverlibrary/models/DecodedRealtimeRequestBody.py @@ -1,13 +1,12 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. -from typing import Optional from uuid import UUID from attr import define import attr from .Environment import Environment -from .LibraryUtility import AttrsRawValueAware +from .LibraryUtility import AttrsRawValueAware, validate_not_none @define class DecodedRealtimeRequestBody(AttrsRawValueAware): @@ -17,49 +16,42 @@ class DecodedRealtimeRequestBody(AttrsRawValueAware): https://developer.apple.com/documentation/retentionmessaging/decodedrealtimerequestbody """ - originalTransactionId: str = attr.ib() + originalTransactionId: str = attr.ib(kw_only=True, validator=validate_not_none) """ - The original transaction identifier of the customer's subscription. + The original transaction identifier of the customer’s subscription. https://developer.apple.com/documentation/retentionmessaging/originaltransactionid """ - appAppleId: int = attr.ib() + appAppleId: int = attr.ib(kw_only=True, validator=validate_not_none) """ The unique identifier of the app in the App Store. https://developer.apple.com/documentation/retentionmessaging/appappleid """ - productId: str = attr.ib() + productId: str = attr.ib(kw_only=True, validator=validate_not_none) """ The unique identifier of the auto-renewable subscription. https://developer.apple.com/documentation/retentionmessaging/productid """ - userLocale: str = attr.ib() + userLocale: str = attr.ib(kw_only=True, validator=validate_not_none) """ - The device's locale. + The device’s locale. https://developer.apple.com/documentation/retentionmessaging/locale """ - requestIdentifier: UUID = attr.ib() + requestIdentifier: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ A UUID the App Store server creates to uniquely identify each request. https://developer.apple.com/documentation/retentionmessaging/requestidentifier """ - signedDate: int = attr.ib() - """ - The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data. - - https://developer.apple.com/documentation/retentionmessaging/signeddate - """ - - environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', raw_required=True) + environment: Environment = Environment.create_main_attr('rawEnvironment', raw_required=True, kw_only=True) """ The server environment, either sandbox or production. @@ -71,3 +63,9 @@ class DecodedRealtimeRequestBody(AttrsRawValueAware): See environment """ + signedDate: int = attr.ib(kw_only=True, validator=validate_not_none) + """ + The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data. + + https://developer.apple.com/documentation/retentionmessaging/signeddate + """ diff --git a/appstoreserverlibrary/models/DefaultConfigurationRequest.py b/appstoreserverlibrary/models/DefaultConfigurationRequest.py index 2660163a..dface9a0 100644 --- a/appstoreserverlibrary/models/DefaultConfigurationRequest.py +++ b/appstoreserverlibrary/models/DefaultConfigurationRequest.py @@ -1,11 +1,12 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. -from typing import Optional from uuid import UUID from attr import define import attr +from .LibraryUtility import validate_not_none + @define class DefaultConfigurationRequest: """ @@ -14,11 +15,9 @@ class DefaultConfigurationRequest: https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationrequest """ - messageIdentifier: Optional[UUID] = attr.ib(default=None) + messageIdentifier: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ The message identifier of the message to configure as a default message. - Note: In a future version, this field will become required. - https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ diff --git a/appstoreserverlibrary/models/DefaultConfigurationResponse.py b/appstoreserverlibrary/models/DefaultConfigurationResponse.py index 56d714ea..5ef3a7c8 100644 --- a/appstoreserverlibrary/models/DefaultConfigurationResponse.py +++ b/appstoreserverlibrary/models/DefaultConfigurationResponse.py @@ -5,6 +5,8 @@ from attr import define import attr +from .LibraryUtility import validate_not_none + @define class DefaultConfigurationResponse: """ @@ -13,7 +15,7 @@ class DefaultConfigurationResponse: https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationresponse """ - messageIdentifier: UUID = attr.ib() + messageIdentifier: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ The message identifier of the retention message you configured as a default. diff --git a/appstoreserverlibrary/models/DeliveryStatus.py b/appstoreserverlibrary/models/DeliveryStatus.py index ad29c3c1..ddaf04cb 100644 --- a/appstoreserverlibrary/models/DeliveryStatus.py +++ b/appstoreserverlibrary/models/DeliveryStatus.py @@ -6,12 +6,31 @@ class DeliveryStatus(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - A value that indicates whether the app successfully delivered an in-app purchase that works properly. + A value that indicates whether the app successfully delivered an In-App Purchase that works properly. https://developer.apple.com/documentation/appstoreserverapi/deliverystatus """ DELIVERED = "DELIVERED" + """ + The app delivered the In-App Purchase and it’s working properly. + """ + UNDELIVERED_QUALITY_ISSUE = "UNDELIVERED_QUALITY_ISSUE" + """ + The app didn’t deliver the In-App Purchase due to a quality issue. + """ + UNDELIVERED_WRONG_ITEM = "UNDELIVERED_WRONG_ITEM" + """ + The app delivered the wrong item. + """ + UNDELIVERED_SERVER_OUTAGE = "UNDELIVERED_SERVER_OUTAGE" + """ + The app didn’t deliver the In-App Purchase due to a server outage. + """ + UNDELIVERED_OTHER = "UNDELIVERED_OTHER" + """ + The app didn’t deliver the In-App Purchase for other reasons. + """ diff --git a/appstoreserverlibrary/models/DeliveryStatusV1.py b/appstoreserverlibrary/models/DeliveryStatusV1.py index 8195d101..e5416bee 100644 --- a/appstoreserverlibrary/models/DeliveryStatusV1.py +++ b/appstoreserverlibrary/models/DeliveryStatusV1.py @@ -6,16 +6,39 @@ class DeliveryStatusV1(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ - A value that indicates whether the app successfully delivered an in-app purchase that works properly. + A value that indicates whether the app successfully delivered an In-App Purchase that works properly. - .. deprecated:: + .. deprecated:: 3.0.0 Use :class:`DeliveryStatus` instead. https://developer.apple.com/documentation/appstoreserverapi/deliverystatusv1 """ DELIVERED_AND_WORKING_PROPERLY = 0 + """ + The app delivered the consumable In-App Purchase and it’s working properly. + """ + DID_NOT_DELIVER_DUE_TO_QUALITY_ISSUE = 1 + """ + The app didn’t deliver the consumable In-App Purchase due to a quality issue. + """ + DELIVERED_WRONG_ITEM = 2 + """ + The app delivered the wrong item. + """ + DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE = 3 + """ + The app didn’t deliver the consumable In-App Purchase due to a server outage. + """ + DID_NOT_DELIVER_DUE_TO_IN_GAME_CURRENCY_CHANGE = 4 + """ + The app didn’t deliver the consumable In-App Purchase due to an in-game currency change. + """ + DID_NOT_DELIVER_FOR_OTHER_REASON = 5 + """ + The app didn’t deliver the consumable In-App Purchase for other reasons. + """ diff --git a/appstoreserverlibrary/models/Environment.py b/appstoreserverlibrary/models/Environment.py index f3eb9dc5..4bdf1962 100644 --- a/appstoreserverlibrary/models/Environment.py +++ b/appstoreserverlibrary/models/Environment.py @@ -7,10 +7,18 @@ class Environment(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The server environment, either sandbox or production. - + https://developer.apple.com/documentation/appstoreserverapi/environment """ SANDBOX = "Sandbox" + """ + Indicates that the data applies to testing in the sandbox environment. + """ + PRODUCTION = "Production" + """ + Indicates that the data applies to the production environment. + """ + XCODE = "Xcode" - LOCAL_TESTING = "LocalTesting" # Used for unit testing + LOCAL_TESTING = "LocalTesting" diff --git a/appstoreserverlibrary/models/ExpirationIntent.py b/appstoreserverlibrary/models/ExpirationIntent.py index 120082f8..b842007e 100644 --- a/appstoreserverlibrary/models/ExpirationIntent.py +++ b/appstoreserverlibrary/models/ExpirationIntent.py @@ -7,11 +7,30 @@ class ExpirationIntent(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The reason an auto-renewable subscription expired. - + https://developer.apple.com/documentation/appstoreserverapi/expirationintent """ CUSTOMER_CANCELLED = 1 + """ + The customer canceled their subscription. + """ + BILLING_ERROR = 2 + """ + Billing error; for example, the customer’s payment information is no longer valid. + """ + CUSTOMER_DID_NOT_CONSENT_TO_PRICE_INCREASE = 3 + """ + The customer didn’t consent to an auto-renewable subscription price increase that requires their consent, or to a subscription offer conversion that requires their consent, so the subscription expired. + """ + PRODUCT_NOT_AVAILABLE = 4 + """ + The product wasn’t available for purchase at the time of renewal. + """ + OTHER = 5 + """ + The subscription expired for some other reason. + """ diff --git a/appstoreserverlibrary/models/ExtendReasonCode.py b/appstoreserverlibrary/models/ExtendReasonCode.py index 4143f9ed..8677fd61 100644 --- a/appstoreserverlibrary/models/ExtendReasonCode.py +++ b/appstoreserverlibrary/models/ExtendReasonCode.py @@ -7,10 +7,25 @@ class ExtendReasonCode(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The code that represents the reason for the subscription-renewal-date extension. - + https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode """ UNDECLARED = 0 + """ + Undeclared; no information provided. + """ + CUSTOMER_SATISFACTION = 1 + """ + The renewal-date extension is for customer satisfaction. + """ + OTHER = 2 + """ + The renewal-date extension is for other reasons. + """ + SERVICE_ISSUE_OR_OUTAGE = 3 + """ + The renewal-date extension is due to a service issue or outage. + """ diff --git a/appstoreserverlibrary/models/ExtendRenewalDateRequest.py b/appstoreserverlibrary/models/ExtendRenewalDateRequest.py index c59c952a..15477938 100644 --- a/appstoreserverlibrary/models/ExtendRenewalDateRequest.py +++ b/appstoreserverlibrary/models/ExtendRenewalDateRequest.py @@ -1,37 +1,41 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. -from typing import Optional from attr import define import attr from .ExtendReasonCode import ExtendReasonCode +from .LibraryUtility import AttrsRawValueAware, validate_not_none @define -class ExtendRenewalDateRequest: +class ExtendRenewalDateRequest(AttrsRawValueAware): """ The request body that contains subscription-renewal-extension data for an individual subscription. - + https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldaterequest """ - extendByDays: Optional[int] = attr.ib(default=None) + extendByDays: int = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.and_(attr.validators.ge(1), attr.validators.le(90))]) """ The number of days to extend the subscription renewal date. https://developer.apple.com/documentation/appstoreserverapi/extendbydays - maximum: 90 """ - extendReasonCode: Optional[ExtendReasonCode] = attr.ib(default=None) + extendReasonCode: ExtendReasonCode = ExtendReasonCode.create_main_attr('rawExtendReasonCode', raw_required=True, kw_only=True) """ - The reason code for the subscription date extension - + The reason code for the subscription date extension. + https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode """ - requestIdentifier: Optional[str] = attr.ib(default=None) + rawExtendReasonCode: int = ExtendReasonCode.create_raw_attr('extendReasonCode', required=True) """ - A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. - + See extendReasonCode + """ + + requestIdentifier: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(128)]) + """ + A string that contains a value you provide to uniquely identify this renewal-date extension request. + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/ExtendRenewalDateResponse.py b/appstoreserverlibrary/models/ExtendRenewalDateResponse.py index f4c0ccf2..1f84a269 100644 --- a/appstoreserverlibrary/models/ExtendRenewalDateResponse.py +++ b/appstoreserverlibrary/models/ExtendRenewalDateResponse.py @@ -1,41 +1,42 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define import attr @define -class ExtendRenewalDateResponse: +class ExtendRenewalDateResponse: """ A response that indicates whether an individual renewal-date extension succeeded, and related details. - + https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldateresponse """ - originalTransactionId: Optional[str] = attr.ib(default=None) + effectiveDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The original transaction identifier of a purchase. - - https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + The new subscription expiration date of a successful subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/effectivedate """ - webOrderLineItemId: Optional[str] = attr.ib(default=None) + originalTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The unique identifier of subscription-purchase events across devices, including renewals. - - https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid + The original transaction identifier of the subscription that experienced a service interruption. + + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid """ - success: Optional[bool] = attr.ib(default=None) + success: Optional[bool] = attr.ib(default=None, kw_only=True) """ A Boolean value that indicates whether the subscription-renewal-date extension succeeded. - + https://developer.apple.com/documentation/appstoreserverapi/success """ - effectiveDate: Optional[int] = attr.ib(default=None) + webOrderLineItemId: Optional[str] = attr.ib(default=None, kw_only=True) + """ + A unique ID that identifies subscription-purchase events, including subscription renewals, across devices. + + https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid """ - The new subscription expiration date for a subscription-renewal extension. - - https://developer.apple.com/documentation/appstoreserverapi/effectivedate - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/ExternalPurchaseToken.py b/appstoreserverlibrary/models/ExternalPurchaseToken.py index 23a67fac..933e0797 100644 --- a/appstoreserverlibrary/models/ExternalPurchaseToken.py +++ b/appstoreserverlibrary/models/ExternalPurchaseToken.py @@ -1,52 +1,60 @@ # Copyright (c) 2024 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define import attr -from .LibraryUtility import AttrsRawValueAware +from .LibraryUtility import AttrsRawValueAware, validate_not_none from .TokenType import TokenType @define class ExternalPurchaseToken(AttrsRawValueAware): """ The payload data that contains an external purchase token. - + https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken """ - externalPurchaseId: Optional[str] = attr.ib(default=None) + externalPurchaseId: str = attr.ib(kw_only=True, validator=validate_not_none) """ - The field of an external purchase token that uniquely identifies the token. - + The unique identifier of the token. + https://developer.apple.com/documentation/appstoreservernotifications/externalpurchaseid """ - tokenCreationDate: Optional[int] = attr.ib(default=None) + tokenCreationDate: int = attr.ib(kw_only=True, validator=validate_not_none) """ - The field of an external purchase token that contains the UNIX date, in milliseconds, when the system created the token. - + The UNIX time, in milliseconds, when the system created the token. + https://developer.apple.com/documentation/appstoreservernotifications/tokencreationdate """ - appAppleId: Optional[int] = attr.ib(default=None) + appAppleId: int = attr.ib(kw_only=True, validator=validate_not_none) """ - The unique identifier of an app in the App Store. - + The app Apple ID for which the system generated the token. + https://developer.apple.com/documentation/appstoreservernotifications/appappleid """ - bundleId: Optional[str] = attr.ib(default=None) + bundleId: str = attr.ib(kw_only=True, validator=validate_not_none) """ - The bundle identifier of an app. - + The bundle ID of the app for which the system generated the token. + https://developer.apple.com/documentation/appstoreservernotifications/bundleid """ - tokenType: Optional[TokenType] = TokenType.create_main_attr('rawTokenType') + tokenExpirationDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The type of an external purchase custom link token. - + The UNIX time, in milliseconds, when a token expires. + + https://developer.apple.com/documentation/appstoreservernotifications/tokenexpirationdate + """ + + tokenType: Optional[TokenType] = TokenType.create_main_attr('rawTokenType', kw_only=True) + """ + The custom link token type, either SERVICES or ACQUISITION. + https://developer.apple.com/documentation/appstoreservernotifications/tokentype """ @@ -54,10 +62,3 @@ class ExternalPurchaseToken(AttrsRawValueAware): """ See tokenType """ - - tokenExpirationDate: Optional[int] = attr.ib(default=None) - """ - The field of a custom link token that contains the UNIX date, in milliseconds, when the token expires. - - https://developer.apple.com/documentation/appstoreservernotifications/tokenexpirationdate - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/FirstSendAttemptResult.py b/appstoreserverlibrary/models/FirstSendAttemptResult.py deleted file mode 100644 index 6c2e1f5f..00000000 --- a/appstoreserverlibrary/models/FirstSendAttemptResult.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) 2023 Apple Inc. Licensed under MIT License. - -from enum import Enum, unique - -from .LibraryUtility import AppStoreServerLibraryEnumMeta - -@unique -class FirstSendAttemptResult(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): - """ - An error or result that the App Store server receives when attempting to send an App Store server notification to your server. - - https://developer.apple.com/documentation/appstoreserverapi/firstsendattemptresult - """ - SUCCESS = "SUCCESS" - TIMED_OUT = "TIMED_OUT" - TLS_ISSUE = "TLS_ISSUE" - CIRCULAR_REDIRECT = "CIRCULAR_REDIRECT" - NO_RESPONSE = "NO_RESPONSE" - SOCKET_ISSUE = "SOCKET_ISSUE" - UNSUPPORTED_CHARSET = "UNSUPPORTED_CHARSET" - INVALID_RESPONSE = "INVALID_RESPONSE" - PREMATURE_CLOSE = "PREMATURE_CLOSE" - UNSUCCESSFUL_HTTP_RESPONSE_CODE = "UNSUCCESSFUL_HTTP_RESPONSE_CODE" - OTHER = "OTHER" diff --git a/appstoreserverlibrary/models/GetCustomerGroupsResponse.py b/appstoreserverlibrary/models/GetCustomerGroupsResponse.py new file mode 100644 index 00000000..c798b8c8 --- /dev/null +++ b/appstoreserverlibrary/models/GetCustomerGroupsResponse.py @@ -0,0 +1,23 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional, List + +from attr import define +import attr + +from .GroupEntry import GroupEntry + +@define +class GetCustomerGroupsResponse: + """ + A response that contains the groups a customer belongs to, and their role in each group. + + https://developer.apple.com/documentation/appstoreserverapi/getcustomergroupsresponse + """ + + groups: Optional[List[GroupEntry]] = attr.ib(default=None, kw_only=True) + """ + An array of groups that the customer belongs to. + + https://developer.apple.com/documentation/appstoreserverapi/groupentry + """ diff --git a/appstoreserverlibrary/models/GetGroupMembersResponse.py b/appstoreserverlibrary/models/GetGroupMembersResponse.py new file mode 100644 index 00000000..39e0c860 --- /dev/null +++ b/appstoreserverlibrary/models/GetGroupMembersResponse.py @@ -0,0 +1,37 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional, List + +from attr import define +import attr + +from .GroupMemberEntry import GroupMemberEntry + +@define +class GetGroupMembersResponse: + """ + A response that contains a page of the customers that belong to a group. + + https://developer.apple.com/documentation/appstoreserverapi/getgroupmembersresponse + """ + + members: Optional[List[GroupMemberEntry]] = attr.ib(default=None, kw_only=True) + """ + An array of the customers that belong to the group. + + https://developer.apple.com/documentation/appstoreserverapi/groupmemberentry + """ + + paginationToken: Optional[str] = attr.ib(default=None, kw_only=True) + """ + A token you send in a subsequent request to get the next set of members. + + https://developer.apple.com/documentation/appstoreserverapi/paginationtoken + """ + + hasMore: Optional[bool] = attr.ib(default=None, kw_only=True) + """ + A Boolean value that indicates whether the group has more members than the response contains. + + https://developer.apple.com/documentation/appstoreserverapi/hasmore + """ diff --git a/appstoreserverlibrary/models/GetImageListResponse.py b/appstoreserverlibrary/models/GetImageListResponse.py index 6eb2747f..b4a18443 100644 --- a/appstoreserverlibrary/models/GetImageListResponse.py +++ b/appstoreserverlibrary/models/GetImageListResponse.py @@ -15,7 +15,7 @@ class GetImageListResponse: https://developer.apple.com/documentation/retentionmessaging/getimagelistresponse """ - imageIdentifiers: Optional[List[GetImageListResponseItem]] = attr.ib(default=None) + imageIdentifiers: Optional[List[GetImageListResponseItem]] = attr.ib(default=None, kw_only=True) """ An array of all image identifiers and their image state. diff --git a/appstoreserverlibrary/models/GetImageListResponseItem.py b/appstoreserverlibrary/models/GetImageListResponseItem.py index 4c582a3a..d664e63a 100644 --- a/appstoreserverlibrary/models/GetImageListResponseItem.py +++ b/appstoreserverlibrary/models/GetImageListResponseItem.py @@ -18,33 +18,33 @@ class GetImageListResponseItem(AttrsRawValueAware): https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem """ - imageIdentifier: Optional[UUID] = attr.ib(default=None) + imageIdentifier: Optional[UUID] = attr.ib(default=None, kw_only=True) """ The identifier of the image. https://developer.apple.com/documentation/retentionmessaging/imageidentifier """ - imageState: Optional[ImageState] = ImageState.create_main_attr('rawImageState') + imageSize: Optional[ImageSize] = ImageSize.create_main_attr('rawImageSize', kw_only=True) """ - The current state of the image. + The size of the image. - https://developer.apple.com/documentation/retentionmessaging/imagestate + https://developer.apple.com/documentation/retentionmessaging/imagesize """ - rawImageState: Optional[str] = ImageState.create_raw_attr('imageState') + rawImageSize: Optional[str] = ImageSize.create_raw_attr('imageSize') """ - See imageState + See imageSize """ - imageSize: Optional[ImageSize] = ImageSize.create_main_attr('rawImageSize') + imageState: Optional[ImageState] = ImageState.create_main_attr('rawImageState', kw_only=True) """ - The size of the image. + The current state of the image. - https://developer.apple.com/documentation/retentionmessaging/imagesize + https://developer.apple.com/documentation/retentionmessaging/imagestate """ - rawImageSize: Optional[str] = ImageSize.create_raw_attr('imageSize') + rawImageState: Optional[str] = ImageState.create_raw_attr('imageState') + """ + See imageState """ - See imageSize - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/GetMessageListResponse.py b/appstoreserverlibrary/models/GetMessageListResponse.py index 0fdbd037..65dc337c 100644 --- a/appstoreserverlibrary/models/GetMessageListResponse.py +++ b/appstoreserverlibrary/models/GetMessageListResponse.py @@ -15,9 +15,9 @@ class GetMessageListResponse: https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponse """ - messageIdentifiers: Optional[List[GetMessageListResponseItem]] = attr.ib(default=None) + messageIdentifiers: Optional[List[GetMessageListResponseItem]] = attr.ib(default=None, kw_only=True) """ - An array of all message identifiers and their message state. + An array of all message identifiers and their message states. https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem """ diff --git a/appstoreserverlibrary/models/GetMessageListResponseItem.py b/appstoreserverlibrary/models/GetMessageListResponseItem.py index af810eeb..a02e55ea 100644 --- a/appstoreserverlibrary/models/GetMessageListResponseItem.py +++ b/appstoreserverlibrary/models/GetMessageListResponseItem.py @@ -6,8 +6,8 @@ from attr import define import attr -from .MessageState import MessageState from .LibraryUtility import AttrsRawValueAware +from .MessageState import MessageState @define class GetMessageListResponseItem(AttrsRawValueAware): @@ -17,21 +17,21 @@ class GetMessageListResponseItem(AttrsRawValueAware): https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem """ - messageIdentifier: Optional[UUID] = attr.ib(default=None) + messageIdentifier: Optional[UUID] = attr.ib(default=None, kw_only=True) """ The identifier of the message. https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ - messageState: Optional[MessageState] = MessageState.create_main_attr('rawMessageState') + messageState: Optional[MessageState] = MessageState.create_main_attr('rawMessageState', kw_only=True) """ The current state of the message. - https://developer.apple.com/documentation/retentionmessaging/messageState + https://developer.apple.com/documentation/retentionmessaging/messagestate """ rawMessageState: Optional[str] = MessageState.create_raw_attr('messageState') """ See messageState - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/GroupEntry.py b/appstoreserverlibrary/models/GroupEntry.py new file mode 100644 index 00000000..ee29444e --- /dev/null +++ b/appstoreserverlibrary/models/GroupEntry.py @@ -0,0 +1,44 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional, List + +from attr import define +import attr + +from .GroupType import GroupType +from .LibraryUtility import AttrsRawValueAware +from .RoleEntry import RoleEntry + +@define +class GroupEntry(AttrsRawValueAware): + """ + The identifier, type, and per-product roles for a group that a customer belongs to. + + https://developer.apple.com/documentation/appstoreserverapi/groupentry + """ + + groupId: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The identifier of the group. + + https://developer.apple.com/documentation/appstoreserverapi/groupid + """ + + groupType: Optional[GroupType] = GroupType.create_main_attr('rawGroupType', kw_only=True) + """ + The type of the group. + + https://developer.apple.com/documentation/appstoreserverapi/grouptype + """ + + rawGroupType: Optional[str] = GroupType.create_raw_attr('groupType') + """ + See groupType + """ + + roles: Optional[List[RoleEntry]] = attr.ib(default=None, kw_only=True) + """ + An optional array of the customer’s roles in this group, one for each of your products that the group provides access to. + + https://developer.apple.com/documentation/appstoreserverapi/roleentry + """ diff --git a/appstoreserverlibrary/models/GroupMemberEntry.py b/appstoreserverlibrary/models/GroupMemberEntry.py new file mode 100644 index 00000000..0f762de5 --- /dev/null +++ b/appstoreserverlibrary/models/GroupMemberEntry.py @@ -0,0 +1,21 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional + +from attr import define +import attr + +@define +class GroupMemberEntry: + """ + A customer that belongs to a group. + + https://developer.apple.com/documentation/appstoreserverapi/groupmemberentry + """ + + appTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The unique identifier of the member’s app download transaction. + + https://developer.apple.com/documentation/appstoreserverapi/apptransactionid + """ diff --git a/appstoreserverlibrary/models/GroupType.py b/appstoreserverlibrary/models/GroupType.py new file mode 100644 index 00000000..83d94325 --- /dev/null +++ b/appstoreserverlibrary/models/GroupType.py @@ -0,0 +1,21 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from enum import Enum + +from .LibraryUtility import AppStoreServerLibraryEnumMeta + +class GroupType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): + """ + A string that describes the kind of multiseat purchase a customer’s access comes from. + + https://developer.apple.com/documentation/appstoreserverapi/grouptype + """ + ORGANIZATION = "ORGANIZATION" + """ + An organization bought seats through Volume Purchasing and assigned one to the customer. + """ + + CONSUMER = "CONSUMER" + """ + A subscriber bought seats through Group Purchases and invited the customer to join. + """ diff --git a/appstoreserverlibrary/models/HeaderPosition.py b/appstoreserverlibrary/models/HeaderPosition.py index 7c9519c7..4ac43ab2 100644 --- a/appstoreserverlibrary/models/HeaderPosition.py +++ b/appstoreserverlibrary/models/HeaderPosition.py @@ -11,4 +11,11 @@ class HeaderPosition(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): https://developer.apple.com/documentation/retentionmessaging/headerposition """ ABOVE_BODY = "ABOVE_BODY" + """ + Position the header text above the message body. + """ + ABOVE_IMAGE = "ABOVE_IMAGE" + """ + Position the header text above the image. + """ diff --git a/appstoreserverlibrary/models/HelperValidationUtils.py b/appstoreserverlibrary/models/HelperValidationUtils.py index 3469fbad..360cde8c 100644 --- a/appstoreserverlibrary/models/HelperValidationUtils.py +++ b/appstoreserverlibrary/models/HelperValidationUtils.py @@ -1,9 +1,5 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import TypeVar - -T = TypeVar('T') - class HelperValidationUtils: MAXIMUM_DESCRIPTION_LENGTH = 45 @@ -18,8 +14,10 @@ def description_validator(instance, attribute, value): Validates description is not None and does not exceed maximum length. Raises: - ValueError: If description exceeds maximum length + ValueError: If description is None or exceeds maximum length """ + if value is None: + raise ValueError("Description cannot be None") if len(value) > HelperValidationUtils.MAXIMUM_DESCRIPTION_LENGTH: raise ValueError( f"Description length cannot exceed " @@ -32,8 +30,10 @@ def display_name_validator(instance, attribute, value): Validates display name is not None and does not exceed maximum length. Raises: - ValueError: If display name exceeds maximum length + ValueError: If display name is None or exceeds maximum length """ + if value is None: + raise ValueError("Display name cannot be None") if len(value) > HelperValidationUtils.MAXIMUM_DISPLAY_NAME_LENGTH: raise ValueError( f"Display name length cannot exceed " @@ -43,11 +43,13 @@ def display_name_validator(instance, attribute, value): @staticmethod def sku_validator(instance, attribute, value): """ - Validates SKU does not exceed maximum length. + Validates SKU is not None and does not exceed maximum length. Raises: - ValueError: If SKU exceeds maximum length + ValueError: If SKU is None or exceeds maximum length """ + if value is None: + raise ValueError("SKU cannot be None") if len(value) > HelperValidationUtils.MAXIMUM_SKU_LENGTH: raise ValueError( f"SKU length cannot exceed " @@ -60,8 +62,10 @@ def period_count_validator(instance, attribute, value): Validates periodCount is not None and between `MIN_PERIOD` and `MAX_PERIOD` inclusive. Raises: - ValueError: If period_count is out of range + ValueError: If period_count is None or out of range """ + if value is None: + raise ValueError("Period count cannot be None") if (value < HelperValidationUtils.MIN_PERIOD or value > HelperValidationUtils.MAX_PERIOD): raise ValueError( @@ -88,12 +92,14 @@ def items_validator(instance, attribute, value): @staticmethod def dependent_skus_validator(instance, attribute, value): """ - Validates that each SKU in the dependentSKUs list does not exceed maximum length. + Validates that each SKU in the dependentSKUs list is not None and does not exceed maximum length. Raises: - ValueError: If any SKU exceeds maximum length + ValueError: If any SKU is None or exceeds maximum length """ - for sku in value: + for i, sku in enumerate(value): + if sku is None: + raise ValueError(f"Item at index {i} in the list cannot be None") if len(sku) > HelperValidationUtils.MAXIMUM_SKU_LENGTH: raise ValueError( f"SKU length cannot exceed " diff --git a/appstoreserverlibrary/models/HistoryResponse.py b/appstoreserverlibrary/models/HistoryResponse.py index 429356c6..4f09c71a 100644 --- a/appstoreserverlibrary/models/HistoryResponse.py +++ b/appstoreserverlibrary/models/HistoryResponse.py @@ -1,7 +1,8 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List + from attr import define -from typing import List, Optional import attr from .Environment import Environment @@ -10,43 +11,29 @@ @define class HistoryResponse(AttrsRawValueAware): """ - A response that contains the customer's transaction history for an app. - + A response that contains the customer’s transaction history for an app. + https://developer.apple.com/documentation/appstoreserverapi/historyresponse """ - revision: Optional[str] = attr.ib(default=None) - """ - A token you use in a query to request the next set of transactions for the customer. - - https://developer.apple.com/documentation/appstoreserverapi/revision + appAppleId: Optional[int] = attr.ib(default=None, kw_only=True) """ + The app’s identifier in the App Store. - hasMore: Optional[bool] = attr.ib(default=None) - """ - A Boolean value indicating whether the App Store has more transaction data. - - https://developer.apple.com/documentation/appstoreserverapi/hasmore + https://developer.apple.com/documentation/appstoreserverapi/appappleid """ - bundleId: Optional[str] = attr.ib(default=None) + bundleId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The bundle identifier of an app. - + The bundle identifier of the app. + https://developer.apple.com/documentation/appstoreserverapi/bundleid """ - appAppleId: Optional[int] = attr.ib(default=None) - """ - The unique identifier of an app in the App Store. - - https://developer.apple.com/documentation/appstoreservernotifications/appappleid + environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', kw_only=True) """ + The server environment in which you’re making the request, whether sandbox or production. - environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') - """ - The server environment in which you're making the request, whether sandbox or production. - https://developer.apple.com/documentation/appstoreserverapi/environment """ @@ -55,9 +42,23 @@ class HistoryResponse(AttrsRawValueAware): See environment """ - signedTransactions: Optional[List[str]] = attr.ib(default=None) + hasMore: Optional[bool] = attr.ib(default=None, kw_only=True) """ - An array of in-app purchase transactions for the customer, signed by Apple, in JSON Web Signature format. - + A Boolean value that indicates whether the App Store has more transactions than it returns in this response. + + https://developer.apple.com/documentation/appstoreserverapi/hasmore + """ + + revision: Optional[str] = attr.ib(default=None, kw_only=True) + """ + A token you use in a query to request the next set of transactions from the endpoint. + + https://developer.apple.com/documentation/appstoreserverapi/revision + """ + + signedTransactions: Optional[List[str]] = attr.ib(default=None, kw_only=True) + """ + An array of in-app purchase transactions for the customer, signed by Apple, in JSON Web Signature (JWS) format. + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/ImageSize.py b/appstoreserverlibrary/models/ImageSize.py index 1669d3e6..b657b3d0 100644 --- a/appstoreserverlibrary/models/ImageSize.py +++ b/appstoreserverlibrary/models/ImageSize.py @@ -11,4 +11,11 @@ class ImageSize(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): https://developer.apple.com/documentation/retentionmessaging/imagesize """ FULL_SIZE = "FULL_SIZE" + """ + 3840 pixels in width, and from 160 to 2160 pixels in height, inclusive. + """ + BULLET_POINT = "BULLET_POINT" + """ + 1024 by 1024 pixels, to use for bullet points only. + """ diff --git a/appstoreserverlibrary/models/ImageState.py b/appstoreserverlibrary/models/ImageState.py index 9557b25f..4d705546 100644 --- a/appstoreserverlibrary/models/ImageState.py +++ b/appstoreserverlibrary/models/ImageState.py @@ -11,5 +11,16 @@ class ImageState(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): https://developer.apple.com/documentation/retentionmessaging/imagestate """ PENDING = "PENDING" + """ + The image is awaiting approval. + """ + APPROVED = "APPROVED" + """ + The image is approved. + """ + REJECTED = "REJECTED" + """ + The image is rejected. + """ diff --git a/appstoreserverlibrary/models/InAppOwnershipType.py b/appstoreserverlibrary/models/InAppOwnershipType.py index b7d7cd37..dfff9ca1 100644 --- a/appstoreserverlibrary/models/InAppOwnershipType.py +++ b/appstoreserverlibrary/models/InAppOwnershipType.py @@ -6,9 +6,21 @@ class InAppOwnershipType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - The relationship of the user with the family-shared purchase to which they have access. - + A string that describes whether the transaction was purchased by the customer, or is available to them through Family Sharing, an organization, or a group. + https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype """ FAMILY_SHARED = "FAMILY_SHARED" + """ + The transaction belongs to a family member who benefits from service. + """ + PURCHASED = "PURCHASED" + """ + The transaction belongs to the purchaser. + """ + + ASSIGNED = "ASSIGNED" + """ + The transaction belongs to a customer who has access through an organization or group. + """ diff --git a/appstoreserverlibrary/models/JWSRenewalInfoDecodedPayload.py b/appstoreserverlibrary/models/JWSRenewalInfoDecodedPayload.py index a9215bab..b3af9181 100644 --- a/appstoreserverlibrary/models/JWSRenewalInfoDecodedPayload.py +++ b/appstoreserverlibrary/models/JWSRenewalInfoDecodedPayload.py @@ -1,65 +1,55 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. -from typing import List, Optional + +from typing import Optional, List +from uuid import UUID from attr import define import attr + +from .AdvancedCommerceRenewalInfo import AdvancedCommerceRenewalInfo from .AutoRenewStatus import AutoRenewStatus from .Environment import Environment - from .ExpirationIntent import ExpirationIntent from .LibraryUtility import AttrsRawValueAware +from .OfferDiscountType import OfferDiscountType from .OfferType import OfferType from .PriceIncreaseStatus import PriceIncreaseStatus -from .OfferDiscountType import OfferDiscountType from .RenewalBillingPlanType import RenewalBillingPlanType -from .AdvancedCommerceRenewalInfo import AdvancedCommerceRenewalInfo from .RenewalCommitmentInfo import RenewalCommitmentInfo @define class JWSRenewalInfoDecodedPayload(AttrsRawValueAware): """ A decoded payload containing subscription renewal information for an auto-renewable subscription. - + https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload """ - expirationIntent: Optional[ExpirationIntent] = ExpirationIntent.create_main_attr('rawExpirationIntent') - """ - The reason the subscription expired. - - https://developer.apple.com/documentation/appstoreserverapi/expirationintent + appAccountToken: Optional[UUID] = attr.ib(default=None, kw_only=True) """ + A UUID you create at the time of purchase that associates the transaction with a customer on your own service. - rawExpirationIntent: Optional[int] = ExpirationIntent.create_raw_attr('expirationIntent') - """ - See expirationIntent + https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken """ - originalTransactionId: Optional[str] = attr.ib(default=None) - """ - The original transaction identifier of a purchase. - - https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + appTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) """ + The unique identifier of the app download transaction. - autoRenewProductId: Optional[str] = attr.ib(default=None) - """ - The product identifier of the product that will renew at the next billing period. - - https://developer.apple.com/documentation/appstoreserverapi/autorenewproductid + https://developer.apple.com/documentation/appstoreserverapi/apptransactionid """ - productId: Optional[str] = attr.ib(default=None) + autoRenewProductId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The unique identifier for the product, that you create in App Store Connect. - - https://developer.apple.com/documentation/appstoreserverapi/productid + The identifier of the product that renews at the next billing period. + + https://developer.apple.com/documentation/appstoreserverapi/autorenewproductid """ - autoRenewStatus: Optional[AutoRenewStatus] = AutoRenewStatus.create_main_attr('rawAutoRenewStatus') + autoRenewStatus: Optional[AutoRenewStatus] = AutoRenewStatus.create_main_attr('rawAutoRenewStatus', kw_only=True) """ The renewal status of the auto-renewable subscription. - + https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus """ @@ -68,151 +58,163 @@ class JWSRenewalInfoDecodedPayload(AttrsRawValueAware): See autoRenewStatus """ - isInBillingRetryPeriod: Optional[bool] = attr.ib(default=None) + currency: Optional[str] = attr.ib(default=None, kw_only=True) """ - A Boolean value that indicates whether the App Store is attempting to automatically renew an expired subscription. - - https://developer.apple.com/documentation/appstoreserverapi/isinbillingretryperiod + The currency code for the renewalPrice of the subscription. + + https://developer.apple.com/documentation/appstoreserverapi/currency """ - priceIncreaseStatus: Optional[PriceIncreaseStatus] = PriceIncreaseStatus.create_main_attr('rawPriceIncreaseStatus') + eligibleWinBackOfferIds: Optional[List[str]] = attr.ib(default=None, kw_only=True) """ - The status that indicates whether the auto-renewable subscription is subject to a price increase. - - https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus + The list of win-back offer IDs that the customer is eligible for. + + https://developer.apple.com/documentation/appstoreserverapi/eligiblewinbackofferids """ - rawPriceIncreaseStatus: Optional[int] = PriceIncreaseStatus.create_raw_attr('priceIncreaseStatus') + environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', kw_only=True) """ - See priceIncreaseStatus + The server environment, either sandbox or production. + + https://developer.apple.com/documentation/appstoreserverapi/environment """ - gracePeriodExpiresDate: Optional[int] = attr.ib(default=None) + rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') """ - The time when the billing grace period for subscription renewals expires. - - https://developer.apple.com/documentation/appstoreserverapi/graceperiodexpiresdate + See environment """ - offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType') + expirationIntent: Optional[ExpirationIntent] = ExpirationIntent.create_main_attr('rawExpirationIntent', kw_only=True) """ - The type of subscription offer. - - https://developer.apple.com/documentation/appstoreserverapi/offertype + The reason the subscription expired. + + https://developer.apple.com/documentation/appstoreserverapi/expirationintent """ - rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType') + rawExpirationIntent: Optional[int] = ExpirationIntent.create_raw_attr('expirationIntent') """ - See offerType + See expirationIntent """ - offerIdentifier: Optional[str] = attr.ib(default=None) + gracePeriodExpiresDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The offer code or the promotional offer identifier. - - https://developer.apple.com/documentation/appstoreserverapi/offeridentifier + The time when the Billing Grace Period for subscription renewals expires. + + https://developer.apple.com/documentation/appstoreserverapi/graceperiodexpiresdate """ - signedDate: Optional[int] = attr.ib(default=None) + isInBillingRetryPeriod: Optional[bool] = attr.ib(default=None, kw_only=True) """ - The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. - - https://developer.apple.com/documentation/appstoreserverapi/signeddate + A Boolean value that indicates whether the App Store is attempting to automatically renew the expired subscription. + + https://developer.apple.com/documentation/appstoreserverapi/isinbillingretryperiod """ - environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') + offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType', kw_only=True) """ - The server environment, either sandbox or production. - - https://developer.apple.com/documentation/appstoreserverapi/environment + The payment mode you configure for the offer. + + https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype """ - rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') + rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType') """ - See environment + See offerDiscountType """ - recentSubscriptionStartDate: Optional[int] = attr.ib(default=None) + offerIdentifier: Optional[str] = attr.ib(default=None, kw_only=True) """ - The earliest start date of a subscription in a series of auto-renewable subscription purchases that ignores all lapses of paid service shorter than 60 days. - - https://developer.apple.com/documentation/appstoreserverapi/recentsubscriptionstartdate + The offer code or the promotional offer identifier. + + https://developer.apple.com/documentation/appstoreserverapi/offeridentifier """ - renewalDate: Optional[int] = attr.ib(default=None) + offerPeriod: Optional[str] = attr.ib(default=None, kw_only=True) """ - The UNIX time, in milliseconds, that the most recent auto-renewable subscription purchase expires. - - https://developer.apple.com/documentation/appstoreserverapi/renewaldate + The duration of the offer. + + https://developer.apple.com/documentation/appstoreserverapi/offerperiod """ - currency: Optional[str] = attr.ib(default=None) + offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType', kw_only=True) """ - The currency code for the renewalPrice of the subscription. - - https://developer.apple.com/documentation/appstoreserverapi/currency + The type of subscription offer. + + https://developer.apple.com/documentation/appstoreserverapi/offertype """ - renewalPrice: Optional[int] = attr.ib(default=None) + rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType') """ - The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period. - - https://developer.apple.com/documentation/appstoreserverapi/renewalprice + See offerType """ - offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType') + originalTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The payment mode you configure for the offer. - - https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype + The transaction identifier of the original purchase associated with this transaction. + + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid """ - rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType') + priceIncreaseStatus: Optional[PriceIncreaseStatus] = PriceIncreaseStatus.create_main_attr('rawPriceIncreaseStatus', kw_only=True) """ - See offerDiscountType + The status that indicates whether the auto-renewable subscription is subject to a price increase. + + https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus """ - eligibleWinBackOfferIds: Optional[List[str]] = attr.ib(default=None) + rawPriceIncreaseStatus: Optional[int] = PriceIncreaseStatus.create_raw_attr('priceIncreaseStatus') """ - An array of win-back offer identifiers that a customer is eligible to redeem, which sorts the identifiers to present the better offers first. - - https://developer.apple.com/documentation/appstoreserverapi/eligiblewinbackofferids + See priceIncreaseStatus """ - appAccountToken: Optional[str] = attr.ib(default=None) + productId: Optional[str] = attr.ib(default=None, kw_only=True) """ - 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 + The product identifier of the In-App Purchase. + + https://developer.apple.com/documentation/appstoreserverapi/productid """ - appTransactionId: Optional[str] = attr.ib(default=None) + recentSubscriptionStartDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The unique identifier of the app download transaction. - - https://developer.apple.com/documentation/appstoreserverapi/appTransactionId + The earliest start date of the auto-renewable subscription in a series of subscription purchases that ignores all lapses of paid service that are 60 days or fewer. + + https://developer.apple.com/documentation/appstoreserverapi/recentsubscriptionstartdate """ - offerPeriod: Optional[str] = attr.ib(default=None) + renewalDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The duration of the offer. + The UNIX time, in milliseconds, when the most recent auto-renewable subscription purchase expires. + + https://developer.apple.com/documentation/appstoreserverapi/renewaldate + """ + + renewalPrice: Optional[int] = attr.ib(default=None, kw_only=True) + """ + The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period. - https://developer.apple.com/documentation/appstoreserverapi/offerPeriod + https://developer.apple.com/documentation/appstoreserverapi/renewalprice + """ + + signedDate: Optional[int] = attr.ib(default=None, kw_only=True) """ + The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data. - advancedCommerceInfo: Optional[AdvancedCommerceRenewalInfo] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreserverapi/signeddate + """ + + advancedCommerceInfo: Optional[AdvancedCommerceRenewalInfo] = attr.ib(default=None, kw_only=True) """ Renewal information that is present only for Advanced Commerce SKUs. https://developer.apple.com/documentation/appstoreserverapi/advancedcommercerenewalinfo """ - commitmentInfo: Optional[RenewalCommitmentInfo] = attr.ib(default=None) + commitmentInfo: Optional[RenewalCommitmentInfo] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/renewalcommitmentinfo """ - renewalBillingPlanType: Optional[RenewalBillingPlanType] = RenewalBillingPlanType.create_main_attr('rawRenewalBillingPlanType') + renewalBillingPlanType: Optional[RenewalBillingPlanType] = RenewalBillingPlanType.create_main_attr('rawRenewalBillingPlanType', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/renewalbillingplantype """ @@ -220,4 +222,4 @@ class JWSRenewalInfoDecodedPayload(AttrsRawValueAware): rawRenewalBillingPlanType: Optional[str] = RenewalBillingPlanType.create_raw_attr('renewalBillingPlanType') """ See renewalBillingPlanType - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/JWSTransactionDecodedPayload.py b/appstoreserverlibrary/models/JWSTransactionDecodedPayload.py index 6ccc3e58..1a10fc1a 100644 --- a/appstoreserverlibrary/models/JWSTransactionDecodedPayload.py +++ b/appstoreserverlibrary/models/JWSTransactionDecodedPayload.py @@ -1,288 +1,290 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional +from uuid import UUID from attr import define import attr -from .OfferDiscountType import OfferDiscountType +from .AdvancedCommerceTransactionInfo import AdvancedCommerceTransactionInfo +from .BillingPlanType import BillingPlanType from .Environment import Environment from .InAppOwnershipType import InAppOwnershipType from .LibraryUtility import AttrsRawValueAware +from .OfferDiscountType import OfferDiscountType from .OfferType import OfferType from .RevocationReason import RevocationReason from .RevocationType import RevocationType -from .TransactionReason import TransactionReason -from .BillingPlanType import BillingPlanType -from .AdvancedCommerceTransactionInfo import AdvancedCommerceTransactionInfo from .TransactionCommitmentInfo import TransactionCommitmentInfo - +from .TransactionReason import TransactionReason from .Type import Type @define class JWSTransactionDecodedPayload(AttrsRawValueAware): """ - A decoded payload containing transaction information. - + A decoded payload that contains transaction information. + https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload """ - originalTransactionId: Optional[str] = attr.ib(default=None) + appAccountToken: Optional[UUID] = attr.ib(default=None, kw_only=True) """ - The original transaction identifier of a purchase. - - https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + A UUID you create at the time of purchase that associates the transaction with a customer on your own service. + + https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken """ - transactionId: Optional[str] = attr.ib(default=None) + appTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The unique identifier for a transaction such as an in-app purchase, restored in-app purchase, or subscription renewal. - - https://developer.apple.com/documentation/appstoreserverapi/transactionid + The unique identifier of the app download transaction. + + https://developer.apple.com/documentation/appstoreserverapi/apptransactionid """ - webOrderLineItemId: Optional[str] = attr.ib(default=None) + bundleId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The unique identifier of subscription-purchase events across devices, including renewals. - - https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid + The bundle identifier of the app. + + https://developer.apple.com/documentation/appstoreserverapi/bundleid """ - bundleId: Optional[str] = attr.ib(default=None) + currency: Optional[str] = attr.ib(default=None, kw_only=True) """ - The bundle identifier of an app. - - https://developer.apple.com/documentation/appstoreserverapi/bundleid + The three-letter ISO 4217 currency code associated with the price parameter. + + https://developer.apple.com/documentation/appstoreserverapi/currency """ - productId: Optional[str] = attr.ib(default=None) + environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', kw_only=True) """ - The unique identifier for the product, that you create in App Store Connect. - - https://developer.apple.com/documentation/appstoreserverapi/productid - """ + The server environment, either sandbox or production. - subscriptionGroupIdentifier: Optional[str] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreserverapi/environment """ - The identifier of the subscription group that the subscription belongs to. - - https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier + + rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') + """ + See environment """ - purchaseDate: Optional[int] = attr.ib(default=None) + expiresDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The time that the App Store charged the user's account for an in-app purchase, a restored in-app purchase, a subscription, or a subscription renewal after a lapse. - - https://developer.apple.com/documentation/appstoreserverapi/purchasedate + The UNIX time, in milliseconds, that the subscription expires or renews. + + https://developer.apple.com/documentation/appstoreserverapi/expiresdate """ - originalPurchaseDate: Optional[int] = attr.ib(default=None) + inAppOwnershipType: Optional[InAppOwnershipType] = InAppOwnershipType.create_main_attr('rawInAppOwnershipType', kw_only=True) """ - The purchase date of the transaction associated with the original transaction identifier. - - https://developer.apple.com/documentation/appstoreserverapi/originalpurchasedate + A string that describes whether the transaction was purchased by the customer, or is available to them through Family Sharing, an organization, or a group. + + https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype """ - expiresDate: Optional[int] = attr.ib(default=None) + rawInAppOwnershipType: Optional[str] = InAppOwnershipType.create_raw_attr('inAppOwnershipType') """ - The UNIX time, in milliseconds, an auto-renewable subscription expires or renews. - - https://developer.apple.com/documentation/appstoreserverapi/expiresdate + See inAppOwnershipType """ - quantity: Optional[int] = attr.ib(default=None) + isUpgraded: Optional[bool] = attr.ib(default=None, kw_only=True) """ - The number of consumable products purchased. - - https://developer.apple.com/documentation/appstoreserverapi/quantity + A Boolean value that indicates whether the customer upgraded to another subscription. + + https://developer.apple.com/documentation/appstoreserverapi/isupgraded """ - type: Optional[Type] = Type.create_main_attr('rawType') + offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType', kw_only=True) """ - The type of the in-app purchase. - - https://developer.apple.com/documentation/appstoreserverapi/type + The payment mode you configure for the offer. + + https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype """ - rawType: Optional[str] = Type.create_raw_attr('type') + rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType') """ - See type + See offerDiscountType """ - appAccountToken: Optional[str] = attr.ib(default=None) + offerIdentifier: Optional[str] = attr.ib(default=None, kw_only=True) """ - 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 + The identifier that contains the offer code or the promotional offer identifier. + + https://developer.apple.com/documentation/appstoreserverapi/offeridentifier """ - inAppOwnershipType: Optional[InAppOwnershipType] = InAppOwnershipType.create_main_attr('rawInAppOwnershipType') + offerPeriod: Optional[str] = attr.ib(default=None, kw_only=True) """ - A string that describes whether the transaction was purchased by the user, or is available to them through Family Sharing. - - https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype + The duration of the offer applied to the transaction. + + https://developer.apple.com/documentation/appstoreserverapi/offerperiod """ - rawInAppOwnershipType: Optional[str] = InAppOwnershipType.create_raw_attr('inAppOwnershipType') + offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType', kw_only=True) """ - See inAppOwnershipType + A value that represents the promotional offer type. + + https://developer.apple.com/documentation/appstoreserverapi/offertype """ - signedDate: Optional[int] = attr.ib(default=None) + rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType') """ - The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. - - https://developer.apple.com/documentation/appstoreserverapi/signeddate + See offerType """ - revocationReason: Optional[RevocationReason] = RevocationReason.create_main_attr('rawRevocationReason') + originalPurchaseDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The reason that the App Store refunded the transaction or revoked it from Family Sharing. - - https://developer.apple.com/documentation/appstoreserverapi/revocationreason + The UNIX time, in milliseconds, that represents the purchase date of the original transaction identifier. + + https://developer.apple.com/documentation/appstoreserverapi/originalpurchasedate """ - rawRevocationReason: Optional[int] = RevocationReason.create_raw_attr('revocationReason') + originalTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) """ - See revocationReason + The transaction identifier of the original purchase. + + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid """ - revocationDate: Optional[int] = attr.ib(default=None) + price: Optional[int] = attr.ib(default=None, kw_only=True) + """ + An integer value that represents the price multiplied by 1000 of the in-app purchase or subscription offer you configured in App Store Connect and that the system records at the time of the purchase. + + https://developer.apple.com/documentation/appstoreserverapi/price """ - The UNIX time, in milliseconds, that Apple Support refunded a transaction. - - https://developer.apple.com/documentation/appstoreserverapi/revocationdate - """ - isUpgraded: Optional[bool] = attr.ib(default=None) + productId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The Boolean value that indicates whether the user upgraded to another subscription. - - https://developer.apple.com/documentation/appstoreserverapi/isupgraded + The unique identifier of the product. + + https://developer.apple.com/documentation/appstoreserverapi/productid """ - offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType') + purchaseDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - A value that represents the promotional offer type. - - https://developer.apple.com/documentation/appstoreserverapi/offertype + The UNIX time, in milliseconds, that the App Store charged the customer’s account for a purchase, restored product, subscription, or subscription renewal after a lapse. + + https://developer.apple.com/documentation/appstoreserverapi/purchasedate """ - rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType') + quantity: Optional[int] = attr.ib(default=None, kw_only=True) """ - See offerType + The number of products or seats the customer purchased. + + https://developer.apple.com/documentation/appstoreserverapi/quantity """ - offerIdentifier: Optional[str] = attr.ib(default=None) + revocationDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The identifier that contains the offer code or the promotional offer identifier. - - https://developer.apple.com/documentation/appstoreserverapi/offeridentifier + The UNIX time, in milliseconds, that the App Store refunded the transaction or revoked it from Family Sharing. + + https://developer.apple.com/documentation/appstoreserverapi/revocationdate """ - environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') + revocationPercentage: Optional[int] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(attr.validators.and_(attr.validators.ge(0), attr.validators.le(100000)))) """ - The server environment, either sandbox or production. - - https://developer.apple.com/documentation/appstoreserverapi/environment + The percentage, in milliunits, of the transaction that the App Store has refunded or revoked. + + https://developer.apple.com/documentation/appstoreserverapi/revocationpercentage """ - rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') + revocationReason: Optional[RevocationReason] = RevocationReason.create_main_attr('rawRevocationReason', kw_only=True) """ - See environment + The reason that the App Store refunded the transaction or revoked it from Family Sharing. + + https://developer.apple.com/documentation/appstoreserverapi/revocationreason """ - storefront: Optional[str] = attr.ib(default=None) + rawRevocationReason: Optional[int] = RevocationReason.create_raw_attr('revocationReason') """ - The three-letter code that represents the country or region associated with the App Store storefront for the purchase. - - https://developer.apple.com/documentation/appstoreserverapi/storefront + See revocationReason """ - storefrontId: Optional[str] = attr.ib(default=None) + revocationType: Optional[RevocationType] = RevocationType.create_main_attr('rawRevocationType', kw_only=True) """ - An Apple-defined value that uniquely identifies the App Store storefront associated with the purchase. - - https://developer.apple.com/documentation/appstoreserverapi/storefrontid + The type of the refund or revocation that applies to the transaction. + + https://developer.apple.com/documentation/appstoreserverapi/revocationtype """ - transactionReason: Optional[TransactionReason] = TransactionReason.create_main_attr('rawTransactionReason') + rawRevocationType: Optional[str] = RevocationType.create_raw_attr('revocationType') """ - The reason for the purchase transaction, which indicates whether it's a customer's purchase or a renewal for an auto-renewable subscription that the system initiates. - - https://developer.apple.com/documentation/appstoreserverapi/transactionreason + See revocationType """ - rawTransactionReason: Optional[str] = TransactionReason.create_raw_attr('transactionReason') + signedDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - See transactionReason + The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data. + + https://developer.apple.com/documentation/appstoreserverapi/signeddate """ - currency: Optional[str] = attr.ib(default=None) + storefront: Optional[str] = attr.ib(default=None, kw_only=True) """ - The three-letter ISO 4217 currency code for the price of the product. - - https://developer.apple.com/documentation/appstoreserverapi/currency + The three-letter code that represents the country or region associated with the App Store storefront for the purchase. + + https://developer.apple.com/documentation/appstoreserverapi/storefront """ - price: Optional[int] = attr.ib(default=None) + storefrontId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The price, in milliunits, of the in-app purchase or subscription offer that you configured in App Store Connect. - - https://developer.apple.com/documentation/appstoreserverapi/price + An Apple-defined value that uniquely identifies the App Store storefront associated with the purchase. + + https://developer.apple.com/documentation/appstoreserverapi/storefrontid """ - offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType') + subscriptionGroupIdentifier: Optional[str] = attr.ib(default=None, kw_only=True) """ - The payment mode you configure for the offer. - - https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype + The identifier of the subscription group to which the subscription belongs. + + https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier """ - rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType') + transactionId: Optional[str] = attr.ib(default=None, kw_only=True) """ - See offerDiscountType + The unique identifier of the transaction. + + https://developer.apple.com/documentation/appstoreserverapi/transactionid """ - appTransactionId: Optional[str] = attr.ib(default=None) + transactionReason: Optional[TransactionReason] = TransactionReason.create_main_attr('rawTransactionReason', kw_only=True) """ - The unique identifier of the app download transaction. - - https://developer.apple.com/documentation/appstoreserverapi/appTransactionId + The reason for the purchase transaction, which indicates whether it’s a customer’s purchase or a renewal for an auto-renewable subscription that the system initiates. + + https://developer.apple.com/documentation/appstoreserverapi/transactionreason """ - offerPeriod: Optional[str] = attr.ib(default=None) + rawTransactionReason: Optional[str] = TransactionReason.create_raw_attr('transactionReason') """ - The duration of the offer. - - https://developer.apple.com/documentation/appstoreserverapi/offerPeriod + See transactionReason """ - revocationType: Optional[RevocationType] = RevocationType.create_main_attr('rawRevocationType') + + type: Optional[Type] = Type.create_main_attr('rawType', kw_only=True) """ - The type of the refund or revocation that applies to the transaction. + The type of the in-app purchase. - https://developer.apple.com/documentation/appstoreservernotifications/revocationtype + https://developer.apple.com/documentation/appstoreserverapi/type """ - rawRevocationType: Optional[str] = RevocationType.create_raw_attr('revocationType') + rawType: Optional[str] = Type.create_raw_attr('type') """ - See revocationType + See type """ - revocationPercentage: Optional[int] = attr.ib(default=None) + webOrderLineItemId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The percentage, in milliunits, of the transaction that the App Store has refunded or revoked. + The unique identifier of subscription purchase events across devices, including subscription renewals. - https://developer.apple.com/documentation/appstoreservernotifications/revocationpercentage + https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid """ - advancedCommerceInfo: Optional[AdvancedCommerceTransactionInfo] = attr.ib(default=None) + advancedCommerceInfo: Optional[AdvancedCommerceTransactionInfo] = attr.ib(default=None, kw_only=True) """ Transaction information that is present only for Advanced Commerce SKUs. https://developer.apple.com/documentation/appstoreserverapi/advancedcommercetransactioninfo """ - billingPlanType: Optional[BillingPlanType] = BillingPlanType.create_main_attr('rawBillingPlanType') + billingPlanType: Optional[BillingPlanType] = BillingPlanType.create_main_attr('rawBillingPlanType', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/billingplantype """ @@ -292,7 +294,14 @@ class JWSTransactionDecodedPayload(AttrsRawValueAware): See billingPlanType """ - commitmentInfo: Optional[TransactionCommitmentInfo] = attr.ib(default=None) + commitmentInfo: Optional[TransactionCommitmentInfo] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/transactioncommitmentinfo """ + + previousOriginalTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The subscription’s original transaction identifier, before you migrated the subscription. + + https://developer.apple.com/documentation/appstoreservernotifications/previousoriginaltransactionid + """ diff --git a/appstoreserverlibrary/models/LastTransactionsItem.py b/appstoreserverlibrary/models/LastTransactionsItem.py index 63b90278..2bf370d3 100644 --- a/appstoreserverlibrary/models/LastTransactionsItem.py +++ b/appstoreserverlibrary/models/LastTransactionsItem.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -11,14 +12,21 @@ class LastTransactionsItem(AttrsRawValueAware): """ The most recent App Store-signed transaction information and App Store-signed renewal information for an auto-renewable subscription. - + https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem """ - status: Optional[Status] = Status.create_main_attr('rawStatus') + originalTransactionId: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The original transaction identifier of the auto-renewable subscription. + + https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + """ + + status: Optional[Status] = Status.create_main_attr('rawStatus', kw_only=True) """ The status of the auto-renewable subscription. - + https://developer.apple.com/documentation/appstoreserverapi/status """ @@ -27,23 +35,16 @@ class LastTransactionsItem(AttrsRawValueAware): See status """ - originalTransactionId: Optional[str] = attr.ib(default=None) - """ - The original transaction identifier of a purchase. - - https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid + signedRenewalInfo: Optional[str] = attr.ib(default=None, kw_only=True) """ + The subscription renewal information signed by the App Store, in JSON Web Signature (JWS) format. - signedTransactionInfo: Optional[str] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo """ - Transaction information signed by the App Store, in JSON Web Signature (JWS) format. - - https://developer.apple.com/documentation/appstoreserverapi/jwstransaction + + signedTransactionInfo: Optional[str] = attr.ib(default=None, kw_only=True) """ + The transaction information signed by the App Store, in JWS format. - signedRenewalInfo: Optional[str] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ - Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. - - https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/LibraryUtility.py b/appstoreserverlibrary/models/LibraryUtility.py index 0ee535d8..233d92d1 100644 --- a/appstoreserverlibrary/models/LibraryUtility.py +++ b/appstoreserverlibrary/models/LibraryUtility.py @@ -6,7 +6,6 @@ from uuid import UUID from attr import Attribute, has, ib, fields -from cattr import override from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override import cattrs @@ -15,6 +14,16 @@ metadata_key = 'correspondingFieldName' metadata_type_key = 'typeOfField' metadata_required_key = 'requiredField' +metadata_none_value_key = 'serializedValueWhenNone' + +def validate_not_none(instance, attribute: Attribute, value): + if value is None: + raise ValueError(f"{attribute.name} cannot be None") + +def _structure_required_value(value: Any, cl: Type[T]) -> T: + if value is None: + raise ValueError() + return cl(value) class AppStoreServerLibraryEnumMeta(EnumMeta): def __contains__(c, val): @@ -24,15 +33,15 @@ def __contains__(c, val): return False return True - def create_main_attr(c, raw_field_name: str, raw_required: bool = False) -> Any: - def value_set(self, _: Attribute, value: c): + def create_main_attr(c, raw_field_name: str, raw_required: bool = False, kw_only: bool = False) -> Any: + def value_set(self, attribute: Attribute, value: c): newValue = value.value if value is not None else None if raw_required and newValue is None: - raise ValueError(f"{raw_field_name} cannot be set to None when field is required") + raise ValueError(f"{attribute.name} cannot be set to None when field is required") if newValue != getattr(self, raw_field_name): object.__setattr__(self, raw_field_name, newValue) return value - return ib(default=None, on_setattr=value_set, metadata={metadata_key: raw_field_name, metadata_type_key: 'main'}) + return ib(default=None, kw_only=kw_only, on_setattr=value_set, metadata={metadata_key: raw_field_name, metadata_type_key: 'main'}) def create_raw_attr(c, field_name: str, required: bool = False) -> Any: def value_set(self, _: Attribute, value: str): @@ -43,10 +52,6 @@ def value_set(self, _: Attribute, value: str): object.__setattr__(self, field_name, newValue) return value - def validate_not_none(instance, attribute, value): - if value is None: - raise ValueError(f"{attribute.name} cannot be None") - if required: from attr import Factory def factory(instance): @@ -80,7 +85,9 @@ def _get_cattrs_converter(destination_class: Type[T]) -> cattrs.Converter: # Register UUID hooks to ensure lowercase serialization c.register_unstructure_hook(UUID, lambda uuid: str(uuid).lower() if uuid is not None else None) - c.register_structure_hook(UUID, lambda d, _: UUID(d) if d is not None else None) + + for required_type in (str, int, float, bool, UUID): + c.register_structure_hook(required_type, _structure_required_value) # Need a function here because it must be a lambda based on cl, which is not always destination_class def make_overrides(cl): @@ -99,6 +106,9 @@ def make_overrides(cl): cattrs_overrides[raw_field] = override(rename=matching_name, omit_if_default=True) elif not attribute.init and attribute.name not in cattrs_overrides: cattrs_overrides[attribute.name] = override(omit=False) + elif metadata_none_value_key in attribute.metadata and attribute.name not in cattrs_overrides: + none_value = attribute.metadata[metadata_none_value_key] + cattrs_overrides[attribute.name] = override(unstruct_hook=lambda value, none_value=none_value: none_value if value is None else c.unstructure(value)) elif attribute.default is None and attribute.name not in cattrs_overrides: cattrs_overrides[attribute.name] = override(omit_if_default=True) return cattrs_overrides diff --git a/appstoreserverlibrary/models/LifetimeDollarsPurchased.py b/appstoreserverlibrary/models/LifetimeDollarsPurchased.py index 3327f07c..f231c42b 100644 --- a/appstoreserverlibrary/models/LifetimeDollarsPurchased.py +++ b/appstoreserverlibrary/models/LifetimeDollarsPurchased.py @@ -6,15 +6,46 @@ class LifetimeDollarsPurchased(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ - A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. - + A value that indicates the dollar amount of in-app purchases the customer has made in your app, since purchasing the app, across all platforms. + https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased """ UNDECLARED = 0 + """ + Lifetime purchase amount is undeclared. + """ + ZERO_DOLLARS = 1 + """ + Lifetime purchase amount is 0 USD. + """ + ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2 + """ + Lifetime purchase amount is between 0.01–49.99 USD. + """ + FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3 + """ + Lifetime purchase amount is between 50–99.99 USD. + """ + ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4 + """ + Lifetime purchase amount is between 100–499.99 USD. + """ + FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5 + """ + Lifetime purchase amount is between 500–999.99 USD. + """ + ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6 + """ + Lifetime purchase amount is between 1000–1999.99 USD. + """ + TWO_THOUSAND_DOLLARS_OR_GREATER = 7 + """ + Lifetime purchase amount is over 2000 USD. + """ diff --git a/appstoreserverlibrary/models/LifetimeDollarsRefunded.py b/appstoreserverlibrary/models/LifetimeDollarsRefunded.py index 6e340a81..e4704cd8 100644 --- a/appstoreserverlibrary/models/LifetimeDollarsRefunded.py +++ b/appstoreserverlibrary/models/LifetimeDollarsRefunded.py @@ -7,14 +7,45 @@ class LifetimeDollarsRefunded(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ A value that indicates the dollar amount of refunds the customer has received in your app, since purchasing the app, across all platforms. - + https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded """ UNDECLARED = 0 + """ + Lifetime refund amount is undeclared. + """ + ZERO_DOLLARS = 1 + """ + Lifetime refund amount is 0 USD. + """ + ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2 + """ + Lifetime refund amount is between 0.01–49.99 USD. + """ + FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3 + """ + Lifetime refund amount is between 50–99.99 USD. + """ + ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4 + """ + Lifetime refund amount is between 100–499.99 USD. + """ + FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5 + """ + Lifetime refund amount is between 500–999.99 USD. + """ + ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6 + """ + Lifetime refund amount is between 1000–1999.99 USD. + """ + TWO_THOUSAND_DOLLARS_OR_GREATER = 7 + """ + Lifetime refund amount is over 2000 USD. + """ diff --git a/appstoreserverlibrary/models/MassExtendRenewalDateRequest.py b/appstoreserverlibrary/models/MassExtendRenewalDateRequest.py index c5976bf9..d73d3cce 100644 --- a/appstoreserverlibrary/models/MassExtendRenewalDateRequest.py +++ b/appstoreserverlibrary/models/MassExtendRenewalDateRequest.py @@ -1,51 +1,57 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List + from attr import define -from typing import List, Optional import attr from .ExtendReasonCode import ExtendReasonCode +from .LibraryUtility import AttrsRawValueAware, validate_not_none @define -class MassExtendRenewalDateRequest: +class MassExtendRenewalDateRequest(AttrsRawValueAware): """ The request body that contains subscription-renewal-extension data to apply for all eligible active subscribers. - + https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldaterequest """ - extendByDays: Optional[int] = attr.ib(default=None) + requestIdentifier: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(128)]) + """ + A string that contains a one-time UUID value you provide to identify this subscription-renewal-date extension request. + + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier + """ + + extendByDays: int = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.and_(attr.validators.ge(1), attr.validators.le(90))]) """ The number of days to extend the subscription renewal date. - + https://developer.apple.com/documentation/appstoreserverapi/extendbydays - maximum: 90 """ - extendReasonCode: Optional[ExtendReasonCode] = attr.ib(default=None) + extendReasonCode: ExtendReasonCode = ExtendReasonCode.create_main_attr('rawExtendReasonCode', raw_required=True, kw_only=True) """ The reason code for the subscription-renewal-date extension. - + https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode - """ + """ - requestIdentifier: Optional[str] = attr.ib(default=None) + rawExtendReasonCode: int = ExtendReasonCode.create_raw_attr('extendReasonCode', required=True) """ - A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. - - https://developer.apple.com/documentation/appstoreserverapi/requestidentifier + See extendReasonCode """ - storefrontCountryCodes: Optional[List[str]] = attr.ib(default=None) + productId: str = attr.ib(kw_only=True, validator=validate_not_none) """ - A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension. - - https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes + The product identifier of the auto-renewable subscription that you’re requesting the renewal-date extension for. + + https://developer.apple.com/documentation/appstoreserverapi/productid """ - productId: Optional[str] = attr.ib(default=None) + storefrontCountryCodes: Optional[List[str]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(attr.validators.deep_iterable(attr.validators.max_len(3)))) + """ + A list of storefront country codes you provide to limit the storefronts that are eligible to receive the subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes """ - The unique identifier for the product, that you create in App Store Connect. - - https://developer.apple.com/documentation/appstoreserverapi/productid - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/MassExtendRenewalDateResponse.py b/appstoreserverlibrary/models/MassExtendRenewalDateResponse.py index a8aeab46..607442ee 100644 --- a/appstoreserverlibrary/models/MassExtendRenewalDateResponse.py +++ b/appstoreserverlibrary/models/MassExtendRenewalDateResponse.py @@ -1,20 +1,21 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define import attr @define -class MassExtendRenewalDateResponse: +class MassExtendRenewalDateResponse: """ A response that indicates the server successfully received the subscription-renewal-date extension request. - + https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldateresponse """ - requestIdentifier: Optional[str] = attr.ib(default=None) + requestIdentifier: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(attr.validators.max_len(128))) """ - A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. - + A string that contains the UUID that identifies the subscription-renewal-date extension request. + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/MassExtendRenewalDateStatusResponse.py b/appstoreserverlibrary/models/MassExtendRenewalDateStatusResponse.py index 298a9083..f2a10f6b 100644 --- a/appstoreserverlibrary/models/MassExtendRenewalDateStatusResponse.py +++ b/appstoreserverlibrary/models/MassExtendRenewalDateStatusResponse.py @@ -1,48 +1,49 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define import attr @define -class MassExtendRenewalDateStatusResponse: +class MassExtendRenewalDateStatusResponse: """ A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers. - + https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldatestatusresponse """ - requestIdentifier: Optional[str] = attr.ib(default=None) + requestIdentifier: Optional[str] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(attr.validators.max_len(128))) """ - A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. - + The UUID that represents your request for a subscription-renewal-date extension. + https://developer.apple.com/documentation/appstoreserverapi/requestidentifier """ - complete: Optional[bool] = attr.ib(default=None) + complete: Optional[bool] = attr.ib(default=None, kw_only=True) """ - A Boolean value that indicates whether the App Store completed the request to extend a subscription renewal date to active subscribers. - + A Boolean value that’s TRUE to indicate that the App Store completed your request to extend a subscription renewal date for all eligible subscribers. + https://developer.apple.com/documentation/appstoreserverapi/complete """ - completeDate: Optional[int] = attr.ib(default=None) + completeDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The UNIX time, in milliseconds, that the App Store completes a request to extend a subscription renewal date for eligible subscribers. - + The date that the App Store completes the request. + https://developer.apple.com/documentation/appstoreserverapi/completedate """ - succeededCount: Optional[int] = attr.ib(default=None) + failedCount: Optional[int] = attr.ib(default=None, kw_only=True) """ - The count of subscriptions that successfully receive a subscription-renewal-date extension. - - https://developer.apple.com/documentation/appstoreserverapi/succeededcount + The final count of subscribers that fail to receive a subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/failedcount """ - failedCount: Optional[int] = attr.ib(default=None) + succeededCount: Optional[int] = attr.ib(default=None, kw_only=True) + """ + The final count of subscribers that successfully receive a subscription-renewal-date extension. + + https://developer.apple.com/documentation/appstoreserverapi/succeededcount """ - The count of subscriptions that fail to receive a subscription-renewal-date extension. - - https://developer.apple.com/documentation/appstoreserverapi/failedcount - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/Message.py b/appstoreserverlibrary/models/Message.py index b975bc66..370b4a8b 100644 --- a/appstoreserverlibrary/models/Message.py +++ b/appstoreserverlibrary/models/Message.py @@ -1,4 +1,5 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. + from typing import Optional from uuid import UUID @@ -13,7 +14,7 @@ class Message: https://developer.apple.com/documentation/retentionmessaging/message """ - messageIdentifier: Optional[UUID] = attr.ib(default=None) + messageIdentifier: Optional[UUID] = attr.ib(default=None, kw_only=True) """ The identifier of the message to display to the customer. diff --git a/appstoreserverlibrary/models/MessageState.py b/appstoreserverlibrary/models/MessageState.py index bec03ed1..ed398e7b 100644 --- a/appstoreserverlibrary/models/MessageState.py +++ b/appstoreserverlibrary/models/MessageState.py @@ -11,5 +11,16 @@ class MessageState(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): https://developer.apple.com/documentation/retentionmessaging/messagestate """ PENDING = "PENDING" + """ + The message is awaiting approval. + """ + APPROVED = "APPROVED" + """ + The message is approved. + """ + REJECTED = "REJECTED" + """ + The message is rejected. + """ diff --git a/appstoreserverlibrary/models/NotificationHistoryRequest.py b/appstoreserverlibrary/models/NotificationHistoryRequest.py index 46b3e36c..9bdeedf5 100644 --- a/appstoreserverlibrary/models/NotificationHistoryRequest.py +++ b/appstoreserverlibrary/models/NotificationHistoryRequest.py @@ -1,60 +1,70 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional + from attr import define import attr +from .LibraryUtility import AttrsRawValueAware, validate_not_none from .NotificationTypeV2 import NotificationTypeV2 from .Subtype import Subtype @define -class NotificationHistoryRequest: +class NotificationHistoryRequest(AttrsRawValueAware): """ The request body for notification history. - + https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryrequest """ - startDate: Optional[int] = attr.ib(default=None) + startDate: int = attr.ib(kw_only=True, validator=validate_not_none) """ - The start date of the timespan for the requested App Store Server Notification history records. The startDate needs to precede the endDate. Choose a startDate that's within the past 180 days from the current date. - + The start date of the timespan for the requested App Store Server Notification history records. + https://developer.apple.com/documentation/appstoreserverapi/startdate """ - endDate: Optional[int] = attr.ib(default=None) + endDate: int = attr.ib(kw_only=True, validator=validate_not_none) """ - The end date of the timespan for the requested App Store Server Notification history records. Choose an endDate that's later than the startDate. If you choose an endDate in the future, the endpoint automatically uses the current date as the endDate. - + The end date of the timespan for the requested App Store Server Notification history records. + https://developer.apple.com/documentation/appstoreserverapi/enddate """ - notificationType: Optional[NotificationTypeV2] = attr.ib(default=None) + notificationType: Optional[NotificationTypeV2] = NotificationTypeV2.create_main_attr('rawNotificationType', kw_only=True) """ - A notification type. Provide this field to limit the notification history records to those with this one notification type. For a list of notifications types, see notificationType. - Include either the transactionId or the notificationType in your query, but not both. - + A notification type. + https://developer.apple.com/documentation/appstoreserverapi/notificationtype """ - notificationSubtype: Optional[Subtype] = attr.ib(default=None) + rawNotificationType: Optional[str] = NotificationTypeV2.create_raw_attr('notificationType') """ - A notification subtype. Provide this field to limit the notification history records to those with this one notification subtype. For a list of subtypes, see subtype. If you specify a notificationSubtype, you need to also specify its related notificationType. - + See notificationType + """ + + notificationSubtype: Optional[Subtype] = Subtype.create_main_attr('rawNotificationSubtype', kw_only=True) + """ + A notification subtype. + https://developer.apple.com/documentation/appstoreserverapi/notificationsubtype """ - transactionId: Optional[str] = attr.ib(default=None) + rawNotificationSubtype: Optional[str] = Subtype.create_raw_attr('notificationSubtype') """ - The transaction identifier, which may be an original transaction identifier, of any transaction belonging to the customer. Provide this field to limit the notification history request to this one customer. - Include either the transactionId or the notificationType in your query, but not both. - - https://developer.apple.com/documentation/appstoreserverapi/transactionid + See notificationSubtype """ - onlyFailures: Optional[bool] = attr.ib(default=None) + onlyFailures: Optional[bool] = attr.ib(default=None, kw_only=True) """ - A Boolean value you set to true to request only the notifications that haven’t reached your server successfully. The response also includes notifications that the App Store server is currently retrying to send to your server. - + A Boolean value you set to true to request only the notifications that haven’t reached your server successfully. + https://developer.apple.com/documentation/appstoreserverapi/onlyfailures - """ \ No newline at end of file + """ + + transactionId: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The transaction identifier, which may be an original transaction identifier, of any transaction belonging to the customer. + + https://developer.apple.com/documentation/appstoreserverapi/transactionid + """ diff --git a/appstoreserverlibrary/models/NotificationHistoryResponse.py b/appstoreserverlibrary/models/NotificationHistoryResponse.py index 4a4ef20f..92860177 100644 --- a/appstoreserverlibrary/models/NotificationHistoryResponse.py +++ b/appstoreserverlibrary/models/NotificationHistoryResponse.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional, List from attr import define @@ -7,30 +8,30 @@ from .NotificationHistoryResponseItem import NotificationHistoryResponseItem @define -class NotificationHistoryResponse: +class NotificationHistoryResponse: """ A response that contains the App Store Server Notifications history for your app. - + https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponse """ - paginationToken: Optional[str] = attr.ib(default=None) + notificationHistory: Optional[List[NotificationHistoryResponseItem]] = attr.ib(default=None, kw_only=True) """ - A pagination token that you return to the endpoint on a subsequent call to receive the next set of results. - - https://developer.apple.com/documentation/appstoreserverapi/paginationtoken + An array of App Store Server Notifications history records. + + https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem """ - hasMore: Optional[bool] = attr.ib(default=None) + hasMore: Optional[bool] = attr.ib(default=None, kw_only=True) """ - A Boolean value indicating whether the App Store has more transaction data. - + A Boolean value that indicates whether the App Store has more notification history records to send. + https://developer.apple.com/documentation/appstoreserverapi/hasmore """ - notificationHistory: Optional[List[NotificationHistoryResponseItem]] = attr.ib(default=None) + paginationToken: Optional[str] = attr.ib(default=None, kw_only=True) """ - An array of App Store server notification history records. - - https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem + A pagination token that you provide to Get Notification History on a subsequent request to get the next page of responses. + + https://developer.apple.com/documentation/appstoreserverapi/paginationtoken """ diff --git a/appstoreserverlibrary/models/NotificationHistoryResponseItem.py b/appstoreserverlibrary/models/NotificationHistoryResponseItem.py index ed61018f..0efc8be1 100644 --- a/appstoreserverlibrary/models/NotificationHistoryResponseItem.py +++ b/appstoreserverlibrary/models/NotificationHistoryResponseItem.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional, List from attr import define @@ -7,23 +8,23 @@ from .SendAttemptItem import SendAttemptItem @define -class NotificationHistoryResponseItem: +class NotificationHistoryResponseItem: """ - The App Store server notification history record, including the signed notification payload and the result of the server's first send attempt. - + The App Store server notification history record, including the signed notification payload and the result of the server’s first send attempt. + https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem """ - signedPayload: Optional[str] = attr.ib(default=None) + sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None, kw_only=True) """ - A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. - - https://developer.apple.com/documentation/appstoreservernotifications/signedpayload + An array of information the App Store server records for its attempts to send a notification to your server. + + https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem """ - sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None) + signedPayload: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The cryptographically signed payload, in JSON Web Signature (JWS) format, containing the original response body of a version 2 notification. + + https://developer.apple.com/documentation/appstoreserverapi/signedpayload """ - An array of information the App Store server records for its attempts to send a notification to your server. The maximum number of entries in the array is six. - - https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/NotificationTypeV2.py b/appstoreserverlibrary/models/NotificationTypeV2.py index e4a80b5e..8d53a304 100644 --- a/appstoreserverlibrary/models/NotificationTypeV2.py +++ b/appstoreserverlibrary/models/NotificationTypeV2.py @@ -6,30 +6,121 @@ class NotificationTypeV2(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - The type that describes the in-app purchase or external purchase event for which the App Store sends the version 2 notification. - + The type that describes the In-App Purchase or external purchase event for which the App Store sends the version 2 notification. + https://developer.apple.com/documentation/appstoreservernotifications/notificationtype """ SUBSCRIBED = "SUBSCRIBED" + """ + A notification type that, along with its subtype, indicates that the customer subscribed to an auto-renewable subscription. + """ + DID_CHANGE_RENEWAL_PREF = "DID_CHANGE_RENEWAL_PREF" + """ + A notification type that, along with its subtype, indicates that the customer made a change to their subscription plan. + """ + DID_CHANGE_RENEWAL_STATUS = "DID_CHANGE_RENEWAL_STATUS" + """ + A notification type that, along with its subtype, indicates that the customer made a change to the subscription renewal status. + """ + OFFER_REDEEMED = "OFFER_REDEEMED" + """ + A notification type that, along with its subtype, indicates that a customer with an active subscription redeemed a subscription offer. + """ + DID_RENEW = "DID_RENEW" + """ + A notification type that, along with its subtype, indicates that the subscription successfully renewed. + """ + EXPIRED = "EXPIRED" + """ + A notification type that, along with its subtype, indicates that a subscription expired. + """ + DID_FAIL_TO_RENEW = "DID_FAIL_TO_RENEW" + """ + A notification type that, along with its subtype, indicates that the subscription failed to renew due to a billing issue. + """ + GRACE_PERIOD_EXPIRED = "GRACE_PERIOD_EXPIRED" + """ + A notification type that indicates that the billing grace period has ended without renewing the subscription, so you can turn off access to the service or content. + """ + PRICE_INCREASE = "PRICE_INCREASE" + """ + A notification type that, along with its subtype, indicates that the system has informed the customer of an auto-renewable subscription price increase. + """ + REFUND = "REFUND" + """ + A notification type that indicates that the App Store successfully refunded a transaction for a consumable In-App Purchase, a non-consumable In-App Purchase, an auto-renewable subscription, or a non-renewing subscription. + """ + REFUND_DECLINED = "REFUND_DECLINED" + """ + A notification type that indicates the App Store declined a refund request. + """ + CONSUMPTION_REQUEST = "CONSUMPTION_REQUEST" + """ + A notification type that indicates that the customer initiated a refund request for a consumable In-App Purchase or auto-renewable subscription, and the App Store is requesting that you provide consumption data. + """ + RENEWAL_EXTENDED = "RENEWAL_EXTENDED" + """ + A notification type that indicates the App Store extended the subscription renewal date for a specific subscription. + """ + REVOKE = "REVOKE" + """ + A notification type that indicates that an In-App Purchase the customer was entitled to through Family Sharing is no longer available through sharing. + """ + TEST = "TEST" + """ + A notification type that the App Store server sends when you request it by calling the Request a Test Notification endpoint. + """ + RENEWAL_EXTENSION = "RENEWAL_EXTENSION" + """ + A notification type that, along with its subtype, indicates that the App Store is attempting to extend the subscription renewal date that you request by calling Extend Subscription Renewal Dates for All Active Subscribers. + """ + REFUND_REVERSED = "REFUND_REVERSED" + """ + A notification type that indicates the App Store reversed a previously granted refund due to a dispute that the customer raised. + """ + EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN" + """ + A notification type that applies only to apps that get tokens from the External Purchase or TransactionReporting APIs. + """ + ONE_TIME_CHARGE = "ONE_TIME_CHARGE" - RESCIND_CONSENT = "RESCIND_CONSENT" + """ + A notification type that indicates the customer purchased a consumable, non-consumable, or non-renewing subscription. + """ + + PRICE_CHANGE = "PRICE_CHANGE" + """ + A notification type that indicates that you called the Change Subscription Price endpoint. + """ + METADATA_UPDATE = "METADATA_UPDATE" + """ + A notification type that indicates you used the Change Subscription Metadata endpoint to change the metadata for a subscription. + """ + MIGRATION = "MIGRATION" - PRICE_CHANGE = "PRICE_CHANGE" + """ + A notification type that indicates you used the Migrate Subscription to Advanced Commerce API endpoint. + """ + + RESCIND_CONSENT = "RESCIND_CONSENT" + """ + A notification type that indicates the parent or guardian has withdrawn consent for a child’s app usage. + """ diff --git a/appstoreserverlibrary/models/OfferDiscountType.py b/appstoreserverlibrary/models/OfferDiscountType.py index a383ca94..4863d12a 100644 --- a/appstoreserverlibrary/models/OfferDiscountType.py +++ b/appstoreserverlibrary/models/OfferDiscountType.py @@ -7,10 +7,25 @@ class OfferDiscountType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The payment mode for a discount offer on an In-App Purchase. - + https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype """ FREE_TRIAL = "FREE_TRIAL" + """ + A payment mode of a discount for auto-renewable subscriptions that indicates a free trial. + """ + PAY_AS_YOU_GO = "PAY_AS_YOU_GO" + """ + A payment mode of a discount for auto-renewable subscriptions that customers pay over a single or multiple billing periods. + """ + PAY_UP_FRONT = "PAY_UP_FRONT" - ONE_TIME = "ONE_TIME" \ No newline at end of file + """ + A payment mode of a discount for auto-renewable subscriptions that customers pay up front. + """ + + ONE_TIME = "ONE_TIME" + """ + A payment mode for a discount for In-App Purchase types including consumable, non-consumable, and non-renewing subscription. + """ diff --git a/appstoreserverlibrary/models/OfferType.py b/appstoreserverlibrary/models/OfferType.py index 48a5df66..baf77541 100644 --- a/appstoreserverlibrary/models/OfferType.py +++ b/appstoreserverlibrary/models/OfferType.py @@ -7,10 +7,25 @@ class OfferType(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The type of offer. - + https://developer.apple.com/documentation/appstoreserverapi/offertype """ INTRODUCTORY_OFFER = 1 + """ + An introductory offer. + """ + PROMOTIONAL_OFFER = 2 + """ + A promotional offer. + """ + OFFER_CODE = 3 + """ + An offer with an offer code. + """ + WIN_BACK_OFFER = 4 + """ + A win-back offer. + """ diff --git a/appstoreserverlibrary/models/OrderLookupResponse.py b/appstoreserverlibrary/models/OrderLookupResponse.py index deaf9851..86abb3fc 100644 --- a/appstoreserverlibrary/models/OrderLookupResponse.py +++ b/appstoreserverlibrary/models/OrderLookupResponse.py @@ -1,7 +1,8 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List + from attr import define -from typing import List, Optional import attr from .LibraryUtility import AttrsRawValueAware @@ -11,14 +12,14 @@ class OrderLookupResponse(AttrsRawValueAware): """ A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order. - + https://developer.apple.com/documentation/appstoreserverapi/orderlookupresponse """ - status: Optional[OrderLookupStatus] = OrderLookupStatus.create_main_attr('rawStatus') + status: Optional[OrderLookupStatus] = OrderLookupStatus.create_main_attr('rawStatus', kw_only=True) """ The status that indicates whether the order ID is valid. - + https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus """ @@ -27,9 +28,9 @@ class OrderLookupResponse(AttrsRawValueAware): See status """ - signedTransactions: Optional[List[str]] = attr.ib(default=None) + signedTransactions: Optional[List[str]] = attr.ib(default=None, kw_only=True) """ - An array of in-app purchase transactions that are part of order, signed by Apple, in JSON Web Signature format. - + An array of in-app purchase transactions that are part of the order, signed by Apple, in JSON Web Signature format. + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ diff --git a/appstoreserverlibrary/models/OrderLookupStatus.py b/appstoreserverlibrary/models/OrderLookupStatus.py index ee015573..6ee29c56 100644 --- a/appstoreserverlibrary/models/OrderLookupStatus.py +++ b/appstoreserverlibrary/models/OrderLookupStatus.py @@ -7,8 +7,15 @@ class OrderLookupStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ A value that indicates whether the order ID in the request is valid for your app. - + https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus """ VALID = 0 + """ + The orderId that you provided in the Look Up Order ID request is valid and contains at least one in-app purchase for your app. + """ + INVALID = 1 + """ + The orderId is invalid or doesn’t contain any in-app purchases for your app. + """ diff --git a/appstoreserverlibrary/models/PerformanceTestConfig.py b/appstoreserverlibrary/models/PerformanceTestConfig.py index a0f35050..55d4e2b5 100644 --- a/appstoreserverlibrary/models/PerformanceTestConfig.py +++ b/appstoreserverlibrary/models/PerformanceTestConfig.py @@ -1,10 +1,10 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import Optional - from attr import define import attr +from .LibraryUtility import validate_not_none + @define class PerformanceTestConfig: """ @@ -13,37 +13,37 @@ class PerformanceTestConfig: https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ - maxConcurrentRequests: Optional[int] = attr.ib(default=None) + maxConcurrentRequests: int = attr.ib(kw_only=True, validator=validate_not_none) """ The maximum number of concurrent requests the API allows. - https://developer.apple.com/documentation/retentionmessaging/maxconcurrentrequests + https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ - totalRequests: Optional[int] = attr.ib(default=None) + responseTimeThreshold: int = attr.ib(kw_only=True, validator=validate_not_none) """ - The total number of requests to make during the test. + The maximum time your server has to respond when the system calls your Get Retention Message endpoint in the sandbox environment. - https://developer.apple.com/documentation/retentionmessaging/totalrequests + https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ - totalDuration: Optional[int] = attr.ib(default=None) + successRateThreshold: int = attr.ib(kw_only=True, validator=validate_not_none) """ - The total duration of the test in milliseconds. + The success rate threshold percentage. - https://developer.apple.com/documentation/retentionmessaging/totalduration + https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ - responseTimeThreshold: Optional[int] = attr.ib(default=None) + totalDuration: int = attr.ib(kw_only=True, validator=validate_not_none) """ - The maximum time your server has to respond when the system calls your Get Retention Message endpoint in the sandbox environment. + The total duration of the test in milliseconds. - https://developer.apple.com/documentation/retentionmessaging/responsetimethreshold + https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ - successRateThreshold: Optional[int] = attr.ib(default=None) + totalRequests: int = attr.ib(kw_only=True, validator=validate_not_none) """ - The success rate threshold percentage. + The total number of requests to make during the test. - https://developer.apple.com/documentation/retentionmessaging/successratethreshold + https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ diff --git a/appstoreserverlibrary/models/PerformanceTestRequest.py b/appstoreserverlibrary/models/PerformanceTestRequest.py index 66768bcd..a04005e2 100644 --- a/appstoreserverlibrary/models/PerformanceTestRequest.py +++ b/appstoreserverlibrary/models/PerformanceTestRequest.py @@ -3,6 +3,8 @@ from attr import define import attr +from .LibraryUtility import validate_not_none + @define class PerformanceTestRequest: """ @@ -11,7 +13,7 @@ class PerformanceTestRequest: https://developer.apple.com/documentation/retentionmessaging/performancetestrequest """ - originalTransactionId: str = attr.ib() + originalTransactionId: str = attr.ib(kw_only=True, validator=validate_not_none) """ The original transaction identifier of an In-App Purchase you initiate in the sandbox environment, to use as the purchase for this test. diff --git a/appstoreserverlibrary/models/PerformanceTestResponse.py b/appstoreserverlibrary/models/PerformanceTestResponse.py index e5e81915..6e921782 100644 --- a/appstoreserverlibrary/models/PerformanceTestResponse.py +++ b/appstoreserverlibrary/models/PerformanceTestResponse.py @@ -1,10 +1,11 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import Optional +from uuid import UUID from attr import define import attr +from .LibraryUtility import validate_not_none from .PerformanceTestConfig import PerformanceTestConfig @define @@ -15,14 +16,14 @@ class PerformanceTestResponse: https://developer.apple.com/documentation/retentionmessaging/performancetestresponse """ - config: Optional[PerformanceTestConfig] = attr.ib(default=None) + config: PerformanceTestConfig = attr.ib(kw_only=True, validator=validate_not_none) """ The performance test configuration object. https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ - requestId: Optional[str] = attr.ib(default=None) + requestId: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ The performance test request identifier. diff --git a/appstoreserverlibrary/models/PerformanceTestResponseTimes.py b/appstoreserverlibrary/models/PerformanceTestResponseTimes.py index b8d4fe5e..765fa801 100644 --- a/appstoreserverlibrary/models/PerformanceTestResponseTimes.py +++ b/appstoreserverlibrary/models/PerformanceTestResponseTimes.py @@ -1,10 +1,10 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import Optional - from attr import define import attr +from .LibraryUtility import validate_not_none + @define class PerformanceTestResponseTimes: """ @@ -13,37 +13,37 @@ class PerformanceTestResponseTimes: https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ - average: Optional[int] = attr.ib(default=None) + average: int = attr.ib(kw_only=True, validator=validate_not_none) """ Average response time in milliseconds. - https://developer.apple.com/documentation/retentionmessaging/average + https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ - p50: Optional[int] = attr.ib(default=None) + p50: int = attr.ib(kw_only=True, validator=validate_not_none) """ The 50th percentile response time in milliseconds. - https://developer.apple.com/documentation/retentionmessaging/p50 + https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ - p90: Optional[int] = attr.ib(default=None) + p90: int = attr.ib(kw_only=True, validator=validate_not_none) """ The 90th percentile response time in milliseconds. - https://developer.apple.com/documentation/retentionmessaging/p90 + https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ - p95: Optional[int] = attr.ib(default=None) + p95: int = attr.ib(kw_only=True, validator=validate_not_none) """ The 95th percentile response time in milliseconds. - https://developer.apple.com/documentation/retentionmessaging/p95 + https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ - p99: Optional[int] = attr.ib(default=None) + p99: int = attr.ib(kw_only=True, validator=validate_not_none) """ The 99th percentile response time in milliseconds. - https://developer.apple.com/documentation/retentionmessaging/p99 + https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ diff --git a/appstoreserverlibrary/models/PerformanceTestResultResponse.py b/appstoreserverlibrary/models/PerformanceTestResultResponse.py index bd246b8d..9d61579e 100644 --- a/appstoreserverlibrary/models/PerformanceTestResultResponse.py +++ b/appstoreserverlibrary/models/PerformanceTestResultResponse.py @@ -1,33 +1,44 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import Dict, Optional +from typing import Dict from attr import define, Attribute import attr -from .LibraryUtility import AttrsRawValueAware, metadata_key, metadata_type_key +from .LibraryUtility import AttrsRawValueAware, metadata_key, metadata_required_key, metadata_type_key, validate_not_none from .PerformanceTestConfig import PerformanceTestConfig from .PerformanceTestResponseTimes import PerformanceTestResponseTimes from .PerformanceTestStatus import PerformanceTestStatus from .SendAttemptResult import SendAttemptResult -def _failures_value_set(self, _: Attribute, value: Optional[Dict[SendAttemptResult, int]]): - new_raw = {k.value: v for k, v in value.items()} if value is not None else None +def _failures_value_set(self, attribute: Attribute, value: Dict[SendAttemptResult, int]): + if value is None: + raise ValueError(f"{attribute.name} cannot be set to None when field is required") + new_raw = {k.value: v for k, v in value.items()} if new_raw != getattr(self, 'rawFailures'): object.__setattr__(self, 'rawFailures', new_raw) return value -def _raw_failures_value_set(self, _: Attribute, value: Optional[Dict[str, int]]): +def _raw_failures_value_set(self, attribute: Attribute, value: Dict[str, int]): + if value is None: + raise ValueError(f"{attribute.name} cannot be None") new_typed = {} - if value is not None: - for k, v in value.items(): - if k in SendAttemptResult: - new_typed[SendAttemptResult(k)] = v - new_typed = new_typed if new_typed else None + for k, v in value.items(): + if k in SendAttemptResult: + new_typed[SendAttemptResult(k)] = v if new_typed != getattr(self, 'failures'): object.__setattr__(self, 'failures', new_typed) return value +def _raw_failures_default(self) -> Dict[str, int]: + if self.failures is not None: + return {k.value: v for k, v in self.failures.items()} + raise ValueError("Either failures or rawFailures must be provided") + +def _validate_raw_failures(self, attribute: Attribute, value: Dict[str, int]): + if value is None: + raise ValueError(f"{attribute.name} cannot be None") + @define class PerformanceTestResultResponse(AttrsRawValueAware): """ @@ -36,61 +47,61 @@ class PerformanceTestResultResponse(AttrsRawValueAware): https://developer.apple.com/documentation/retentionmessaging/performancetestresultresponse """ - config: Optional[PerformanceTestConfig] = attr.ib(default=None) + config: PerformanceTestConfig = attr.ib(kw_only=True, validator=validate_not_none) """ A PerformanceTestConfig object that enumerates the test parameters. https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ - target: Optional[str] = attr.ib(default=None) + failures: Dict[SendAttemptResult, int] = attr.ib(default=None, kw_only=True, on_setattr=_failures_value_set, metadata={metadata_key: 'rawFailures', metadata_type_key: 'main'}) """ - The target URL for the performance test. + A Failures object that represents a map of server-to-server notification failure reasons and counts that represent the number of failures encountered during the performance test. - https://developer.apple.com/documentation/retentionmessaging/target + https://developer.apple.com/documentation/retentionmessaging/failures """ - result: Optional[PerformanceTestStatus] = PerformanceTestStatus.create_main_attr('rawResult') + rawFailures: Dict[str, int] = attr.ib(default=attr.Factory(_raw_failures_default, takes_self=True), kw_only=True, on_setattr=_raw_failures_value_set, validator=_validate_raw_failures, metadata={metadata_key: 'failures', metadata_type_key: 'raw', metadata_required_key: True}) """ - A PerformanceTestStatus object that describes the overall result of the test. - - https://developer.apple.com/documentation/retentionmessaging/performanceteststatus + See failures """ - rawResult: Optional[str] = PerformanceTestStatus.create_raw_attr('result') - """ - See result + numPending: int = attr.ib(kw_only=True, validator=validate_not_none) """ + An integer that describes the number of pending requests in the performance test. - successRate: Optional[int] = attr.ib(default=None) + https://developer.apple.com/documentation/retentionmessaging/performancetestresultresponse """ - An integer that describes he success rate percentage of the performance test. - https://developer.apple.com/documentation/retentionmessaging/successrate + responseTimes: PerformanceTestResponseTimes = attr.ib(kw_only=True, validator=validate_not_none) """ + A PerformanceTestResponseTimes object that enumerates the response times measured during the test. - numPending: Optional[int] = attr.ib(default=None) + https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ - An integer that describes the number of pending requests in the performance test. - https://developer.apple.com/documentation/retentionmessaging/numpending + result: PerformanceTestStatus = PerformanceTestStatus.create_main_attr('rawResult', raw_required=True, kw_only=True) """ + A PerformanceTestStatus object that describes the overall result of the test. - responseTimes: Optional[PerformanceTestResponseTimes] = attr.ib(default=None) + https://developer.apple.com/documentation/retentionmessaging/performanceteststatus """ - A PerformanceTestResponseTimes object that enumerates the response times measured during the test. - https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes + rawResult: str = PerformanceTestStatus.create_raw_attr('result', required=True) + """ + See result """ - failures: Optional[Dict[SendAttemptResult, int]] = attr.ib(default=None, on_setattr=_failures_value_set, metadata={metadata_key: 'rawFailures', metadata_type_key: 'main'}) + successRate: int = attr.ib(kw_only=True, validator=validate_not_none) """ - A map of server-to-server notification failure reasons and counts that represent the number of failures encountered during the performance test. + An integer that describes the success rate percentage of the performance test. - https://developer.apple.com/documentation/retentionmessaging/failures + https://developer.apple.com/documentation/retentionmessaging/performancetestresultresponse """ - rawFailures: Optional[Dict[str, int]] = attr.ib(default=None, kw_only=True, on_setattr=_raw_failures_value_set, metadata={metadata_key: 'failures', metadata_type_key: 'raw'}) + target: str = attr.ib(kw_only=True, validator=validate_not_none) """ - See failures + The target URL for the performance test. + + https://developer.apple.com/documentation/retentionmessaging/performancetestresultresponse """ diff --git a/appstoreserverlibrary/models/PerformanceTestStatus.py b/appstoreserverlibrary/models/PerformanceTestStatus.py index 92c9501c..448da7d8 100644 --- a/appstoreserverlibrary/models/PerformanceTestStatus.py +++ b/appstoreserverlibrary/models/PerformanceTestStatus.py @@ -11,5 +11,16 @@ class PerformanceTestStatus(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): https://developer.apple.com/documentation/retentionmessaging/performanceteststatus """ PENDING = "PENDING" + """ + The test is still pending. + """ + PASS = "PASS" + """ + The test passed. + """ + FAIL = "FAIL" + """ + The test failed. + """ diff --git a/appstoreserverlibrary/models/Platform.py b/appstoreserverlibrary/models/Platform.py index ff380bb9..3a81c3d9 100644 --- a/appstoreserverlibrary/models/Platform.py +++ b/appstoreserverlibrary/models/Platform.py @@ -7,9 +7,20 @@ class Platform(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The platform on which the customer consumed the in-app purchase. - + https://developer.apple.com/documentation/appstoreserverapi/platform """ UNDECLARED = 0 + """ + The platform is undeclared. + """ + APPLE = 1 + """ + An Apple platform. + """ + NON_APPLE = 2 + """ + Non-Apple platform. + """ diff --git a/appstoreserverlibrary/models/PlayTime.py b/appstoreserverlibrary/models/PlayTime.py index 8bc3a093..54ecb374 100644 --- a/appstoreserverlibrary/models/PlayTime.py +++ b/appstoreserverlibrary/models/PlayTime.py @@ -7,14 +7,45 @@ class PlayTime(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ A value that indicates the amount of time that the customer used the app. - + https://developer.apple.com/documentation/appstoreserverapi/playtime """ UNDECLARED = 0 + """ + The engagement time is undeclared. + """ + ZERO_TO_FIVE_MINUTES = 1 + """ + The engagement time is between 0–5 minutes. + """ + FIVE_TO_SIXTY_MINUTES = 2 + """ + The engagement time is between 5–60 minutes. + """ + ONE_TO_SIX_HOURS = 3 + """ + The engagement time is between 1–6 hours. + """ + SIX_HOURS_TO_TWENTY_FOUR_HOURS = 4 + """ + The engagement time is between 6–24 hours. + """ + ONE_DAY_TO_FOUR_DAYS = 5 + """ + The engagement time is between 1–4 days. + """ + FOUR_DAYS_TO_SIXTEEN_DAYS = 6 + """ + The engagement time is between 4–16 days. + """ + OVER_SIXTEEN_DAYS = 7 + """ + The engagement time is over 16 days. + """ diff --git a/appstoreserverlibrary/models/PriceIncreaseStatus.py b/appstoreserverlibrary/models/PriceIncreaseStatus.py index f1ecfb5e..1311a3a1 100644 --- a/appstoreserverlibrary/models/PriceIncreaseStatus.py +++ b/appstoreserverlibrary/models/PriceIncreaseStatus.py @@ -7,8 +7,15 @@ class PriceIncreaseStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The status that indicates whether an auto-renewable subscription is subject to a price increase. - + https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus """ CUSTOMER_HAS_NOT_RESPONDED = 0 + """ + The customer hasn’t yet responded to an auto-renewable subscription price increase that requires customer consent. + """ + CUSTOMER_CONSENTED_OR_WAS_NOTIFIED_WITHOUT_NEEDING_CONSENT = 1 + """ + The customer consented to an auto-renewable subscription price increase that requires customer consent, or the App Store has notified the customer of an auto-renewable subscription price increase that doesn’t require consent. + """ diff --git a/appstoreserverlibrary/models/PromotionalOffer.py b/appstoreserverlibrary/models/PromotionalOffer.py index f980f24f..625312ad 100644 --- a/appstoreserverlibrary/models/PromotionalOffer.py +++ b/appstoreserverlibrary/models/PromotionalOffer.py @@ -16,21 +16,21 @@ class PromotionalOffer: https://developer.apple.com/documentation/retentionmessaging/promotionaloffer """ - messageIdentifier: Optional[UUID] = attr.ib(default=None) + messageIdentifier: Optional[UUID] = attr.ib(default=None, kw_only=True) """ The identifier of the message to display to the customer, along with the promotional offer. https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ - promotionalOfferSignatureV2: Optional[str] = attr.ib(default=None) + promotionalOfferSignatureV2: Optional[str] = attr.ib(default=None, kw_only=True) """ The promotional offer signature in V2 format. https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev2 """ - promotionalOfferSignatureV1: Optional[PromotionalOfferSignatureV1] = attr.ib(default=None) + promotionalOfferSignatureV1: Optional[PromotionalOfferSignatureV1] = attr.ib(default=None, kw_only=True) """ The promotional offer signature in V1 format. diff --git a/appstoreserverlibrary/models/PromotionalOfferSignatureV1.py b/appstoreserverlibrary/models/PromotionalOfferSignatureV1.py index 54a15867..48dffbec 100644 --- a/appstoreserverlibrary/models/PromotionalOfferSignatureV1.py +++ b/appstoreserverlibrary/models/PromotionalOfferSignatureV1.py @@ -6,47 +6,61 @@ from attr import define import attr +from .LibraryUtility import validate_not_none + @define class PromotionalOfferSignatureV1: """ - The promotional offer signature you generate using an earlier signature version. + The promotional-offer signature you generate using an earlier signature version. https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ - encodedSignature: str = attr.ib() + encodedSignature: str = attr.ib(kw_only=True, validator=validate_not_none) """ The Base64-encoded cryptographic signature you generate using the offer parameters. + + https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ - productId: str = attr.ib() + productId: str = attr.ib(kw_only=True, validator=validate_not_none) """ - The subscription's product identifier. + The subscription’s product identifier. https://developer.apple.com/documentation/retentionmessaging/productid """ - nonce: UUID = attr.ib() + nonce: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ A one-time-use UUID antireplay value you generate. + + https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ - timestamp: int = attr.ib() + timestamp: int = attr.ib(kw_only=True, validator=validate_not_none) """ The UNIX time, in milliseconds, when you generate the signature. + + https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ - keyId: str = attr.ib() + keyId: str = attr.ib(kw_only=True, validator=validate_not_none) """ A string that identifies the private key you use to generate the signature. + + https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ - offerIdentifier: str = attr.ib() + offerIdentifier: str = attr.ib(kw_only=True, validator=validate_not_none) """ The subscription offer identifier that you set up in App Store Connect. + + https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ - appAccountToken: Optional[UUID] = attr.ib(default=None) + appAccountToken: Optional[UUID] = attr.ib(default=None, kw_only=True) """ A UUID that you provide to associate with the transaction if the customer accepts the promotional offer. + + https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ diff --git a/appstoreserverlibrary/models/PurchasePlatform.py b/appstoreserverlibrary/models/PurchasePlatform.py index 9549c040..558ae630 100644 --- a/appstoreserverlibrary/models/PurchasePlatform.py +++ b/appstoreserverlibrary/models/PurchasePlatform.py @@ -6,11 +6,12 @@ class PurchasePlatform(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - Values that represent Apple platforms. - + The platform on which a customer originally purchases an app. + https://developer.apple.com/documentation/storekit/appstore/platform + https://developer.apple.com/documentation/appstoreserverapi/originalplatform """ - IOS = "iOS" MAC_OS = "macOS" + IOS = "iOS" TV_OS = "tvOS" VISION_OS = "visionOS" diff --git a/appstoreserverlibrary/models/RealtimeRequestBody.py b/appstoreserverlibrary/models/RealtimeRequestBody.py index 67f18047..df596619 100644 --- a/appstoreserverlibrary/models/RealtimeRequestBody.py +++ b/appstoreserverlibrary/models/RealtimeRequestBody.py @@ -1,6 +1,6 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. -from typing import Optional, Dict +from typing import Optional from attr import define import attr @@ -13,9 +13,9 @@ class RealtimeRequestBody: https://developer.apple.com/documentation/retentionmessaging/realtimerequestbody """ - signedPayload: Optional[str] = attr.ib(default=None) + signedPayload: Optional[str] = attr.ib(default=None, kw_only=True) """ The payload in JSON Web Signature (JWS) format, signed by the App Store. https://developer.apple.com/documentation/retentionmessaging/signedpayload - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/RealtimeResponseBody.py b/appstoreserverlibrary/models/RealtimeResponseBody.py index 15420e20..41b6a675 100644 --- a/appstoreserverlibrary/models/RealtimeResponseBody.py +++ b/appstoreserverlibrary/models/RealtimeResponseBody.py @@ -5,10 +5,10 @@ from attr import define import attr -from .Message import Message +from .AdvancedCommerceInfo import AdvancedCommerceInfo from .AlternateProduct import AlternateProduct +from .Message import Message from .PromotionalOffer import PromotionalOffer -from .AdvancedCommerceInfo import AdvancedCommerceInfo @define class RealtimeResponseBody: @@ -18,28 +18,28 @@ class RealtimeResponseBody: https://developer.apple.com/documentation/retentionmessaging/realtimeresponsebody """ - message: Optional[Message] = attr.ib(default=None) + message: Optional[Message] = attr.ib(default=None, kw_only=True) """ - A retention message that's text-based and can include an optional image. + A retention message that’s text-based and can include an optional image. https://developer.apple.com/documentation/retentionmessaging/message """ - alternateProduct: Optional[AlternateProduct] = attr.ib(default=None) + alternateProduct: Optional[AlternateProduct] = attr.ib(default=None, kw_only=True) """ A retention message with a switch-plan option. https://developer.apple.com/documentation/retentionmessaging/alternateproduct """ - promotionalOffer: Optional[PromotionalOffer] = attr.ib(default=None) + promotionalOffer: Optional[PromotionalOffer] = attr.ib(default=None, kw_only=True) """ A retention message that includes a promotional offer. https://developer.apple.com/documentation/retentionmessaging/promotionaloffer """ - advancedCommerceInfo: Optional[AdvancedCommerceInfo] = attr.ib(default=None) + advancedCommerceInfo: Optional[AdvancedCommerceInfo] = attr.ib(default=None, kw_only=True) """ A retention offer or switch plan option. diff --git a/appstoreserverlibrary/models/RealtimeUrlRequest.py b/appstoreserverlibrary/models/RealtimeUrlRequest.py index 36a64463..ef3962e1 100644 --- a/appstoreserverlibrary/models/RealtimeUrlRequest.py +++ b/appstoreserverlibrary/models/RealtimeUrlRequest.py @@ -3,6 +3,8 @@ from attr import define import attr +from .LibraryUtility import validate_not_none + @define class RealtimeUrlRequest: """ @@ -11,7 +13,7 @@ class RealtimeUrlRequest: https://developer.apple.com/documentation/retentionmessaging/realtimeurlrequest """ - realtimeURL: str = attr.ib(validator=attr.validators.max_len(256)) + realtimeURL: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(256)]) """ A string that contains the URL of your Get Retention Message endpoint for configuration. diff --git a/appstoreserverlibrary/models/RealtimeUrlResponse.py b/appstoreserverlibrary/models/RealtimeUrlResponse.py index 5cd75f07..bf36ab97 100644 --- a/appstoreserverlibrary/models/RealtimeUrlResponse.py +++ b/appstoreserverlibrary/models/RealtimeUrlResponse.py @@ -1,10 +1,10 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. -from typing import Optional - from attr import define import attr +from .LibraryUtility import validate_not_none + @define class RealtimeUrlResponse: """ @@ -13,7 +13,7 @@ class RealtimeUrlResponse: https://developer.apple.com/documentation/retentionmessaging/realtimeurlresponse """ - realtimeURL: Optional[str] = attr.ib() + realtimeURL: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(256)]) """ A string that contains the URL you provided for your Get Retention Message endpoint. diff --git a/appstoreserverlibrary/models/RefundHistoryResponse.py b/appstoreserverlibrary/models/RefundHistoryResponse.py index 97b15d2e..1b4e229f 100644 --- a/appstoreserverlibrary/models/RefundHistoryResponse.py +++ b/appstoreserverlibrary/models/RefundHistoryResponse.py @@ -1,34 +1,35 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List + from attr import define -from typing import List, Optional import attr @define -class RefundHistoryResponse: +class RefundHistoryResponse: """ A response that contains an array of signed JSON Web Signature (JWS) refunded transactions, and paging information. - + https://developer.apple.com/documentation/appstoreserverapi/refundhistoryresponse """ - signedTransactions: Optional[List[str]] = attr.ib(default=None) + hasMore: Optional[bool] = attr.ib(default=None, kw_only=True) """ - A list of up to 20 JWS transactions, or an empty array if the customer hasn't received any refunds in your app. The transactions are sorted in ascending order by revocationDate. - - https://developer.apple.com/documentation/appstoreserverapi/jwstransaction + A Boolean value that indicates whether the App Store has more transactions than it returns in signedTransactions. + + https://developer.apple.com/documentation/appstoreserverapi/hasmore """ - revision: Optional[str] = attr.ib(default=None) + revision: Optional[str] = attr.ib(default=None, kw_only=True) """ - A token you use in a query to request the next set of transactions for the customer. - + A token you provide in a query to request the next set of transactions from the Get Refund History endpoint. + https://developer.apple.com/documentation/appstoreserverapi/revision """ - hasMore: Optional[bool] = attr.ib(default=None) + signedTransactions: Optional[List[str]] = attr.ib(default=None, kw_only=True) + """ + A list of up to 20 JWS transactions, or an empty array if the customer hasn’t received any refunds in your app. + + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ - A Boolean value indicating whether the App Store has more transaction data. - - https://developer.apple.com/documentation/appstoreserverapi/hasmore - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/RefundPreference.py b/appstoreserverlibrary/models/RefundPreference.py index c20664e6..04b1817b 100644 --- a/appstoreserverlibrary/models/RefundPreference.py +++ b/appstoreserverlibrary/models/RefundPreference.py @@ -6,10 +6,21 @@ class RefundPreference(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund. + A value that indicates your preferred outcome for the refund request. https://developer.apple.com/documentation/appstoreserverapi/refundpreference """ DECLINE = "DECLINE" + """ + You prefer that the App Store declines the refund. + """ + GRANT_FULL = "GRANT_FULL" + """ + You prefer that the App Store grants the refund in full. + """ + GRANT_PRORATED = "GRANT_PRORATED" + """ + You prefer that the App Store grants a prorated refund. + """ diff --git a/appstoreserverlibrary/models/RefundPreferenceV1.py b/appstoreserverlibrary/models/RefundPreferenceV1.py index 80737e5d..6789973a 100644 --- a/appstoreserverlibrary/models/RefundPreferenceV1.py +++ b/appstoreserverlibrary/models/RefundPreferenceV1.py @@ -8,12 +8,27 @@ class RefundPreferenceV1(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ A value that indicates your preferred outcome for the refund request. - .. deprecated:: + .. deprecated:: 3.0.0 Use :class:`RefundPreference` instead. https://developer.apple.com/documentation/appstoreserverapi/refundpreferencev1 """ UNDECLARED = 0 + """ + The refund preference is undeclared. + """ + PREFER_GRANT = 1 + """ + You prefer that the App Store grants the refund. + """ + PREFER_DECLINE = 2 + """ + You prefer that the App Store declines the refund. + """ + NO_PREFERENCE = 3 + """ + You have no preference whether the App Store grants or declines the refund. + """ diff --git a/appstoreserverlibrary/models/RenewalCommitmentInfo.py b/appstoreserverlibrary/models/RenewalCommitmentInfo.py index cc830de1..868fe9b6 100644 --- a/appstoreserverlibrary/models/RenewalCommitmentInfo.py +++ b/appstoreserverlibrary/models/RenewalCommitmentInfo.py @@ -6,8 +6,8 @@ import attr from .AutoRenewStatus import AutoRenewStatus -from .RenewalBillingPlanType import RenewalBillingPlanType from .LibraryUtility import AttrsRawValueAware +from .RenewalBillingPlanType import RenewalBillingPlanType @define class RenewalCommitmentInfo(AttrsRawValueAware): @@ -15,12 +15,12 @@ class RenewalCommitmentInfo(AttrsRawValueAware): https://developer.apple.com/documentation/appstoreserverapi/renewalcommitmentinfo """ - commitmentAutoRenewProductId: Optional[str] = attr.ib(default=None) + commitmentAutoRenewProductId: Optional[str] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/commitmentautorenewproductid """ - commitmentAutoRenewStatus: Optional[AutoRenewStatus] = AutoRenewStatus.create_main_attr('rawCommitmentAutoRenewStatus') + commitmentAutoRenewStatus: Optional[AutoRenewStatus] = AutoRenewStatus.create_main_attr('rawCommitmentAutoRenewStatus', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/commitmentautorenewstatus """ @@ -30,7 +30,7 @@ class RenewalCommitmentInfo(AttrsRawValueAware): See commitmentAutoRenewStatus """ - commitmentRenewalBillingPlanType: Optional[RenewalBillingPlanType] = RenewalBillingPlanType.create_main_attr('rawCommitmentRenewalBillingPlanType') + commitmentRenewalBillingPlanType: Optional[RenewalBillingPlanType] = RenewalBillingPlanType.create_main_attr('rawCommitmentRenewalBillingPlanType', kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/commitmentrenewalbillingplantype """ @@ -40,12 +40,12 @@ class RenewalCommitmentInfo(AttrsRawValueAware): See commitmentRenewalBillingPlanType """ - commitmentRenewalDate: Optional[int] = attr.ib(default=None) + commitmentRenewalDate: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/commitmentrenewaldate """ - commitmentRenewalPrice: Optional[int] = attr.ib(default=None) + commitmentRenewalPrice: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/commitmentrenewalprice """ diff --git a/appstoreserverlibrary/models/ResponseBodyV2.py b/appstoreserverlibrary/models/ResponseBodyV2.py index f0ab04ac..0d6f9ed8 100644 --- a/appstoreserverlibrary/models/ResponseBodyV2.py +++ b/appstoreserverlibrary/models/ResponseBodyV2.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -11,10 +12,10 @@ class ResponseBodyV2: https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2 """ - - signedPayload: Optional[str] = attr.ib(default=None) + + signedPayload: Optional[str] = attr.ib(default=None, kw_only=True) """ - A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification. - + The payload in JSON Web Signature (JWS) format, signed by the App Store. + https://developer.apple.com/documentation/appstoreservernotifications/signedpayload """ diff --git a/appstoreserverlibrary/models/ResponseBodyV2DecodedPayload.py b/appstoreserverlibrary/models/ResponseBodyV2DecodedPayload.py index 855e89ab..844be123 100644 --- a/appstoreserverlibrary/models/ResponseBodyV2DecodedPayload.py +++ b/appstoreserverlibrary/models/ResponseBodyV2DecodedPayload.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -12,19 +13,18 @@ from .Subtype import Subtype from .Summary import Summary - @define class ResponseBodyV2DecodedPayload(AttrsRawValueAware): """ - A decoded payload containing the version 2 notification data. - + A decoded payload that contains the version 2 notification data. + https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload """ - notificationType: Optional[NotificationTypeV2] = NotificationTypeV2.create_main_attr('rawNotificationType') + notificationType: Optional[NotificationTypeV2] = NotificationTypeV2.create_main_attr('rawNotificationType', kw_only=True) """ The in-app purchase event for which the App Store sends this version 2 notification. - + https://developer.apple.com/documentation/appstoreservernotifications/notificationtype """ @@ -33,65 +33,63 @@ class ResponseBodyV2DecodedPayload(AttrsRawValueAware): See notificationType """ - subtype: Optional[Subtype] = Subtype.create_main_attr('rawSubtype') + subtype: Optional[Subtype] = Subtype.create_main_attr('rawSubtype', kw_only=True) """ - Additional information that identifies the notification event. The subtype field is present only for specific version 2 notifications. - + Additional information that identifies the notification event. + https://developer.apple.com/documentation/appstoreservernotifications/subtype """ - + rawSubtype: Optional[str] = Subtype.create_raw_attr('subtype') """ See subtype """ - notificationUUID: Optional[str] = attr.ib(default=None) + data: Optional[Data] = attr.ib(default=None, kw_only=True) """ - A unique identifier for the notification. - - https://developer.apple.com/documentation/appstoreservernotifications/notificationuuid + The object that contains the app metadata and signed renewal and transaction information. + + https://developer.apple.com/documentation/appstoreservernotifications/data """ - data: Optional[Data] = attr.ib(default=None) + summary: Optional[Summary] = attr.ib(default=None, kw_only=True) """ - The object that contains the app metadata and signed renewal and transaction information. - The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. - - https://developer.apple.com/documentation/appstoreservernotifications/data + The summary data that appears when the App Store server completes your request to extend a subscription renewal date for eligible subscribers. + + https://developer.apple.com/documentation/appstoreservernotifications/summary """ - version: Optional[str] = attr.ib(default=None) + externalPurchaseToken: Optional[ExternalPurchaseToken] = attr.ib(default=None, kw_only=True) """ - A string that indicates the notification's App Store Server Notifications version number. - - https://developer.apple.com/documentation/appstoreservernotifications/version + This field appears when the notificationType is EXTERNAL_PURCHASE_TOKEN. + + https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken """ - signedDate: Optional[int] = attr.ib(default=None) + appData: Optional[AppData] = attr.ib(default=None, kw_only=True) """ - The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. - - https://developer.apple.com/documentation/appstoreserverapi/signeddate + The object that contains the app metadata and signed app transaction information. + + https://developer.apple.com/documentation/appstoreservernotifications/appdata """ - summary: Optional[Summary] = attr.ib(default=None) + version: Optional[str] = attr.ib(default=None, kw_only=True) """ - The summary data that appears when the App Store server completes your request to extend a subscription renewal date for eligible subscribers. - The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. - - https://developer.apple.com/documentation/appstoreservernotifications/summary + The App Store Server Notification version number, "2.0". + + https://developer.apple.com/documentation/appstoreservernotifications/version """ - externalPurchaseToken: Optional[ExternalPurchaseToken] = attr.ib(default=None) + signedDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - This field appears when the notificationType is EXTERNAL_PURCHASE_TOKEN. - The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields. - - https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken + The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. + + https://developer.apple.com/documentation/appstoreservernotifications/signeddate """ - appData: Optional[AppData] = attr.ib(default=None) + + notificationUUID: Optional[str] = attr.ib(default=None, kw_only=True) """ - The object that contains the app metadata and signed app transaction information. This field appears when the notificationType is RESCIND_CONSENT. + A unique identifier for the notification. - https://developer.apple.com/documentation/appstoreservernotifications/appdata + https://developer.apple.com/documentation/appstoreservernotifications/notificationuuid """ diff --git a/appstoreserverlibrary/models/RevocationReason.py b/appstoreserverlibrary/models/RevocationReason.py index 5b1dd8e6..2f25a1b7 100644 --- a/appstoreserverlibrary/models/RevocationReason.py +++ b/appstoreserverlibrary/models/RevocationReason.py @@ -7,8 +7,15 @@ class RevocationReason(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The reason for a refunded transaction. - + https://developer.apple.com/documentation/appstoreserverapi/revocationreason """ REFUNDED_DUE_TO_ISSUE = 1 + """ + The App Store refunded the transaction on behalf of the customer due to an actual or perceived issue within your app. + """ + REFUNDED_FOR_OTHER_REASON = 0 + """ + The App Store refunded the transaction on behalf of the customer for other reasons, for example, an accidental purchase. + """ diff --git a/appstoreserverlibrary/models/RevocationType.py b/appstoreserverlibrary/models/RevocationType.py index d48f287b..6ef59f69 100644 --- a/appstoreserverlibrary/models/RevocationType.py +++ b/appstoreserverlibrary/models/RevocationType.py @@ -8,8 +8,24 @@ class RevocationType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The type of the refund or revocation that applies to the transaction. - https://developer.apple.com/documentation/appstoreservernotifications/revocationtype + https://developer.apple.com/documentation/appstoreserverapi/revocationtype """ REFUND_FULL = "REFUND_FULL" + """ + The transaction has a full refund. + """ + REFUND_PRORATED = "REFUND_PRORATED" + """ + The transaction has a prorated refund. + """ + FAMILY_REVOKE = "FAMILY_REVOKE" + """ + The transaction is revoked from Family Sharing. + """ + + ASSIGNMENT_REVOKE = "ASSIGNMENT_REVOKE" + """ + The organization or group purchaser removed the subscription from the customer. + """ diff --git a/appstoreserverlibrary/models/Role.py b/appstoreserverlibrary/models/Role.py new file mode 100644 index 00000000..10e9d24c --- /dev/null +++ b/appstoreserverlibrary/models/Role.py @@ -0,0 +1,21 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from enum import Enum + +from .LibraryUtility import AppStoreServerLibraryEnumMeta + +class Role(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): + """ + A string that identifies a customer’s role for a product within a group. + + https://developer.apple.com/documentation/appstoreserverapi/role + """ + NONE = "NONE" + """ + The customer has a seat in the group but doesn’t administer it. + """ + + ADMIN = "ADMIN" + """ + The customer administers the group for the product. + """ diff --git a/appstoreserverlibrary/models/RoleEntry.py b/appstoreserverlibrary/models/RoleEntry.py new file mode 100644 index 00000000..167d9e36 --- /dev/null +++ b/appstoreserverlibrary/models/RoleEntry.py @@ -0,0 +1,36 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from typing import Optional + +from attr import define +import attr + +from .LibraryUtility import AttrsRawValueAware +from .Role import Role + +@define +class RoleEntry(AttrsRawValueAware): + """ + A customer’s role for a single product within a group. + + https://developer.apple.com/documentation/appstoreserverapi/roleentry + """ + + productId: Optional[str] = attr.ib(default=None, kw_only=True) + """ + The product identifier of the in-app purchase that the role applies to. + + https://developer.apple.com/documentation/appstoreserverapi/productid + """ + + role: Optional[Role] = Role.create_main_attr('rawRole', kw_only=True) + """ + The customer’s role for the product. + + https://developer.apple.com/documentation/appstoreserverapi/role + """ + + rawRole: Optional[str] = Role.create_raw_attr('role') + """ + See role + """ diff --git a/appstoreserverlibrary/models/SendAttemptItem.py b/appstoreserverlibrary/models/SendAttemptItem.py index c7927e92..4ac1af07 100644 --- a/appstoreserverlibrary/models/SendAttemptItem.py +++ b/appstoreserverlibrary/models/SendAttemptItem.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -11,25 +12,25 @@ class SendAttemptItem(AttrsRawValueAware): """ The success or error information and the date the App Store server records when it attempts to send a server notification to your server. - + https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem """ - attemptDate: Optional[int] = attr.ib(default=None) + attemptDate: Optional[int] = attr.ib(default=None, kw_only=True) """ - The date the App Store server attempts to send a notification. - + The date the App Store server attempts to send the notification. + https://developer.apple.com/documentation/appstoreserverapi/attemptdate """ - sendAttemptResult: Optional[SendAttemptResult] = SendAttemptResult.create_main_attr('rawSendAttemptResult') + sendAttemptResult: Optional[SendAttemptResult] = SendAttemptResult.create_main_attr('rawSendAttemptResult', kw_only=True) """ The success or error information the App Store server records when it attempts to send an App Store server notification to your server. - + https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult """ - + rawSendAttemptResult: Optional[str] = SendAttemptResult.create_raw_attr('sendAttemptResult') """ See sendAttemptResult - """ \ No newline at end of file + """ diff --git a/appstoreserverlibrary/models/SendAttemptResult.py b/appstoreserverlibrary/models/SendAttemptResult.py index 5dbb7a20..b4bccb3b 100644 --- a/appstoreserverlibrary/models/SendAttemptResult.py +++ b/appstoreserverlibrary/models/SendAttemptResult.py @@ -7,17 +7,60 @@ class SendAttemptResult(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The success or error information the App Store server records when it attempts to send an App Store server notification to your server. - + https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult """ SUCCESS = "SUCCESS" + """ + The App Store server received a success response when it sent the notification to your server. + """ + TIMED_OUT = "TIMED_OUT" + """ + The App Store server didn’t get a response from your server and timed out. + """ + TLS_ISSUE = "TLS_ISSUE" + """ + The App Store server couldn’t establish a TLS session or validate your certificate. + """ + CIRCULAR_REDIRECT = "CIRCULAR_REDIRECT" + """ + The App Store server detected a continual redirect. + """ + NO_RESPONSE = "NO_RESPONSE" + """ + The App Store server didn’t receive a valid HTTP response from your server. + """ + SOCKET_ISSUE = "SOCKET_ISSUE" + """ + A network error caused the notification attempt to fail. + """ + UNSUPPORTED_CHARSET = "UNSUPPORTED_CHARSET" + """ + The App Store server doesn’t support the supplied charset. + """ + INVALID_RESPONSE = "INVALID_RESPONSE" + """ + The App Store server received an invalid response from your server. + """ + PREMATURE_CLOSE = "PREMATURE_CLOSE" + """ + The App Store server’s connection to your server was closed while the send was in progress. + """ + UNSUCCESSFUL_HTTP_RESPONSE_CODE = "UNSUCCESSFUL_HTTP_RESPONSE_CODE" + """ + The App Store server didn’t receive an HTTP 200 response from your server. + """ + OTHER = "OTHER" + """ + Another error occurred that prevented your server from receiving the notification. + """ diff --git a/appstoreserverlibrary/models/SendTestNotificationResponse.py b/appstoreserverlibrary/models/SendTestNotificationResponse.py index a493345f..80171315 100644 --- a/appstoreserverlibrary/models/SendTestNotificationResponse.py +++ b/appstoreserverlibrary/models/SendTestNotificationResponse.py @@ -1,20 +1,21 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define import attr @define -class SendTestNotificationResponse: +class SendTestNotificationResponse: """ A response that contains the test notification token. - + https://developer.apple.com/documentation/appstoreserverapi/sendtestnotificationresponse """ - testNotificationToken: Optional[str] = attr.ib(default=None) + testNotificationToken: Optional[str] = attr.ib(default=None, kw_only=True) """ - A unique identifier for a notification test that the App Store server sends to your server. - + The test notification token that uniquely identifies the notification test that App Store Server Notifications sends to your server. + https://developer.apple.com/documentation/appstoreserverapi/testnotificationtoken """ diff --git a/appstoreserverlibrary/models/Status.py b/appstoreserverlibrary/models/Status.py index 56abee2f..2d7441fd 100644 --- a/appstoreserverlibrary/models/Status.py +++ b/appstoreserverlibrary/models/Status.py @@ -7,11 +7,30 @@ class Status(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The status of an auto-renewable subscription. - + https://developer.apple.com/documentation/appstoreserverapi/status """ ACTIVE = 1 + """ + The auto-renewable subscription is active. + """ + EXPIRED = 2 + """ + The auto-renewable subscription is expired. + """ + BILLING_RETRY = 3 + """ + The auto-renewable subscription is in a billing retry period. + """ + BILLING_GRACE_PERIOD = 4 + """ + The auto-renewable subscription is in a Billing Grace Period. + """ + REVOKED = 5 + """ + The auto-renewable subscription is revoked. + """ diff --git a/appstoreserverlibrary/models/StatusResponse.py b/appstoreserverlibrary/models/StatusResponse.py index 3492f14b..c7f1b20a 100644 --- a/appstoreserverlibrary/models/StatusResponse.py +++ b/appstoreserverlibrary/models/StatusResponse.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional, List from attr import define @@ -11,40 +12,40 @@ @define class StatusResponse(AttrsRawValueAware): """ - A response that contains status information for all of a customer's auto-renewable subscriptions in your app. - + A response that contains status information for all of a customer’s auto-renewable subscriptions in your app. + https://developer.apple.com/documentation/appstoreserverapi/statusresponse """ - environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') + data: Optional[List[SubscriptionGroupIdentifierItem]] = attr.ib(default=None, kw_only=True) + """ + An array of information for auto-renewable subscriptions, including App Store-signed transaction information and App Store-signed renewal information. + + https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem + """ + + environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', kw_only=True) """ The server environment, sandbox or production, in which the App Store generated the response. - + https://developer.apple.com/documentation/appstoreserverapi/environment """ - + rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') """ See environment """ - bundleId: Optional[str] = attr.ib(default=None) - """ - The bundle identifier of an app. - - https://developer.apple.com/documentation/appstoreserverapi/bundleid + appAppleId: Optional[int] = attr.ib(default=None, kw_only=True) """ + Your app’s App Store identifier. - appAppleId: Optional[int] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreserverapi/appappleid """ - The unique identifier of an app in the App Store. - - https://developer.apple.com/documentation/appstoreservernotifications/appappleid + + bundleId: Optional[str] = attr.ib(default=None, kw_only=True) """ + Your app’s bundle identifier. - data: Optional[List[SubscriptionGroupIdentifierItem]] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreserverapi/bundleid """ - An array of information for auto-renewable subscriptions, including App Store-signed transaction information and App Store-signed renewal information. - - https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/StoreType.py b/appstoreserverlibrary/models/StoreType.py new file mode 100644 index 00000000..df803425 --- /dev/null +++ b/appstoreserverlibrary/models/StoreType.py @@ -0,0 +1,16 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from enum import Enum + +from .LibraryUtility import AppStoreServerLibraryEnumMeta + +class StoreType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): + """ + A string that describes the store the customer obtained the app from. + + https://developer.apple.com/documentation/appstoreserverapi/storetype + """ + CONSUMER = "CONSUMER" + """ + The customer obtained the app from the App Store. + """ diff --git a/appstoreserverlibrary/models/SubscriptionGroupIdentifierItem.py b/appstoreserverlibrary/models/SubscriptionGroupIdentifierItem.py index a95f5c75..528f09d5 100644 --- a/appstoreserverlibrary/models/SubscriptionGroupIdentifierItem.py +++ b/appstoreserverlibrary/models/SubscriptionGroupIdentifierItem.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional, List from attr import define @@ -7,23 +8,23 @@ from .LastTransactionsItem import LastTransactionsItem @define -class SubscriptionGroupIdentifierItem: +class SubscriptionGroupIdentifierItem: """ Information for auto-renewable subscriptions, including signed transaction information and signed renewal information, for one subscription group. - + https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem """ - subscriptionGroupIdentifier: Optional[str] = attr.ib(default=None) + subscriptionGroupIdentifier: Optional[str] = attr.ib(default=None, kw_only=True) """ - The identifier of the subscription group that the subscription belongs to. - + The subscription group identifier of the auto-renewable subscriptions in the lastTransactions array. + https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier - """ + """ - lastTransactions: Optional[List[LastTransactionsItem]] = attr.ib(default=None) + lastTransactions: Optional[List[LastTransactionsItem]] = attr.ib(default=None, kw_only=True) """ An array of the most recent App Store-signed transaction information and App Store-signed renewal information for all auto-renewable subscriptions in the subscription group. - + https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem """ diff --git a/appstoreserverlibrary/models/Subtype.py b/appstoreserverlibrary/models/Subtype.py index 04879edf..5149c67b 100644 --- a/appstoreserverlibrary/models/Subtype.py +++ b/appstoreserverlibrary/models/Subtype.py @@ -7,25 +7,100 @@ class Subtype(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ A string that provides details about select notification types in version 2. - + https://developer.apple.com/documentation/appstoreservernotifications/subtype """ INITIAL_BUY = "INITIAL_BUY" + """ + Applies to the SUBSCRIBED notificationType. + """ + RESUBSCRIBE = "RESUBSCRIBE" + """ + Applies to the SUBSCRIBED notificationType. + """ + DOWNGRADE = "DOWNGRADE" + """ + Applies to the DID_CHANGE_RENEWAL_PREF and OFFER_REDEEMED notificationType. + """ + UPGRADE = "UPGRADE" + """ + Applies to the DID_CHANGE_RENEWAL_PREF and OFFER_REDEEMED notificationType. + """ + AUTO_RENEW_ENABLED = "AUTO_RENEW_ENABLED" + """ + Applies to the DID_CHANGE_RENEWAL_STATUS notificationType. + """ + AUTO_RENEW_DISABLED = "AUTO_RENEW_DISABLED" + """ + Applies to the DID_CHANGE_RENEWAL_STATUS notificationType. + """ + VOLUNTARY = "VOLUNTARY" + """ + Applies to the EXPIRED notificationType. + """ + BILLING_RETRY = "BILLING_RETRY" + """ + Applies to the EXPIRED notificationType. + """ + PRICE_INCREASE = "PRICE_INCREASE" + """ + Applies to the EXPIRED notificationType. + """ + GRACE_PERIOD = "GRACE_PERIOD" + """ + Applies to the DID_FAIL_TO_RENEW notificationType. + """ + PENDING = "PENDING" + """ + Applies to the PRICE_INCREASE notificationType. + """ + ACCEPTED = "ACCEPTED" + """ + Applies to the PRICE_INCREASE notificationType. + """ + BILLING_RECOVERY = "BILLING_RECOVERY" + """ + Applies to the DID_RENEW notificationType. + """ + PRODUCT_NOT_FOR_SALE = "PRODUCT_NOT_FOR_SALE" + """ + Applies to the EXPIRED notificationType. + """ + SUMMARY = "SUMMARY" + """ + Applies to the RENEWAL_EXTENSION notificationType. + """ + FAILURE = "FAILURE" + """ + Applies to the RENEWAL_EXTENSION notificationType. + """ + UNREPORTED = "UNREPORTED" - ACTIVE_TOKEN_REMINDER = "ACTIVE_TOKEN_REMINDER" + """ + Applies to the EXTERNAL_PURCHASE_TOKEN notificationType. + """ + CREATED = "CREATED" + """ + Applies to the EXTERNAL_PURCHASE_TOKEN notificationType. + """ + + ACTIVE_TOKEN_REMINDER = "ACTIVE_TOKEN_REMINDER" + """ + Applies to the EXTERNAL_PURCHASE_TOKEN notificationType. + """ diff --git a/appstoreserverlibrary/models/Summary.py b/appstoreserverlibrary/models/Summary.py index 708d8281..442b43a0 100644 --- a/appstoreserverlibrary/models/Summary.py +++ b/appstoreserverlibrary/models/Summary.py @@ -1,7 +1,9 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Optional, List +from uuid import UUID + from attr import define -from typing import List, Optional import attr from .Environment import Environment @@ -11,67 +13,67 @@ class Summary(AttrsRawValueAware): """ The payload data for a subscription-renewal-date extension notification. - + https://developer.apple.com/documentation/appstoreservernotifications/summary """ - environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') + requestIdentifier: Optional[UUID] = attr.ib(default=None, kw_only=True) + """ + The UUID that represents a specific request to extend a subscription renewal date. + + https://developer.apple.com/documentation/appstoreservernotifications/requestidentifier + """ + + environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', kw_only=True) """ The server environment that the notification applies to, either sandbox or production. - + https://developer.apple.com/documentation/appstoreservernotifications/environment """ - + rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') """ See environment """ - appAppleId: Optional[int] = attr.ib(default=None) + appAppleId: Optional[int] = attr.ib(default=None, kw_only=True) """ - The unique identifier of an app in the App Store. - + The unique identifier of the app that the notification applies to. + https://developer.apple.com/documentation/appstoreservernotifications/appappleid """ - bundleId: Optional[str] = attr.ib(default=None) + bundleId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The bundle identifier of an app. - - https://developer.apple.com/documentation/appstoreserverapi/bundleid + The bundle identifier of the app. + + https://developer.apple.com/documentation/appstoreservernotifications/bundleid """ - productId: Optional[str] = attr.ib(default=None) + productId: Optional[str] = attr.ib(default=None, kw_only=True) """ - The unique identifier for the product, that you create in App Store Connect. - - https://developer.apple.com/documentation/appstoreserverapi/productid - """ + The product identifier of the auto-renewable subscription that the subscription-renewal-date extension applies to. - requestIdentifier: Optional[str] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreservernotifications/productid """ - A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. - - https://developer.apple.com/documentation/appstoreserverapi/requestidentifier + + storefrontCountryCodes: Optional[List[str]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(attr.validators.deep_iterable(attr.validators.max_len(3)))) """ + A list of country codes that limits the App Store’s attempt to apply the subscription-renewal-date extension. - storefrontCountryCodes: Optional[List[str]] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreservernotifications/storefrontcountrycodes """ - A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension. - - https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes + + failedCount: Optional[int] = attr.ib(default=None, kw_only=True) """ + The final count of subscriptions that fail to receive a subscription-renewal-date extension. - succeededCount: Optional[int] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreservernotifications/failedcount """ - The count of subscriptions that successfully receive a subscription-renewal-date extension. - - https://developer.apple.com/documentation/appstoreserverapi/succeededcount + + succeededCount: Optional[int] = attr.ib(default=None, kw_only=True) """ + The final count of subscriptions that successfully receive a subscription-renewal-date extension. - failedCount: Optional[int] = attr.ib(default=None) + https://developer.apple.com/documentation/appstoreservernotifications/succeededcount """ - The count of subscriptions that fail to receive a subscription-renewal-date extension. - - https://developer.apple.com/documentation/appstoreserverapi/failedcount - """ \ No newline at end of file diff --git a/appstoreserverlibrary/models/TokenType.py b/appstoreserverlibrary/models/TokenType.py index 2b028b50..89c3c0eb 100644 --- a/appstoreserverlibrary/models/TokenType.py +++ b/appstoreserverlibrary/models/TokenType.py @@ -10,5 +10,17 @@ class TokenType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): https://developer.apple.com/documentation/appstoreservernotifications/tokentype """ - SERVICES = "SERVICES" ACQUISITION = "ACQUISITION" + """ + A token type that indicates an initial acquisition. + """ + + SERVICES = "SERVICES" + """ + A token type that indicates usage of App Store services. + """ + + CORE_TECHNOLOGY = "CORE_TECHNOLOGY" + """ + A token type an app receives from the MarketplaceKit token(for:) API. + """ diff --git a/appstoreserverlibrary/models/TransactionCommitmentInfo.py b/appstoreserverlibrary/models/TransactionCommitmentInfo.py index 13bd9a0b..d2816fb7 100644 --- a/appstoreserverlibrary/models/TransactionCommitmentInfo.py +++ b/appstoreserverlibrary/models/TransactionCommitmentInfo.py @@ -5,30 +5,28 @@ from attr import define import attr -from .HelperValidationUtils import HelperValidationUtils - @define class TransactionCommitmentInfo: """ https://developer.apple.com/documentation/appstoreserverapi/transactioncommitmentinfo """ - billingPeriodNumber: Optional[int] = attr.ib(default=None, validator=attr.validators.optional(HelperValidationUtils.period_count_validator)) + billingPeriodNumber: Optional[int] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(attr.validators.and_(attr.validators.ge(1), attr.validators.le(12)))) """ https://developer.apple.com/documentation/appstoreserverapi/billingperiodnumber """ - commitmentExpiresDate: Optional[int] = attr.ib(default=None) + commitmentExpiresDate: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/commitmentexpiresdate """ - commitmentPrice: Optional[int] = attr.ib(default=None) + commitmentPrice: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/commitmentprice """ - totalBillingPeriods: Optional[int] = attr.ib(default=None) + totalBillingPeriods: Optional[int] = attr.ib(default=None, kw_only=True) """ https://developer.apple.com/documentation/appstoreserverapi/totalbillingperiods """ diff --git a/appstoreserverlibrary/models/TransactionHistoryRequest.py b/appstoreserverlibrary/models/TransactionHistoryRequest.py index 15bc27af..d2a5d772 100644 --- a/appstoreserverlibrary/models/TransactionHistoryRequest.py +++ b/appstoreserverlibrary/models/TransactionHistoryRequest.py @@ -6,65 +6,66 @@ from .InAppOwnershipType import InAppOwnershipType +from .LibraryUtility import AppStoreServerLibraryEnumMeta -class ProductType(str, Enum): +class ProductType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): AUTO_RENEWABLE = "AUTO_RENEWABLE" NON_RENEWABLE = "NON_RENEWABLE" CONSUMABLE = "CONSUMABLE" NON_CONSUMABLE = "NON_CONSUMABLE" -class Order(str, Enum): +class Order(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): ASCENDING = "ASCENDING" DESCENDING = "DESCENDING" @attr.define class TransactionHistoryRequest: - startDate: Optional[int] = attr.ib(default=None) + startDate: Optional[int] = attr.ib(default=None, kw_only=True) """ An optional start date of the timespan for the transaction history records you're requesting. The startDate must precede the endDate if you specify both dates. To be included in results, the transaction's purchaseDate must be equal to or greater than the startDate. https://developer.apple.com/documentation/appstoreserverapi/startdate """ - endDate: Optional[int] = attr.ib(default=None) + endDate: Optional[int] = attr.ib(default=None, kw_only=True) """ An optional end date of the timespan for the transaction history records you're requesting. Choose an endDate that's later than the startDate if you specify both dates. Using an endDate in the future is valid. To be included in results, the transaction's purchaseDate must be less than the endDate. https://developer.apple.com/documentation/appstoreserverapi/enddate """ - productIds: Optional[List[str]] = attr.ib(default=None) + productIds: Optional[List[str]] = attr.ib(default=None, kw_only=True) """ An optional filter that indicates the product identifier to include in the transaction history. Your query may specify more than one productID. https://developer.apple.com/documentation/appstoreserverapi/productid """ - productTypes: Optional[List[ProductType]] = attr.ib(default=None) + productTypes: Optional[List[ProductType]] = attr.ib(default=None, kw_only=True) """ An optional filter that indicates the product type to include in the transaction history. Your query may specify more than one productType. """ - sort: Optional[Order] = attr.ib(default=None) + sort: Optional[Order] = attr.ib(default=None, kw_only=True) """ An optional sort order for the transaction history records. The response sorts the transaction records by their recently modified date. The default value is ASCENDING, so you receive the oldest records first. """ - subscriptionGroupIdentifiers: Optional[List[str]] = attr.ib(default=None) + subscriptionGroupIdentifiers: Optional[List[str]] = attr.ib(default=None, kw_only=True) """ An optional filter that indicates the subscription group identifier to include in the transaction history. Your query may specify more than one subscriptionGroupIdentifier. https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier """ - inAppOwnershipType: Optional[InAppOwnershipType] = attr.ib(default=None) + inAppOwnershipType: Optional[InAppOwnershipType] = attr.ib(default=None, kw_only=True) """ An optional filter that limits the transaction history by the in-app ownership type. https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype """ - revoked: Optional[bool] = attr.ib(default=None) + revoked: Optional[bool] = attr.ib(default=None, kw_only=True) """ An optional Boolean value that indicates whether the response includes only revoked transactions when the value is true, or contains only nonrevoked transactions when the value is false. By default, the request doesn't include this parameter. """ diff --git a/appstoreserverlibrary/models/TransactionInfoResponse.py b/appstoreserverlibrary/models/TransactionInfoResponse.py index ecb11f21..a61956b7 100644 --- a/appstoreserverlibrary/models/TransactionInfoResponse.py +++ b/appstoreserverlibrary/models/TransactionInfoResponse.py @@ -1,4 +1,5 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. + from typing import Optional from attr import define @@ -8,13 +9,13 @@ class TransactionInfoResponse: """ A response that contains signed transaction information for a single transaction. - + https://developer.apple.com/documentation/appstoreserverapi/transactioninforesponse """ - - signedTransactionInfo: Optional[str] = attr.ib(default=None) + + signedTransactionInfo: Optional[str] = attr.ib(default=None, kw_only=True) """ A customer’s in-app purchase transaction, signed by Apple, in JSON Web Signature (JWS) format. - + https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ diff --git a/appstoreserverlibrary/models/TransactionReason.py b/appstoreserverlibrary/models/TransactionReason.py index 478875ac..30877e88 100644 --- a/appstoreserverlibrary/models/TransactionReason.py +++ b/appstoreserverlibrary/models/TransactionReason.py @@ -1,13 +1,21 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum + from .LibraryUtility import AppStoreServerLibraryEnumMeta class TransactionReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The cause of a purchase transaction, which indicates whether it’s a customer’s purchase or a renewal for an auto-renewable subscription that the system initiates. - + https://developer.apple.com/documentation/appstoreserverapi/transactionreason """ PURCHASE = "PURCHASE" + """ + The customer initiated the purchase, which may be for any in-app purchase type: consumable, non-consumable, non-renewing subscription, or auto-renewable subscription. + """ + RENEWAL = "RENEWAL" + """ + The App Store server initiated the purchase transaction to renew an auto-renewable subscription. + """ diff --git a/appstoreserverlibrary/models/Type.py b/appstoreserverlibrary/models/Type.py index d91e08b1..1f5a686d 100644 --- a/appstoreserverlibrary/models/Type.py +++ b/appstoreserverlibrary/models/Type.py @@ -1,15 +1,31 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum + from .LibraryUtility import AppStoreServerLibraryEnumMeta class Type(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ - The type of in-app purchase products you can offer in your app. - + The type of In-App Purchase products you can offer in your app. + https://developer.apple.com/documentation/appstoreserverapi/type """ AUTO_RENEWABLE_SUBSCRIPTION = "Auto-Renewable Subscription" + """ + An auto-renewable subscription + """ + NON_CONSUMABLE = "Non-Consumable" + """ + A non-consumable In-App Purchase + """ + CONSUMABLE = "Consumable" - NON_RENEWING_SUBSCRIPTION ="Non-Renewing Subscription" + """ + A consumable In-App Purchase + """ + + NON_RENEWING_SUBSCRIPTION = "Non-Renewing Subscription" + """ + A non-renewing subscription + """ diff --git a/appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py b/appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py index b3e8163a..1b7502d2 100644 --- a/appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py +++ b/appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py @@ -1,7 +1,12 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. + +from uuid import UUID + from attr import define import attr +from .LibraryUtility import validate_not_none + @define class UpdateAppAccountTokenRequest: """ @@ -10,9 +15,9 @@ class UpdateAppAccountTokenRequest: https://developer.apple.com/documentation/appstoreserverapi/updateappaccounttokenrequest """ - appAccountToken: str = attr.ib() + appAccountToken: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ - The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. + A UUID value to associate with the transaction you indicate in the endpoint parameter. https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken """ diff --git a/appstoreserverlibrary/models/UploadMessageImage.py b/appstoreserverlibrary/models/UploadMessageImage.py index 8f828e17..22065553 100644 --- a/appstoreserverlibrary/models/UploadMessageImage.py +++ b/appstoreserverlibrary/models/UploadMessageImage.py @@ -5,6 +5,8 @@ from attr import define import attr +from .LibraryUtility import validate_not_none + @define class UploadMessageImage: """ @@ -13,14 +15,14 @@ class UploadMessageImage: https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage """ - imageIdentifier: UUID = attr.ib() + imageIdentifier: UUID = attr.ib(kw_only=True, validator=validate_not_none) """ The unique identifier of an image. https://developer.apple.com/documentation/retentionmessaging/imageidentifier """ - altText: str = attr.ib(validator=attr.validators.max_len(150)) + altText: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(150)]) """ The alternative text you provide for the corresponding image. diff --git a/appstoreserverlibrary/models/UploadMessageRequestBody.py b/appstoreserverlibrary/models/UploadMessageRequestBody.py index 2cc17f67..38d64414 100644 --- a/appstoreserverlibrary/models/UploadMessageRequestBody.py +++ b/appstoreserverlibrary/models/UploadMessageRequestBody.py @@ -1,45 +1,52 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. -from typing import List, Optional +from typing import Optional, List from attr import define import attr from .BulletPoint import BulletPoint from .HeaderPosition import HeaderPosition -from .LibraryUtility import AttrsRawValueAware +from .LibraryUtility import AttrsRawValueAware, validate_not_none from .UploadMessageImage import UploadMessageImage @define class UploadMessageRequestBody(AttrsRawValueAware): """ - The request body for uploading a message, which includes the message text and an optional image reference. + The request body for uploading a message, which includes the message text and an optional image reference and bullet points. https://developer.apple.com/documentation/retentionmessaging/uploadmessagerequestbody """ - header: str = attr.ib(validator=attr.validators.max_len(66)) + header: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(66)]) """ The header text of the retention message that the system displays to customers. https://developer.apple.com/documentation/retentionmessaging/header """ - body: str = attr.ib(validator=attr.validators.max_len(144)) + body: str = attr.ib(kw_only=True, validator=[validate_not_none, attr.validators.max_len(144)]) """ The body text of the retention message that the system displays to customers. https://developer.apple.com/documentation/retentionmessaging/body """ - image: Optional[UploadMessageImage] = attr.ib(default=None) + image: Optional[UploadMessageImage] = attr.ib(default=None, kw_only=True) """ The optional image identifier and its alternative text to appear as part of a text-based message with an image. https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage """ - headerPosition: Optional[HeaderPosition] = HeaderPosition.create_main_attr('rawHeaderPosition') + bulletPoints: Optional[List[BulletPoint]] = attr.ib(default=None, kw_only=True, validator=attr.validators.optional(attr.validators.max_len(5))) + """ + An optional array of up to five bullet points. + + https://developer.apple.com/documentation/retentionmessaging/bulletpoint + """ + + headerPosition: Optional[HeaderPosition] = HeaderPosition.create_main_attr('rawHeaderPosition', kw_only=True) """ The position of header text, which defaults to placing header text above the body. @@ -50,10 +57,3 @@ class UploadMessageRequestBody(AttrsRawValueAware): """ See headerPosition """ - - bulletPoints: Optional[List[BulletPoint]] = attr.ib(default=None, validator=attr.validators.optional(attr.validators.max_len(5))) - """ - An optional array of bullet points. - - https://developer.apple.com/documentation/retentionmessaging/bulletpoint - """ diff --git a/appstoreserverlibrary/models/UserStatus.py b/appstoreserverlibrary/models/UserStatus.py index 2ac617a6..72d6cfbd 100644 --- a/appstoreserverlibrary/models/UserStatus.py +++ b/appstoreserverlibrary/models/UserStatus.py @@ -6,12 +6,31 @@ class UserStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ - The status of a customer's account within your app. - + The status of a customer’s account within your app. + https://developer.apple.com/documentation/appstoreserverapi/userstatus """ UNDECLARED = 0 + """ + Account status is undeclared. + """ + ACTIVE = 1 + """ + The customer’s account is active. + """ + SUSPENDED = 2 + """ + The customer’s account is suspended. + """ + TERMINATED = 3 + """ + The customer’s account is terminated. + """ + LIMITED_ACCESS = 4 + """ + The customer’s account has limited access. + """ diff --git a/appstoreserverlibrary/models/__init__.py b/appstoreserverlibrary/models/__init__.py index e69de29b..8f5faa26 100644 --- a/appstoreserverlibrary/models/__init__.py +++ b/appstoreserverlibrary/models/__init__.py @@ -0,0 +1 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. diff --git a/appstoreserverlibrary/promotional_offer.py b/appstoreserverlibrary/promotional_offer.py index 3450406a..867d75ee 100644 --- a/appstoreserverlibrary/promotional_offer.py +++ b/appstoreserverlibrary/promotional_offer.py @@ -16,11 +16,11 @@ def __init__(self, signing_key: bytes, key_id: str, bundle_id: str): self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend()) self._key_id = key_id self._bundle_id = bundle_id - def create_signature(self, product_identifier: str, subscription_offer_id: str, application_username: str, nonce: uuid.UUID, timestamp: int): + def create_signature(self, product_identifier: str, subscription_offer_id: str, application_username: str, nonce: uuid.UUID, timestamp: int) -> str: """ Return the Base64 encoded signature - https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers + https://developer.apple.com/documentation/storekit/generating-a-signature-for-promotional-offers :param product_identifier: The subscription product identifier :param subscription_offer_id: The subscription discount identifier diff --git a/appstoreserverlibrary/receipt_utility.py b/appstoreserverlibrary/receipt_utility.py index be47cc30..5e2c31a2 100644 --- a/appstoreserverlibrary/receipt_utility.py +++ b/appstoreserverlibrary/receipt_utility.py @@ -4,7 +4,6 @@ from typing import Optional import asn1 -import base64 import re PKCS7_OID = "1.2.840.113549.1.7.2" @@ -24,7 +23,7 @@ def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[ Extracts a transaction id from an encoded App Receipt. Throws if the receipt does not match the expected format. *NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API. - :param appReceipt: The unmodified app receipt + :param app_receipt: The unmodified app receipt :return: A transaction id from the array of in-app purchases, null if the receipt contains no in-app purchases """ try: @@ -49,16 +48,25 @@ def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[ def extract_transaction_id_from_transaction_receipt(self, transaction_receipt: str) -> Optional[str]: """ - Extracts a transaction id from an encoded transactional receipt. Throws if the receipt does not match the expected format. + Extracts a transaction id from an encoded transactional receipt. *NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API. - :param transactionReceipt: The unmodified transactionReceipt - :return: A transaction id, or null if no transactionId is found in the receipt + + :param transaction_receipt: The unmodified transactionReceipt + :return: A transaction id, or None if the receipt is well-formed but contains no transaction id + :raises ValueError: If the receipt is malformed: not valid base64, no "purchase-info" in the receipt, or otherwise not in the expected format """ - decoded_top_level = base64.b64decode(transaction_receipt).decode('utf-8') + try: + decoded_top_level = b64decode(transaction_receipt, validate=True).decode('utf-8') + except Exception as e: + raise ValueError(e) matching_result = re.search(r'"purchase-info"\s+=\s+"([a-zA-Z0-9+/=]+)";', decoded_top_level) - if matching_result: - decoded_inner_level = base64.b64decode(matching_result.group(1)).decode('utf-8') - inner_matching_result = re.search(r'"transaction-id"\s+=\s+"([a-zA-Z0-9+/=]+)";', decoded_inner_level) - if inner_matching_result: - return inner_matching_result.group(1) + if not matching_result: + raise ValueError() + try: + decoded_inner_level = b64decode(matching_result.group(1), validate=True).decode('utf-8') + except Exception as e: + raise ValueError(e) + inner_matching_result = re.search(r'"transaction-id"\s+=\s+"([a-zA-Z0-9+/=]+)";', decoded_inner_level) + if inner_matching_result: + return inner_matching_result.group(1) return None diff --git a/appstoreserverlibrary/signed_data_verifier.py b/appstoreserverlibrary/signed_data_verifier.py index 944fee55..4ec3e118 100644 --- a/appstoreserverlibrary/signed_data_verifier.py +++ b/appstoreserverlibrary/signed_data_verifier.py @@ -1,6 +1,6 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Tuple, Type, TypeVar from base64 import b64decode from enum import IntEnum import time @@ -25,6 +25,8 @@ from .models.JWSTransactionDecodedPayload import JWSTransactionDecodedPayload from .models.JWSRenewalInfoDecodedPayload import JWSRenewalInfoDecodedPayload +T = TypeVar('T') + class SignedDataVerifier: """ A class providing utility methods for verifying and decoding App Store signed data. @@ -55,12 +57,12 @@ def verify_and_decode_renewal_info(self, signed_renewal_info: str) -> JWSRenewal :throws VerificationException: Thrown if the data could not be verified """ - decoded_renewal_info = _get_cattrs_converter(JWSRenewalInfoDecodedPayload).structure(self._decode_signed_object(signed_renewal_info), JWSRenewalInfoDecodedPayload) + decoded_renewal_info = self._decode_and_structure_signed_object(signed_renewal_info, JWSRenewalInfoDecodedPayload) if decoded_renewal_info.environment != self._environment: raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT) return decoded_renewal_info - def verify_and_decode_signed_transaction(self, signed_transaction: str) -> JWSTransactionDecodedPayload: + def verify_and_decode_transaction(self, signed_transaction: str) -> JWSTransactionDecodedPayload: """ Verifies and decodes a signedTransaction obtained from the App Store Server API, an App Store Server Notification, or from a device See https://developer.apple.com/documentation/appstoreserverapi/jwstransaction @@ -69,7 +71,7 @@ def verify_and_decode_signed_transaction(self, signed_transaction: str) -> JWSTr :return: The decoded transaction info after verification :throws VerificationException: Thrown if the data could not be verified """ - decoded_transaction_info = _get_cattrs_converter(JWSTransactionDecodedPayload).structure(self._decode_signed_object(signed_transaction), JWSTransactionDecodedPayload) + decoded_transaction_info = self._decode_and_structure_signed_object(signed_transaction, JWSTransactionDecodedPayload) if decoded_transaction_info.bundleId != self._bundle_id: raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) if decoded_transaction_info.environment != self._environment: @@ -81,12 +83,11 @@ def verify_and_decode_notification(self, signed_payload: str) -> ResponseBodyV2D Verifies and decodes an App Store Server Notification signedPayload See https://developer.apple.com/documentation/appstoreservernotifications/signedpayload - :param signedPayload: The payload received by your server + :param signed_payload: The payload received by your server :return: The decoded payload after verification :throws VerificationException: Thrown if the data could not be verified """ - decoded_dict = self._decode_signed_object(signed_payload) - decoded_signed_notification = _get_cattrs_converter(ResponseBodyV2DecodedPayload).structure(decoded_dict, ResponseBodyV2DecodedPayload) + decoded_signed_notification = self._decode_and_structure_signed_object(signed_payload, ResponseBodyV2DecodedPayload) bundle_id = None app_apple_id = None environment = None @@ -127,8 +128,7 @@ def verify_and_decode_app_transaction(self, signed_app_transaction: str) -> AppT :return: The decoded AppTransaction after validation :throws VerificationException: Thrown if the data could not be verified """ - decoded_dict = self._decode_signed_object(signed_app_transaction) - decoded_app_transaction = _get_cattrs_converter(AppTransaction).structure(decoded_dict, AppTransaction) + decoded_app_transaction = self._decode_and_structure_signed_object(signed_app_transaction, AppTransaction) environment = decoded_app_transaction.receiptType if decoded_app_transaction.bundleId != self._bundle_id or (self._environment == Environment.PRODUCTION and decoded_app_transaction.appAppleId != self._app_apple_id): raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) @@ -141,18 +141,24 @@ def verify_and_decode_realtime_request(self, signed_payload: str) -> DecodedReal Verifies and decodes a Retention Messaging API signedPayload See https://developer.apple.com/documentation/retentionmessaging/signedpayload - :param signedPayload: The payload received by your server + :param signed_payload: The payload received by your server :return: The decoded payload after verification :throws VerificationException: Thrown if the data could not be verified """ - decoded_dict = self._decode_signed_object(signed_payload) - decoded_realtime_request = _get_cattrs_converter(DecodedRealtimeRequestBody).structure(decoded_dict, DecodedRealtimeRequestBody) + decoded_realtime_request = self._decode_and_structure_signed_object(signed_payload, DecodedRealtimeRequestBody) if self._environment == Environment.PRODUCTION and decoded_realtime_request.appAppleId != self._app_apple_id: raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) if decoded_realtime_request.environment != self._environment: raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT) return decoded_realtime_request + def _decode_and_structure_signed_object(self, signed_obj: str, cl: Type[T]) -> T: + decoded_dict = self._decode_signed_object(signed_obj) + try: + return _get_cattrs_converter(cl).structure(decoded_dict, cl) + except Exception as e: + raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e + def _decode_signed_object(self, signed_obj: str) -> dict: try: decoded_jwt = jwt.decode(signed_obj, options={"verify_signature": False}) @@ -161,10 +167,10 @@ def _decode_signed_object(self, signed_obj: str) -> dict: # The environment MUST be checked in the public method calling this return decoded_jwt unverified_headers: dict = jwt.get_unverified_header(signed_obj) - x5c_header: List[str] = unverified_headers.get("x5c") + x5c_header: Optional[List[str]] = unverified_headers.get("x5c") if x5c_header is None or len(x5c_header) == 0: raise Exception("x5c claim was empty") - algorithm_header: str = unverified_headers.get("alg") + algorithm_header: Optional[str] = unverified_headers.get("alg") if algorithm_header is None or "ES256" != algorithm_header: raise Exception("Algorithm was not ES256") signed_date = decoded_jwt.get('signedDate') if decoded_jwt.get('signedDate') is not None else decoded_jwt.get('receiptCreationDate') @@ -184,9 +190,9 @@ class _ChainVerifier: def __init__(self, root_certificates: List[bytes], enable_strict_checks=True): self.enable_strict_checks = enable_strict_checks self.root_certificates = root_certificates - self.verified_certificates_cache: Dict[tuple[str, ...], (str, int)] = {} + self.verified_certificates_cache: Dict[Tuple[str, ...], Tuple[str, float]] = {} - def verify_chain(self, certificates: List[str], perform_online_checks: bool, effective_date: int) -> str: + def verify_chain(self, certificates: List[str], perform_online_checks: bool, effective_date: float) -> str: if perform_online_checks and len(certificates) > 0: cached_public_key = self.get_cached_public_key(certificates) if cached_public_key is not None: @@ -196,7 +202,7 @@ def verify_chain(self, certificates: List[str], perform_online_checks: bool, eff self.put_verified_public_key(certificates, verified_public_key) return verified_public_key - def _verify_chain_without_caching(self, certificates: List[str], perform_online_checks: bool, effective_date: int) -> str: + def _verify_chain_without_caching(self, certificates: List[str], perform_online_checks: bool, effective_date: float) -> str: if len(self.root_certificates) == 0: raise VerificationException(VerificationStatus.INVALID_CERTIFICATE) if len(certificates) != 3: @@ -290,7 +296,7 @@ def check_ocsp_status(self, cert: crypto.X509, issuer: crypto.X509, root: crypto break elif ocsp_resp.responder_name: - if ocsp_resp.responder_name == potential_signing_cert.subject.rfc4514_string(): + if ocsp_resp.responder_name == potential_signing_cert.to_cryptography().subject: signing_cert = potential_signing_cert break if signing_cert is None: @@ -311,7 +317,7 @@ def check_ocsp_status(self, cert: crypto.X509, issuer: crypto.X509, root: crypto oid.ExtendedKeyUsageOID.OCSP_SIGNING not in signing_cert.to_cryptography() .extensions.get_extension_for_class(x509.ExtendedKeyUsage) - .value._usages + .value ): raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) diff --git a/pyproject.toml b/pyproject.toml index 9b8300d2..7d96ece8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ version = "3.1.2" description = "The App Store Server Library" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "MIT"} -requires-python = ">=3.7, <4" +requires-python = ">=3.8, <4" classifiers = [ "License :: OSI Approved :: MIT License" ] diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..8f5faa26 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Copyright (c) 2023 Apple Inc. Licensed under MIT License. diff --git a/tests/resources/models/advancedCommerceRequestRefundResponseWithSignedRenewalInfo.json b/tests/resources/models/advancedCommerceRequestRefundResponseWithSignedRenewalInfo.json new file mode 100644 index 00000000..0350077f --- /dev/null +++ b/tests/resources/models/advancedCommerceRequestRefundResponseWithSignedRenewalInfo.json @@ -0,0 +1,4 @@ +{ + "signedTransactionInfo": "signed_transaction_info_value", + "signedRenewalInfo": "signed_renewal_info_value" +} diff --git a/tests/resources/models/appTransaction.json b/tests/resources/models/appTransaction.json index 9313c31c..c347e60b 100644 --- a/tests/resources/models/appTransaction.json +++ b/tests/resources/models/appTransaction.json @@ -11,5 +11,6 @@ "deviceVerificationNonce": "48ccfa42-7431-4f22-9908-7e88983e105a", "preorderDate": 1698148700000, "appTransactionId": "71134", - "originalPlatform": "iOS" + "originalPlatform": "iOS", + "storeType": "CONSUMER" } \ No newline at end of file diff --git a/tests/resources/models/consumptionPercentageOutOfRangeError.json b/tests/resources/models/consumptionPercentageOutOfRangeError.json new file mode 100644 index 00000000..03a28ccc --- /dev/null +++ b/tests/resources/models/consumptionPercentageOutOfRangeError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4000207, + "errorMessage": "Invalid request. The consumption percentage is outside the valid range." +} \ No newline at end of file diff --git a/tests/resources/models/getCustomerGroupsResponse.json b/tests/resources/models/getCustomerGroupsResponse.json new file mode 100644 index 00000000..54be137e --- /dev/null +++ b/tests/resources/models/getCustomerGroupsResponse.json @@ -0,0 +1,22 @@ +{ + "groups": [ + { + "groupId": "900000000000000000", + "groupType": "ORGANIZATION", + "roles": [ + { + "productId": "example.productId.one", + "role": "ADMIN" + }, + { + "productId": "example.productId.two", + "role": "NONE" + } + ] + }, + { + "groupId": "900000000000000001", + "groupType": "CONSUMER" + } + ] +} diff --git a/tests/resources/models/getCustomerGroupsResponseWithUnknownValues.json b/tests/resources/models/getCustomerGroupsResponseWithUnknownValues.json new file mode 100644 index 00000000..5d82d759 --- /dev/null +++ b/tests/resources/models/getCustomerGroupsResponseWithUnknownValues.json @@ -0,0 +1,14 @@ +{ + "groups": [ + { + "groupId": "900000000000000000", + "groupType": "UNKNOWN_GROUP_TYPE", + "roles": [ + { + "productId": "example.productId.one", + "role": "UNKNOWN_ROLE" + } + ] + } + ] +} diff --git a/tests/resources/models/getGroupMembersResponse.json b/tests/resources/models/getGroupMembersResponse.json new file mode 100644 index 00000000..42e5c3bc --- /dev/null +++ b/tests/resources/models/getGroupMembersResponse.json @@ -0,0 +1,12 @@ +{ + "members": [ + { + "appTransactionId": "700000000000000000" + }, + { + "appTransactionId": "700000000000000001" + } + ], + "paginationToken": "pagination_token_output", + "hasMore": true +} diff --git a/tests/resources/models/groupNotFoundError.json b/tests/resources/models/groupNotFoundError.json new file mode 100644 index 00000000..f573291a --- /dev/null +++ b/tests/resources/models/groupNotFoundError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4040022, + "errorMessage": "Group not found." +} diff --git a/tests/resources/models/invalidGroupIdError.json b/tests/resources/models/invalidGroupIdError.json new file mode 100644 index 00000000..ffba9c88 --- /dev/null +++ b/tests/resources/models/invalidGroupIdError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4000225, + "errorMessage": "Invalid request. The group ID is invalid." +} diff --git a/tests/resources/models/invalidLimitError.json b/tests/resources/models/invalidLimitError.json new file mode 100644 index 00000000..a340b417 --- /dev/null +++ b/tests/resources/models/invalidLimitError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4000004, + "errorMessage": "Invalid request limit." +} diff --git a/tests/resources/models/signedRenewalInfoAdvancedCommerceOffer.json b/tests/resources/models/signedRenewalInfoAdvancedCommerceOffer.json new file mode 100644 index 00000000..63ac89ab --- /dev/null +++ b/tests/resources/models/signedRenewalInfoAdvancedCommerceOffer.json @@ -0,0 +1,33 @@ +{ + "originalTransactionId": "12345", + "productId": "com.example.product", + "signedDate": 1698148800000, + "environment": "LocalTesting", + "advancedCommerceInfo": { + "descriptors": { + "description": "Premium subscription, billed monthly", + "displayName": "Premium" + }, + "items": [ + { + "SKU": "com.example.sku.premium", + "offer": { + "period": "P1M", + "periodCount": 3, + "price": 4990, + "reason": "WIN_BACK" + } + }, + { + "SKU": "com.example.sku.addon", + "offer": { + "period": "P5Y", + "periodCount": 6, + "price": 2990, + "reason": "NEW_REASON" + } + } + ], + "period": "P1M" + } +} diff --git a/tests/resources/models/signedTransaction.json b/tests/resources/models/signedTransaction.json index 3d405b64..02dc9cea 100644 --- a/tests/resources/models/signedTransaction.json +++ b/tests/resources/models/signedTransaction.json @@ -62,5 +62,6 @@ "commitmentExpiresDate": 1698150000000, "commitmentPrice": 119880, "totalBillingPeriods": 12 - } + }, + "previousOriginalTransactionId": "11111" } \ No newline at end of file diff --git a/tests/resources/models/signedTransactionAdvancedCommerceOffer.json b/tests/resources/models/signedTransactionAdvancedCommerceOffer.json new file mode 100644 index 00000000..ce87c11b --- /dev/null +++ b/tests/resources/models/signedTransactionAdvancedCommerceOffer.json @@ -0,0 +1,35 @@ +{ + "transactionId": "23456", + "originalTransactionId": "12345", + "bundleId": "com.example", + "productId": "com.example.product", + "signedDate": 1698148900000, + "environment": "LocalTesting", + "advancedCommerceInfo": { + "descriptors": { + "description": "Premium Plan", + "displayName": "Premium Plan Monthly" + }, + "items": [ + { + "SKU": "com.example.sku.premium", + "offer": { + "period": "P1W", + "periodCount": 1, + "price": 0, + "reason": "ACQUISITION" + } + }, + { + "SKU": "com.example.sku.addon", + "offer": { + "period": "P2W", + "periodCount": 12, + "price": 1990, + "reason": "RETENTION" + } + } + ], + "period": "P1M" + } +} diff --git a/tests/resources/models/subscriptionDoesNotExistError.json b/tests/resources/models/subscriptionDoesNotExistError.json new file mode 100644 index 00000000..7de813ad --- /dev/null +++ b/tests/resources/models/subscriptionDoesNotExistError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4030008, + "errorMessage": "The subscription doesn't exist." +} \ No newline at end of file diff --git a/tests/test_advanced_commerce_models.py b/tests/test_advanced_commerce_models.py index 93e2315d..51da1709 100644 --- a/tests/test_advanced_commerce_models.py +++ b/tests/test_advanced_commerce_models.py @@ -1,7 +1,13 @@ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. +import inspect import json +from typing import Optional import unittest +from uuid import UUID + +import attr +from cattrs.errors import ClassValidationError from appstoreserverlibrary.models.AdvancedCommerceDescriptors import AdvancedCommerceDescriptors from appstoreserverlibrary.models.AdvancedCommerceEffective import AdvancedCommerceEffective @@ -58,7 +64,8 @@ from appstoreserverlibrary.models.RenewalCommitmentInfo import RenewalCommitmentInfo from appstoreserverlibrary.models.TransactionCommitmentInfo import TransactionCommitmentInfo from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter -from tests.util import read_data_from_file +from appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus +from tests.util import create_signed_data_from_dict, create_signed_data_from_json, get_default_signed_data_verifier, read_data_from_file class AdvancedCommerceModelsTest(unittest.TestCase): @@ -164,6 +171,9 @@ def test_validation_utils_description(self): with self.assertRaises(ValueError): HelperValidationUtils.description_validator(None, None, too_long_description) + with self.assertRaises(ValueError): + HelperValidationUtils.description_validator(None, None, None) + def test_validation_utils_display_name(self): valid_display_name = "Valid Name" HelperValidationUtils.display_name_validator(None, None, valid_display_name) @@ -175,6 +185,9 @@ def test_validation_utils_display_name(self): with self.assertRaises(ValueError): HelperValidationUtils.display_name_validator(None, None, too_long_display_name) + with self.assertRaises(ValueError): + HelperValidationUtils.display_name_validator(None, None, None) + def test_validation_utils_sku(self): valid_sku = "valid.sku.123" HelperValidationUtils.sku_validator(None, None, valid_sku) @@ -186,6 +199,9 @@ def test_validation_utils_sku(self): with self.assertRaises(ValueError): HelperValidationUtils.sku_validator(None, None, too_long_sku) + with self.assertRaises(ValueError): + HelperValidationUtils.sku_validator(None, None, None) + def test_validation_utils_period_count(self): HelperValidationUtils.period_count_validator(None, None, 1) HelperValidationUtils.period_count_validator(None, None, 6) @@ -197,6 +213,22 @@ def test_validation_utils_period_count(self): with self.assertRaises(ValueError): HelperValidationUtils.period_count_validator(None, None, 13) + with self.assertRaises(ValueError): + HelperValidationUtils.period_count_validator(None, None, None) + + def test_required_fields_reject_none(self): + with self.assertRaises(ValueError): + AdvancedCommerceDescriptors(description=None, displayName="name") + + with self.assertRaises(ValueError): + AdvancedCommerceDescriptors(description="desc", displayName=None) + + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionCreateItem(SKU=None, description="desc", displayName="name", price=1000) + + with self.assertRaises(ValueError): + AdvancedCommerceOffer(periodCount=None, price=1000, period=AdvancedCommerceOfferPeriod.P1M, reason=AdvancedCommerceOfferReason.ACQUISITION) + def test_validation_utils_items(self): valid_list = [ AdvancedCommerceOneTimeChargeItem( @@ -218,6 +250,41 @@ def test_validation_utils_items(self): with self.assertRaises(ValueError): HelperValidationUtils.items_validator(None, None, list_with_none) + def test_request_item_lists_are_validated(self): + request_info = AdvancedCommerceRequestInfo(requestReferenceId=UUID("550e8400-e29b-41d4-a716-446655440000")) + descriptors = AdvancedCommerceDescriptors(description="description", displayName="display name") + create_item = AdvancedCommerceSubscriptionCreateItem(SKU="sku", description="description", displayName="display name", price=1000) + for invalid_items in [[], [None], None]: + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionCreateRequest(requestInfo=request_info, currency="USD", descriptors=descriptors, items=invalid_items, taxCode="taxCode", period=AdvancedCommercePeriod.P1M) + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionPriceChangeRequest(requestInfo=request_info, items=invalid_items) + for invalid_items in [[], [None]]: + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionModifyInAppRequest(requestInfo=request_info, transactionId="transactionId", retainBillingCycle=True, removeItems=invalid_items) + + AdvancedCommerceSubscriptionCreateRequest(requestInfo=request_info, currency="USD", descriptors=descriptors, items=[create_item], taxCode="taxCode", period=AdvancedCommercePeriod.P1M) + AdvancedCommerceSubscriptionPriceChangeRequest(requestInfo=request_info, items=[AdvancedCommerceSubscriptionPriceChangeItem(SKU="sku", price=1000)]) + modify_request = AdvancedCommerceSubscriptionModifyInAppRequest(requestInfo=request_info, transactionId="transactionId", retainBillingCycle=True) + self.assertIsNone(modify_request.removeItems) + modify_request = AdvancedCommerceSubscriptionModifyInAppRequest(requestInfo=request_info, transactionId="transactionId", retainBillingCycle=True, removeItems=[AdvancedCommerceSubscriptionModifyRemoveItem(SKU="sku")]) + self.assertEqual(1, len(modify_request.removeItems)) + + def test_reactivate_request_items_are_validated(self): + request_info = AdvancedCommerceRequestInfo(requestReferenceId=UUID("550e8400-e29b-41d4-a716-446655440000")) + item = AdvancedCommerceSubscriptionReactivateItem(SKU="sku") + for invalid_items in [[], [None], [item, None]]: + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionReactivateInAppRequest(requestInfo=request_info, transactionId="transactionId", items=invalid_items) + + reactivate_request = AdvancedCommerceSubscriptionReactivateInAppRequest(requestInfo=request_info, transactionId="transactionId") + self.assertIsNone(reactivate_request.items) + with self.assertRaises(ValueError): + reactivate_request.items = [] + + reactivate_request = AdvancedCommerceSubscriptionReactivateInAppRequest(requestInfo=request_info, transactionId="transactionId", items=[item]) + self.assertEqual([item], reactivate_request.items) + def test_advanced_commerce_descriptors_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceDescriptors.json') @@ -378,6 +445,30 @@ def test_advanced_commerce_request_refund_response_deserialization(self): response = _get_cattrs_converter(AdvancedCommerceRequestRefundResponse).structure(response_dict, AdvancedCommerceRequestRefundResponse) self.assertEqual("signed_transaction_info_value", response.signedTransactionInfo) + self.assertIsNone(response.signedRenewalInfo) + + def test_advanced_commerce_request_refund_response_with_signed_renewal_info_deserialization(self): + json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestRefundResponseWithSignedRenewalInfo.json') + + response_dict = json.loads(json_data) + response = _get_cattrs_converter(AdvancedCommerceRequestRefundResponse).structure(response_dict, AdvancedCommerceRequestRefundResponse) + + self.assertEqual("signed_transaction_info_value", response.signedTransactionInfo) + self.assertEqual("signed_renewal_info_value", response.signedRenewalInfo) + self.assertEqual(AdvancedCommerceRequestRefundResponse(signedTransactionInfo="signed_transaction_info_value", signedRenewalInfo="signed_renewal_info_value"), response) + + def test_advanced_commerce_responses_reject_unknown_attributes(self): + responses = [ + AdvancedCommerceRequestRefundResponse(signedTransactionInfo="signed_transaction_info"), + AdvancedCommerceSubscriptionCancelResponse(signedRenewalInfo="signed_renewal_info", signedTransactionInfo="signed_transaction_info"), + AdvancedCommerceSubscriptionChangeMetadataResponse(signedRenewalInfo="signed_renewal_info", signedTransactionInfo="signed_transaction_info"), + AdvancedCommerceSubscriptionMigrateResponse(signedRenewalInfo="signed_renewal_info", signedTransactionInfo="signed_transaction_info"), + AdvancedCommerceSubscriptionPriceChangeResponse(signedRenewalInfo="signed_renewal_info", signedTransactionInfo="signed_transaction_info"), + AdvancedCommerceSubscriptionRevokeResponse(signedRenewalInfo="signed_renewal_info", signedTransactionInfo="signed_transaction_info"), + ] + for response in responses: + with self.assertRaises(AttributeError): + response.signedTransactionInf = "typo" def test_advanced_commerce_subscription_cancel_response_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCancelResponse.json') @@ -388,6 +479,19 @@ def test_advanced_commerce_subscription_cancel_response_deserialization(self): self.assertEqual("signed_renewal_info", response.signedRenewalInfo) self.assertEqual("signed_transaction_info", response.signedTransactionInfo) + def test_advanced_commerce_subscription_cancel_response_requires_documented_fields(self): + with self.assertRaises(TypeError): + AdvancedCommerceSubscriptionCancelResponse(signedTransactionInfo="signed_transaction_info") + with self.assertRaises(TypeError): + AdvancedCommerceSubscriptionCancelResponse(signedRenewalInfo="signed_renewal_info") + + c = _get_cattrs_converter(AdvancedCommerceSubscriptionCancelResponse) + for field in ['signedRenewalInfo', 'signedTransactionInfo']: + response_dict = json.loads(read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCancelResponse.json')) + del response_dict[field] + with self.subTest(field=field), self.assertRaises(ClassValidationError): + c.structure(response_dict, AdvancedCommerceSubscriptionCancelResponse) + def test_advanced_commerce_subscription_revoke_response_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionRevokeResponse.json') @@ -487,6 +591,8 @@ def test_advanced_commerce_subscription_modify_descriptors_deserialization(self) descriptors_dict = json.loads(json_data) descriptors = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyDescriptors).structure(descriptors_dict, AdvancedCommerceSubscriptionModifyDescriptors) + self.assertEqual(AdvancedCommerceEffective.IMMEDIATELY, descriptors.effective) + self.assertEqual("IMMEDIATELY", descriptors.rawEffective) self.assertEqual("description", descriptors.description) self.assertEqual("displayName", descriptors.displayName) @@ -496,6 +602,8 @@ def test_advanced_commerce_subscription_change_metadata_descriptors_deserializat descriptors_dict = json.loads(json_data) descriptors = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataDescriptors).structure(descriptors_dict, AdvancedCommerceSubscriptionChangeMetadataDescriptors) + self.assertEqual(AdvancedCommerceEffective.IMMEDIATELY, descriptors.effective) + self.assertEqual("IMMEDIATELY", descriptors.rawEffective) self.assertEqual("description", descriptors.description) self.assertEqual("displayName", descriptors.displayName) @@ -506,10 +614,29 @@ def test_advanced_commerce_subscription_change_metadata_item_deserialization(sel item = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataItem).structure(item_dict, AdvancedCommerceSubscriptionChangeMetadataItem) self.assertEqual("currentSku", item.currentSKU) + self.assertEqual(AdvancedCommerceEffective.NEXT_BILL_CYCLE, item.effective) + self.assertEqual("NEXT_BILL_CYCLE", item.rawEffective) self.assertEqual("sku", item.SKU) self.assertEqual("description", item.description) self.assertEqual("displayName", item.displayName) + def test_effective_is_set_from_raw_effective(self): + change_metadata_descriptors = AdvancedCommerceSubscriptionChangeMetadataDescriptors(rawEffective="IMMEDIATELY") + self.assertEqual(AdvancedCommerceEffective.IMMEDIATELY, change_metadata_descriptors.effective) + self.assertEqual(AdvancedCommerceSubscriptionChangeMetadataDescriptors(effective=AdvancedCommerceEffective.IMMEDIATELY), change_metadata_descriptors) + + change_metadata_item = AdvancedCommerceSubscriptionChangeMetadataItem(currentSKU="sku", rawEffective="NEXT_BILL_CYCLE") + self.assertEqual(AdvancedCommerceEffective.NEXT_BILL_CYCLE, change_metadata_item.effective) + self.assertEqual(AdvancedCommerceSubscriptionChangeMetadataItem(currentSKU="sku", effective=AdvancedCommerceEffective.NEXT_BILL_CYCLE), change_metadata_item) + + modify_descriptors = AdvancedCommerceSubscriptionModifyDescriptors(rawEffective="IMMEDIATELY") + self.assertEqual(AdvancedCommerceEffective.IMMEDIATELY, modify_descriptors.effective) + self.assertEqual(AdvancedCommerceSubscriptionModifyDescriptors(effective=AdvancedCommerceEffective.IMMEDIATELY), modify_descriptors) + + unknown_effective = AdvancedCommerceSubscriptionModifyDescriptors(rawEffective="UNKNOWN") + self.assertIsNone(unknown_effective.effective) + self.assertEqual("UNKNOWN", unknown_effective.rawEffective) + def test_advanced_commerce_subscription_migrate_renewal_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateRenewalItem.json') @@ -581,6 +708,9 @@ def test_advanced_commerce_subscription_price_change_item_dependent_sku_validati with self.assertRaises(ValueError): AdvancedCommerceSubscriptionPriceChangeItem(SKU="sku", price=1000, dependentSKUs=[too_long_sku]) + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionPriceChangeItem(SKU="sku", price=1000, dependentSKUs=[None]) + # None list is allowed (field is optional) item_none = AdvancedCommerceSubscriptionPriceChangeItem(SKU="sku", price=1000, dependentSKUs=None) self.assertIsNone(item_none.dependentSKUs) @@ -662,6 +792,131 @@ def test_subscription_reactivate_in_app_request_deserialization_sets_operation_a self.assertEqual("REACTIVATE_SUBSCRIPTION", result["operation"]) self.assertEqual("1", result["version"]) + def test_renewal_info_advanced_commerce_offer_decoding(self): + signed_renewal_info = create_signed_data_from_json('tests/resources/models/signedRenewalInfoAdvancedCommerceOffer.json') + + renewal_info = get_default_signed_data_verifier().verify_and_decode_renewal_info(signed_renewal_info) + + ac_info = renewal_info.advancedCommerceInfo + self.assertIsInstance(ac_info, AdvancedCommerceRenewalInfo) + self.assertIsInstance(ac_info.descriptors, AdvancedCommerceDescriptors) + self.assertEqual("Premium subscription, billed monthly", ac_info.descriptors.description) + self.assertEqual("Premium", ac_info.descriptors.displayName) + self.assertEqual(2, len(ac_info.items)) + self.assertIsInstance(ac_info.items[0], AdvancedCommerceRenewalItem) + offer = ac_info.items[0].offer + self.assertEqual(AdvancedCommerceOffer(periodCount=3, price=4990, period=AdvancedCommerceOfferPeriod.P1M, reason=AdvancedCommerceOfferReason.WIN_BACK), offer) + self.assertEqual("P1M", offer.rawPeriod) + self.assertEqual("WIN_BACK", offer.rawReason) + offer = ac_info.items[1].offer + self.assertIsInstance(offer, AdvancedCommerceOffer) + self.assertIsNone(offer.period) + self.assertEqual("P5Y", offer.rawPeriod) + self.assertEqual(6, offer.periodCount) + self.assertEqual(2990, offer.price) + self.assertIsNone(offer.reason) + self.assertEqual("NEW_REASON", offer.rawReason) + + def test_transaction_advanced_commerce_offer_decoding(self): + signed_transaction = create_signed_data_from_json('tests/resources/models/signedTransactionAdvancedCommerceOffer.json') + + transaction = get_default_signed_data_verifier().verify_and_decode_transaction(signed_transaction) + + ac_info = transaction.advancedCommerceInfo + self.assertIsInstance(ac_info, AdvancedCommerceTransactionInfo) + self.assertIsInstance(ac_info.descriptors, AdvancedCommerceDescriptors) + self.assertEqual("Premium Plan", ac_info.descriptors.description) + self.assertEqual("Premium Plan Monthly", ac_info.descriptors.displayName) + self.assertEqual(2, len(ac_info.items)) + self.assertIsInstance(ac_info.items[0], AdvancedCommerceTransactionItem) + offer = ac_info.items[0].offer + self.assertEqual(AdvancedCommerceOffer(periodCount=1, price=0, period=AdvancedCommerceOfferPeriod.P1W, reason=AdvancedCommerceOfferReason.ACQUISITION), offer) + offer = ac_info.items[1].offer + self.assertIsInstance(offer, AdvancedCommerceOffer) + self.assertEqual(12, offer.periodCount) + self.assertEqual(1990, offer.price) + self.assertEqual(AdvancedCommerceOfferPeriod.P2W, offer.period) + self.assertEqual("P2W", offer.rawPeriod) + self.assertEqual(AdvancedCommerceOfferReason.RETENTION, offer.reason) + self.assertEqual("RETENTION", offer.rawReason) + + def test_offer_or_descriptors_without_a_field_fail_to_decode_in_signed_data(self): + verifier = get_default_signed_data_verifier() + for path, decode in [('tests/resources/models/signedTransactionAdvancedCommerceOffer.json', verifier.verify_and_decode_transaction), + ('tests/resources/models/signedRenewalInfoAdvancedCommerceOffer.json', verifier.verify_and_decode_renewal_info)]: + for obj, key in [('descriptors', 'displayName'), ('descriptors', 'description'), + ('offer', 'price'), ('offer', 'period'), ('offer', 'periodCount'), ('offer', 'reason')]: + for missing in (True, False): + with self.subTest(path=path, field=f'{obj}.{key}', missing=missing): + payload = json.loads(read_data_from_file(path)) + info = payload['advancedCommerceInfo'] + target = info['descriptors'] if obj == 'descriptors' else info['items'][0]['offer'] + if missing: + del target[key] + else: + target[key] = None + with self.assertRaises(VerificationException) as context: + decode(create_signed_data_from_dict(payload)) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_offer_or_descriptors_outside_the_request_limits_fail_to_decode_in_signed_data(self): + verifier = get_default_signed_data_verifier() + for path, decode in [('tests/resources/models/signedTransactionAdvancedCommerceOffer.json', verifier.verify_and_decode_transaction), + ('tests/resources/models/signedRenewalInfoAdvancedCommerceOffer.json', verifier.verify_and_decode_renewal_info)]: + for obj, key, value in [('offer', 'periodCount', 13), ('offer', 'periodCount', 0), + ('descriptors', 'displayName', 'A' * 31), ('descriptors', 'description', 'A' * 46)]: + with self.subTest(path=path, field=f'{obj}.{key}', value=value): + payload = json.loads(read_data_from_file(path)) + info = payload['advancedCommerceInfo'] + target = info['descriptors'] if obj == 'descriptors' else info['items'][0]['offer'] + target[key] = value + with self.assertRaises(VerificationException) as context: + decode(create_signed_data_from_dict(payload)) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_offer_and_descriptors_at_the_request_limits_decode_in_signed_data(self): + verifier = get_default_signed_data_verifier() + for path, decode in [('tests/resources/models/signedTransactionAdvancedCommerceOffer.json', verifier.verify_and_decode_transaction), + ('tests/resources/models/signedRenewalInfoAdvancedCommerceOffer.json', verifier.verify_and_decode_renewal_info)]: + for period_count in (1, 12): + with self.subTest(path=path, periodCount=period_count): + payload = json.loads(read_data_from_file(path)) + info = payload['advancedCommerceInfo'] + info['descriptors']['displayName'] = 'A' * 30 + info['descriptors']['description'] = 'B' * 45 + info['items'][0]['offer']['periodCount'] = period_count + + ac_info = decode(create_signed_data_from_dict(payload)).advancedCommerceInfo + + self.assertEqual('A' * 30, ac_info.descriptors.displayName) + self.assertEqual('B' * 45, ac_info.descriptors.description) + self.assertEqual(period_count, ac_info.items[0].offer.periodCount) + + def test_one_time_charge_transaction_without_descriptors_or_offer_decodes(self): + payload = json.loads(read_data_from_file('tests/resources/models/signedTransactionAdvancedCommerceOffer.json')) + del payload['advancedCommerceInfo']['descriptors'] + del payload['advancedCommerceInfo']['period'] + del payload['advancedCommerceInfo']['items'][0]['offer'] + + transaction = get_default_signed_data_verifier().verify_and_decode_transaction(create_signed_data_from_dict(payload)) + + ac_info = transaction.advancedCommerceInfo + self.assertIsNone(ac_info.descriptors) + self.assertIsNone(ac_info.period) + self.assertIsNone(ac_info.items[0].offer) + self.assertEqual("com.example.sku.premium", ac_info.items[0].SKU) + self.assertEqual(12, ac_info.items[1].offer.periodCount) + + def test_request_offer_and_descriptors_deserialization_is_still_validated(self): + with self.assertRaises(ClassValidationError): + _get_cattrs_converter(AdvancedCommerceOffer).structure({"period": "P1M", "periodCount": 13, "price": 4990, "reason": "WIN_BACK"}, AdvancedCommerceOffer) + + with self.assertRaises(ClassValidationError): + _get_cattrs_converter(AdvancedCommerceDescriptors).structure({"description": "A" * 46, "displayName": "display name"}, AdvancedCommerceDescriptors) + + with self.assertRaises(ClassValidationError): + _get_cattrs_converter(AdvancedCommerceSubscriptionCreateItem).structure({"SKU": "sku", "description": "description", "displayName": "display name", "price": 1000, "offer": {"period": "P1M", "periodCount": 0, "price": 0, "reason": "WIN_BACK"}}, AdvancedCommerceSubscriptionCreateItem) + def test_advanced_commerce_price_increase_info_status(self): self.assertEqual("SCHEDULED", AdvancedCommercePriceIncreaseInfoStatus.SCHEDULED.value) self.assertEqual("PENDING", AdvancedCommercePriceIncreaseInfoStatus.PENDING.value) @@ -710,3 +965,186 @@ def test_transaction_commitment_info_billing_period_number_validation(self): info = TransactionCommitmentInfo(billingPeriodNumber=None) self.assertIsNone(info.billingPeriodNumber) + + for value in (0, 13, -1): + with self.subTest(value=value), self.assertRaises(ValueError): + TransactionCommitmentInfo(billingPeriodNumber=value) + + def test_concrete_types_keep_the_inherited_attribute_order(self): + expected = { + AdvancedCommerceOneTimeChargeItem: ['SKU', 'description', 'displayName', 'price'], + AdvancedCommerceSubscriptionCreateItem: ['SKU', 'description', 'displayName', 'price', 'offer'], + AdvancedCommerceSubscriptionMigrateItem: ['SKU', 'description', 'displayName'], + AdvancedCommerceSubscriptionMigrateRenewalItem: ['SKU', 'description', 'displayName'], + AdvancedCommerceSubscriptionModifyAddItem: ['SKU', 'description', 'displayName', 'price', 'offer', 'proratedPrice'], + AdvancedCommerceSubscriptionModifyChangeItem: ['SKU', 'description', 'displayName', 'currentSKU', 'effective', 'price', 'reason', 'offer', 'proratedPrice'], + AdvancedCommerceRequestRefundItem: ['SKU', 'refundReason', 'refundType', 'revoke', 'refundAmount'], + AdvancedCommerceSubscriptionModifyRemoveItem: ['SKU'], + AdvancedCommerceSubscriptionPriceChangeItem: ['SKU', 'price', 'dependentSKUs'], + AdvancedCommerceSubscriptionReactivateItem: ['SKU'], + AdvancedCommerceSubscriptionMigrateDescriptors: ['description', 'displayName'], + AdvancedCommerceOneTimeChargeCreateRequest: ['requestInfo', 'currency', 'item', 'taxCode', 'storefront'], + AdvancedCommerceSubscriptionCreateRequest: ['requestInfo', 'currency', 'descriptors', 'items', 'period', 'taxCode', 'previousTransactionId', 'storefront'], + AdvancedCommerceSubscriptionModifyInAppRequest: ['requestInfo', 'retainBillingCycle', 'transactionId', 'addItems', 'changeItems', 'currency', 'descriptors', 'periodChange', 'removeItems', 'storefront', 'taxCode'], + AdvancedCommerceSubscriptionReactivateInAppRequest: ['requestInfo', 'transactionId', 'items', 'storefront'], + AdvancedCommerceRequestRefundRequest: ['requestInfo', 'items', 'refundRiskingPreference', 'currency', 'storefront'], + AdvancedCommerceSubscriptionCancelRequest: ['requestInfo', 'storefront'], + AdvancedCommerceSubscriptionChangeMetadataRequest: ['requestInfo', 'descriptors', 'items', 'storefront', 'taxCode'], + AdvancedCommerceSubscriptionMigrateRequest: ['requestInfo', 'descriptors', 'items', 'targetProductId', 'taxCode', 'renewalItems', 'storefront'], + AdvancedCommerceSubscriptionPriceChangeRequest: ['requestInfo', 'items', 'currency', 'storefront'], + AdvancedCommerceSubscriptionRevokeRequest: ['requestInfo', 'refundReason', 'refundRiskingPreference', 'refundType', 'storefront'], + } + validators = { + 'SKU': HelperValidationUtils.sku_validator, + 'description': HelperValidationUtils.description_validator, + 'displayName': HelperValidationUtils.display_name_validator, + } + for cls, order in expected.items(): + with self.subTest(cls=cls.__name__): + parameters = inspect.signature(cls).parameters + self.assertEqual(order, [name for name in parameters if not name.startswith('raw')]) + self.assertEqual([], [name for name, p in parameters.items() if p.kind != inspect.Parameter.KEYWORD_ONLY]) + cls_fields = attr.fields_dict(cls) + for name, validator in validators.items(): + if name in cls_fields: + self.assertIs(validator, cls_fields[name].validator) + self.assertIs(attr.NOTHING, cls_fields[name].default) + if 'requestInfo' in cls_fields: + self.assertIs(attr.NOTHING, cls_fields['requestInfo'].default) + + def test_inherited_attributes_are_still_validated(self): + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionModifyRemoveItem(SKU="a" * 129) + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionReactivateItem(SKU=None) + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionMigrateItem(SKU="sku", description="a" * 46, displayName="name") + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionMigrateDescriptors(description="description", displayName="a" * 31) + with self.assertRaises(TypeError): + AdvancedCommerceSubscriptionCancelRequest() + + def test_advanced_commerce_response_attribute_types(self): + for cls in [AdvancedCommerceSubscriptionCancelResponse, AdvancedCommerceSubscriptionChangeMetadataResponse, + AdvancedCommerceSubscriptionMigrateResponse, AdvancedCommerceSubscriptionPriceChangeResponse, + AdvancedCommerceSubscriptionRevokeResponse]: + with self.subTest(cls=cls.__name__): + self.assertIs(str, attr.fields(cls).signedRenewalInfo.type) + self.assertIs(str, attr.fields(cls).signedTransactionInfo.type) + self.assertEqual(['signedRenewalInfo', 'signedTransactionInfo'], list(inspect.signature(cls).parameters)) + self.assertIs(str, attr.fields(AdvancedCommerceRequestRefundResponse).signedTransactionInfo.type) + self.assertEqual(Optional[str], attr.fields(AdvancedCommerceRequestRefundResponse).signedRenewalInfo.type) + parameters = inspect.signature(AdvancedCommerceRequestRefundResponse).parameters + self.assertEqual(['signedTransactionInfo', 'signedRenewalInfo'], list(parameters)) + self.assertIsNone(parameters['signedRenewalInfo'].default) + self.assertEqual(AdvancedCommerceRequestRefundResponse(signedTransactionInfo="transaction", signedRenewalInfo="renewal"), + AdvancedCommerceRequestRefundResponse(signedRenewalInfo="renewal", signedTransactionInfo="transaction")) + + def test_advanced_commerce_responses_reject_null_required_fields(self): + cases = [ + (AdvancedCommerceRequestRefundResponse, 'advancedCommerceRequestRefundResponseWithSignedRenewalInfo.json', ['signedTransactionInfo']), + (AdvancedCommerceSubscriptionCancelResponse, 'advancedCommerceSubscriptionCancelResponse.json', ['signedRenewalInfo', 'signedTransactionInfo']), + (AdvancedCommerceSubscriptionChangeMetadataResponse, 'advancedCommerceSubscriptionChangeMetadataResponse.json', ['signedRenewalInfo', 'signedTransactionInfo']), + (AdvancedCommerceSubscriptionMigrateResponse, 'advancedCommerceSubscriptionMigrateResponse.json', ['signedRenewalInfo', 'signedTransactionInfo']), + (AdvancedCommerceSubscriptionPriceChangeResponse, 'advancedCommerceSubscriptionPriceChangeResponse.json', ['signedRenewalInfo', 'signedTransactionInfo']), + (AdvancedCommerceSubscriptionRevokeResponse, 'advancedCommerceSubscriptionRevokeResponse.json', ['signedRenewalInfo', 'signedTransactionInfo']), + ] + for cls, file, fields in cases: + c = _get_cattrs_converter(cls) + for field in fields: + response_dict = json.loads(read_data_from_file(f'tests/resources/models/{file}')) + response_dict[field] = None + with self.subTest(response=cls.__name__, field=field), self.assertRaises(ClassValidationError): + c.structure(response_dict, cls) + + response_dict = json.loads(read_data_from_file('tests/resources/models/advancedCommerceRequestRefundResponseWithSignedRenewalInfo.json')) + response_dict['signedRenewalInfo'] = None + response = _get_cattrs_converter(AdvancedCommerceRequestRefundResponse).structure(response_dict, AdvancedCommerceRequestRefundResponse) + self.assertIsNone(response.signedRenewalInfo) + + def test_advanced_commerce_responses_reject_none_when_built(self): + for cls in [AdvancedCommerceSubscriptionCancelResponse, AdvancedCommerceSubscriptionChangeMetadataResponse, + AdvancedCommerceSubscriptionMigrateResponse, AdvancedCommerceSubscriptionPriceChangeResponse, + AdvancedCommerceSubscriptionRevokeResponse]: + for field in ['signedRenewalInfo', 'signedTransactionInfo']: + values = dict(signedRenewalInfo="signed_renewal_info", signedTransactionInfo="signed_transaction_info") + with self.subTest(response=cls.__name__, field=field): + with self.assertRaises(ValueError) as context: + cls(**dict(values, **{field: None})) + self.assertEqual(f'{field} cannot be None', str(context.exception)) + response = cls(**values) + with self.assertRaises(ValueError): + setattr(response, field, None) + self.assertEqual(values[field], getattr(response, field)) + + with self.assertRaises(ValueError): + AdvancedCommerceRequestRefundResponse(signedTransactionInfo=None) + self.assertIsNone(AdvancedCommerceRequestRefundResponse(signedTransactionInfo="signed_transaction_info", signedRenewalInfo=None).signedRenewalInfo) + + def test_advanced_commerce_requests_reject_none_when_built(self): + request_info = AdvancedCommerceRequestInfo(requestReferenceId=UUID("550e8400-e29b-41d4-a716-446655440000")) + with self.assertRaises(ValueError) as context: + AdvancedCommerceRequestInfo(requestReferenceId=None) + self.assertEqual('requestReferenceId cannot be None', str(context.exception)) + with self.assertRaises(ValueError) as context: + AdvancedCommerceSubscriptionCancelRequest(requestInfo=None) + self.assertEqual('requestInfo cannot be None', str(context.exception)) + with self.assertRaises(ValueError) as context: + AdvancedCommerceOneTimeChargeItem(SKU="sku", description="description", displayName="display name", price=None) + self.assertEqual('price cannot be None', str(context.exception)) + for field in ['transactionId', 'retainBillingCycle']: + values = dict(requestInfo=request_info, transactionId="transactionId", retainBillingCycle=True) + with self.subTest(field=field): + with self.assertRaises(ValueError): + AdvancedCommerceSubscriptionModifyInAppRequest(**dict(values, **{field: None})) + request = AdvancedCommerceSubscriptionModifyInAppRequest(**values) + with self.assertRaises(ValueError): + setattr(request, field, None) + + def test_offer_and_descriptors_types_are_required(self): + offer_fields = attr.fields(AdvancedCommerceOffer) + self.assertEqual(int, offer_fields.periodCount.type) + self.assertEqual(int, offer_fields.price.type) + self.assertEqual(AdvancedCommerceOfferPeriod, offer_fields.period.type) + self.assertEqual(str, offer_fields.rawPeriod.type) + self.assertEqual(AdvancedCommerceOfferReason, offer_fields.reason.type) + self.assertEqual(str, offer_fields.rawReason.type) + self.assertEqual(str, attr.fields(AdvancedCommerceDescriptors).description.type) + self.assertEqual(str, attr.fields(AdvancedCommerceDescriptors).displayName.type) + for cls, names in [(AdvancedCommerceOffer, ['periodCount', 'price']), (AdvancedCommerceDescriptors, ['description', 'displayName'])]: + parameters = inspect.signature(cls).parameters + for name in names: + self.assertIs(inspect.Parameter.empty, parameters[name].default) + with self.assertRaises(ValueError) as context: + AdvancedCommerceOffer(periodCount=1, price=None, period=AdvancedCommerceOfferPeriod.P1M, reason=AdvancedCommerceOfferReason.ACQUISITION) + self.assertEqual('price cannot be None', str(context.exception)) + + def test_offer_and_descriptors_without_a_field_fail_to_decode(self): + for cls, value in [(AdvancedCommerceOffer, {"period": "P1M", "periodCount": 3, "price": 4990, "reason": "WIN_BACK"}), + (AdvancedCommerceDescriptors, {"description": "description", "displayName": "display name"})]: + converter = _get_cattrs_converter(cls) + self.assertIsInstance(converter.structure(value, cls), cls) + for key in value: + with self.subTest(cls=cls.__name__, key=key): + with self.assertRaises(ClassValidationError): + converter.structure({k: v for k, v in value.items() if k != key}, cls) + with self.assertRaises(ClassValidationError): + converter.structure(dict(value, **{key: None}), cls) + + def test_offer_and_descriptors_are_required_to_build(self): + with self.assertRaises(TypeError): + AdvancedCommerceDescriptors(description="description") + with self.assertRaises(TypeError): + AdvancedCommerceDescriptors(displayName="display name") + with self.assertRaises(TypeError): + AdvancedCommerceOffer(price=1000, period=AdvancedCommerceOfferPeriod.P1M, reason=AdvancedCommerceOfferReason.ACQUISITION) + with self.assertRaises(TypeError): + AdvancedCommerceOffer(periodCount=1, period=AdvancedCommerceOfferPeriod.P1M, reason=AdvancedCommerceOfferReason.ACQUISITION) + with self.assertRaises(ValueError): + AdvancedCommerceOffer(periodCount=1, price=1000, reason=AdvancedCommerceOfferReason.ACQUISITION) + with self.assertRaises(ValueError): + AdvancedCommerceOffer(periodCount=1, price=1000, period=AdvancedCommerceOfferPeriod.P1M) + with self.assertRaises(ValueError): + AdvancedCommerceOffer(periodCount=13, price=1000, period=AdvancedCommerceOfferPeriod.P1M, reason=AdvancedCommerceOfferReason.ACQUISITION) + with self.assertRaises(ValueError): + AdvancedCommerceDescriptors(description="description", displayName="A" * 31) diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 5f6a799a..9e467cde 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -2,7 +2,9 @@ from typing import Any, Dict, List, Union import unittest +import warnings +from cattrs.errors import ClassValidationError from requests import Response from appstoreserverlibrary.api_client import APIError, APIException, AppStoreServerAPIClient, GetTransactionHistoryVersion from appstoreserverlibrary.models.AccountTenure import AccountTenure @@ -32,6 +34,9 @@ from appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent from appstoreserverlibrary.models.ExtendReasonCode import ExtendReasonCode from appstoreserverlibrary.models.ExtendRenewalDateRequest import ExtendRenewalDateRequest +from appstoreserverlibrary.models.GroupEntry import GroupEntry +from appstoreserverlibrary.models.GroupMemberEntry import GroupMemberEntry +from appstoreserverlibrary.models.GroupType import GroupType from appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType from appstoreserverlibrary.models.LastTransactionsItem import LastTransactionsItem from appstoreserverlibrary.models.LifetimeDollarsPurchased import LifetimeDollarsPurchased @@ -46,6 +51,8 @@ from appstoreserverlibrary.models.PlayTime import PlayTime from appstoreserverlibrary.models.RefundPreference import RefundPreference from appstoreserverlibrary.models.RefundPreferenceV1 import RefundPreferenceV1 +from appstoreserverlibrary.models.Role import Role +from appstoreserverlibrary.models.RoleEntry import RoleEntry from appstoreserverlibrary.models.SendAttemptItem import SendAttemptItem from appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult from appstoreserverlibrary.models.Status import Status @@ -71,7 +78,7 @@ from io import BytesIO -class DecodedPayloads(unittest.TestCase): +class APIClientTest(unittest.TestCase): def test_extend_renewal_date_for_all_active_subscribers(self): client = self.get_client_with_body_from_file('tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json', 'POST', @@ -112,6 +119,35 @@ def test_extend_subscription_renewal_date(self): self.assertTrue(extend_renewal_date_response.success) self.assertEqual(1698148900000, extend_renewal_date_response.effectiveDate) + def test_extend_renewal_dates_send_unknown_raw_extend_reason_code(self): + client = self.get_client_with_body_from_file('tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json', + 'POST', + 'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass', + {}, + {'extendByDays': 45, 'extendReasonCode': 99, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac', 'productId': 'com.example.productId'}) + + mass_extend_renewal_date_request = MassExtendRenewalDateRequest( + extendByDays=45, + rawExtendReasonCode=99, + requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', + productId='com.example.productId') + + self.assertIsNotNone(client.extend_renewal_date_for_all_active_subscribers(mass_extend_renewal_date_request)) + + client = self.get_client_with_body_from_file('tests/resources/models/extendSubscriptionRenewalDateResponse.json', + 'PUT', + 'https://local-testing-base-url/inApps/v1/subscriptions/extend/4124214', + {}, + {'extendByDays': 45, 'extendReasonCode': 99, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac'}) + + extend_renewal_date_request = ExtendRenewalDateRequest( + extendByDays=45, + rawExtendReasonCode=99, + requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac' + ) + + self.assertIsNotNone(client.extend_subscription_renewal_date('4124214', extend_renewal_date_request)) + def test_get_all_subscription_statuses(self): client = self.get_client_with_body_from_file('tests/resources/models/getAllSubscriptionStatusesResponse.json', 'GET', @@ -248,6 +284,22 @@ def test_get_notification_history(self): ] self.assertEqual(expected_notification_history, notification_history_response.notificationHistory) + def test_get_notification_history_sends_unknown_raw_notification_type_and_subtype(self): + client = self.get_client_with_body_from_file('tests/resources/models/getNotificationHistoryResponse.json', + 'POST', + 'https://local-testing-base-url/inApps/v1/notifications/history', + {}, + {'startDate': 1698148900000, 'endDate': 1698148950000, 'notificationType': 'NEW_NOTIFICATION_TYPE', 'notificationSubtype': 'NEW_SUBTYPE'}) + + notification_history_request = NotificationHistoryRequest( + startDate=1698148900000, + endDate=1698148950000, + rawNotificationType='NEW_NOTIFICATION_TYPE', + rawNotificationSubtype='NEW_SUBTYPE' + ) + + self.assertIsNotNone(client.get_notification_history(None, notification_history_request)) + def test_get_transaction_history_v1(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', 'GET', @@ -260,7 +312,7 @@ def test_get_transaction_history_v1(self): 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], - 'revoked': ['False']}, + 'revoked': ['false']}, None) request = TransactionHistoryRequest( @@ -274,7 +326,8 @@ def test_get_transaction_history_v1(self): subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) - history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1) + with self.assertWarns(DeprecationWarning): + history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1) self.assertIsNotNone(history_response) self.assertEqual('revision_output', history_response.revision) @@ -297,7 +350,7 @@ def test_get_transaction_history_v2(self): 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], - 'revoked': ['False']}, + 'revoked': ['false']}, None) request = TransactionHistoryRequest( @@ -311,7 +364,10 @@ def test_get_transaction_history_v2(self): subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) - history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter('always') + history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2) + self.assertEqual([], [w for w in caught_warnings if issubclass(w.category, DeprecationWarning)]) self.assertIsNotNone(history_response) self.assertEqual('revision_output', history_response.revision) @@ -322,6 +378,75 @@ def test_get_transaction_history_v2(self): self.assertEqual('LocalTesting', history_response.rawEnvironment) self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions) + def test_get_transaction_history_default_version(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/history/1234', + {}, + None) + + with self.assertWarns(DeprecationWarning): + history_response = client.get_transaction_history('1234', None, TransactionHistoryRequest()) + + self.assertIsNotNone(history_response) + self.assertEqual('revision_output', history_response.revision) + + def test_get_transaction_history_revoked_true(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v2/history/1234', + {'revoked': ['true']}, + None) + + history_response = client.get_transaction_history('1234', None, TransactionHistoryRequest(revoked=True), GetTransactionHistoryVersion.V2) + + self.assertIsNotNone(history_response) + self.assertEqual('revision_output', history_response.revision) + + def test_get_transaction_history_with_string_version(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v2/history/1234', + {}, + None) + + history_response = client.get_transaction_history('1234', None, TransactionHistoryRequest(), 'v2') + + self.assertIsNotNone(history_response) + self.assertEqual('revision_output', history_response.revision) + + def test_path_parameters_are_percent_encoded(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionInfoResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/transactions/..%2F..%2Fv1%2Fnotifications%2Ftest%3Ffoo%3Dbar%23fragment', + {}, + None) + + client.get_transaction_info('../../v1/notifications/test?foo=bar#fragment') + + def test_multiple_path_parameters_are_percent_encoded(self): + client = self.get_client_with_body_from_file('tests/resources/models/getDefaultMessageResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/default/com.example%2Fproduct/en%20US', + {}, + None) + + client.get_default_message('com.example/product', 'en US') + + def test_dot_and_empty_path_parameters_are_rejected(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionInfoResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/transactions/1234', + {}, + None) + + for path_parameter in ('..', '.', ''): + with self.subTest(path_parameter=path_parameter): + with self.assertRaises(ValueError): + client.get_transaction_info(path_parameter) + with self.assertRaises(ValueError): + client.get_default_message('com.example', path_parameter) + def test_get_transaction_info(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionInfoResponse.json', 'GET', @@ -384,7 +509,7 @@ def test_send_consumption_data(self): platform=Platform.NON_APPLE, sampleContentProvided=False, deliveryStatus=DeliveryStatusV1.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE, - appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813', + appAccountToken=UUID('7389a31a-fb6d-4569-a2a6-db7d85d84813'), accountTenure=AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS, playTime=PlayTime.ONE_DAY_TO_FOUR_DAYS, lifetimeDollarsRefunded=LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS, @@ -500,6 +625,62 @@ def test_api_too_many_requests_with_malformed_retry_after(self): self.assertIsNone(e.retry_after) self.assertEqual([raw_retry_after], e.headers['retry-after']) + def test_api_error_with_charset_content_type(self): + client = self.get_client_with_body_from_file('tests/resources/models/apiException.json', + 'POST', + 'https://local-testing-base-url/inApps/v1/notifications/test', + {}, + None, + 500, + response_headers={'Content-Type': 'application/json; charset=utf-8'}) + try: + client.request_test_notification() + except APIException as e: + self.assertEqual(500, e.http_status_code) + self.assertEqual(5000000, e.raw_api_error) + self.assertEqual(APIError.GENERAL_INTERNAL, e.api_error) + self.assertEqual("An unknown error occurred.", e.error_message) + return + + self.assertFalse(True) + + def test_api_error_with_non_json_content_type(self): + client = self.get_client_with_body_from_file('tests/resources/models/apiException.json', + 'POST', + 'https://local-testing-base-url/inApps/v1/notifications/test', + {}, + None, + 500, + response_headers={'Content-Type': 'text/plain'}) + try: + client.request_test_notification() + except APIException as e: + self.assertEqual(500, e.http_status_code) + self.assertIsNone(e.raw_api_error) + self.assertIsNone(e.api_error) + self.assertIsNone(e.error_message) + return + + self.assertFalse(True) + + def test_api_error_without_error_message(self): + client = self.get_client_with_body(b'{"errorCode": 4000006}', + 'GET', + 'https://local-testing-base-url/inApps/v1/transactions/1234', + {}, + None, + 400) + try: + client.get_transaction_info('1234') + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000006, e.raw_api_error) + self.assertEqual(APIError.INVALID_TRANSACTION_ID, e.api_error) + self.assertIsNone(e.error_message) + return + + self.assertFalse(True) + def test_unknown_error(self): client = self.get_client_with_body_from_file('tests/resources/models/apiUnknownError.json', 'POST', @@ -530,7 +711,7 @@ def test_get_transaction_history_with_unknown_environment(self): 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], - 'revoked': ['False']}, + 'revoked': ['false']}, None) request = TransactionHistoryRequest( @@ -561,7 +742,7 @@ def test_get_transaction_history_with_malformed_app_apple_id(self): 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], - 'revoked': ['False']}, + 'revoked': ['false']}, None) request = TransactionHistoryRequest( @@ -575,12 +756,10 @@ def test_get_transaction_history_with_malformed_app_apple_id(self): subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) - try: - client.get_transaction_history('1234', 'revision_input', request) - except Exception: - return - - self.assertFalse(True) + with self.assertWarns(DeprecationWarning): + with self.assertRaises(Exception) as context: + client.get_transaction_history('1234', 'revision_input', request) + self.assertNotIsInstance(context.exception, AssertionError) def test_set_app_account_token(self): client = self.get_client_with_body(b'', @@ -590,7 +769,7 @@ def test_set_app_account_token(self): { 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813' }) - update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813') + update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken=UUID('7389A31A-FB6D-4569-A2A6-DB7D85D84813')) client.set_app_account_token('49571273', update_app_account_token_request) def test_invalid_app_account_token_error(self): @@ -605,7 +784,7 @@ def test_invalid_app_account_token_error(self): 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(APIError.INVALID_APP_ACCOUNT_TOKEN_UUID, e.api_error) self.assertEqual("Invalid request. The app account token field must be a valid UUID.", e.error_message) return @@ -624,7 +803,7 @@ def test_family_transaction_not_supported_error(self): 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(APIError.FAMILY_TRANSACTION_NOT_SUPPORTED, e.api_error) self.assertEqual("Invalid request. Family Sharing transactions aren't supported by this endpoint.", e.error_message) return @@ -644,13 +823,66 @@ def test_transaction_id_not_original_transaction_id_error(self): 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(APIError.TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID, 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 test_api_error_one_name_per_code(self): + expected = { + 4000048: 'APP_TRANSACTION_ID_NOT_SUPPORTED', + 4000183: 'INVALID_APP_ACCOUNT_TOKEN_UUID', + 4000185: 'FAMILY_TRANSACTION_NOT_SUPPORTED', + 4000187: 'TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID', + 4040019: 'APP_TRANSACTION_DOES_NOT_EXIST', + } + for code, name in expected.items(): + self.assertEqual(name, APIError(code).name) + self.assertIs(APIError[name], APIError(code)) + self.assertFalse(hasattr(APIError, name + '_ERROR')) + names_by_code: Dict[int, List[str]] = {} + for name, member in APIError.__members__.items(): + names_by_code.setdefault(member.value, []).append(name) + self.assertEqual({}, {code: names for code, names in names_by_code.items() if len(names) > 1}) + + def test_consumption_percentage_out_of_range_error(self): + client = self.get_client_with_body_from_file('tests/resources/models/consumptionPercentageOutOfRangeError.json', + 'PUT', + 'https://local-testing-base-url/inApps/v2/transactions/consumption/49571273', + {}, + None, + 400) + try: + client.send_consumption_information('49571273', None) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000207, e.raw_api_error) + self.assertEqual(APIError.CONSUMPTION_PERCENTAGE_OUT_OF_RANGE, e.api_error) + self.assertEqual("Invalid request. The consumption percentage is outside the valid range.", e.error_message) + return + + self.assertFalse(True) + + def test_advanced_commerce_subscription_does_not_exist_error(self): + client = self.get_client_with_body_from_file('tests/resources/models/subscriptionDoesNotExistError.json', + 'POST', + 'https://local-testing-base-url/advancedCommerce/v1/subscription/cancel/4124214', + {}, + None, + 403) + try: + client.cancel_subscription('4124214', None) + except APIException as e: + self.assertEqual(403, e.http_status_code) + self.assertEqual(4030008, e.raw_api_error) + self.assertEqual(APIError.SUBSCRIPTION_DOES_NOT_EXIST, e.api_error) + self.assertEqual("The subscription doesn't exist.", e.error_message) + return + + self.assertFalse(True) + def test_upload_image(self): client = self.get_client_with_body(b'', 'PUT', @@ -787,6 +1019,24 @@ def test_get_realtime_url(self): self.assertIsNotNone(response) self.assertEqual('https://example.com/realtime', response.realtimeURL) + def test_get_realtime_url_without_required_field_fails(self): + client = self.get_client_with_body(b'{}', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + None) + with self.assertRaises(ClassValidationError): + client.get_realtime_url() + + def test_get_realtime_url_with_null_required_field_fails(self): + client = self.get_client_with_body(b'{"realtimeURL": null}', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + None) + with self.assertRaises(ClassValidationError): + client.get_realtime_url() + def test_upload_message_with_bullet_points(self): client = self.get_client_with_body(b'', 'PUT', @@ -810,7 +1060,7 @@ def test_initiate_performance_test(self): performance_test_request = PerformanceTestRequest(originalTransactionId='70000500092808') response = client.initiate_performance_test(performance_test_request) self.assertIsNotNone(response) - self.assertEqual('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', response.requestId) + self.assertEqual(UUID('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d'), response.requestId) self.assertIsNotNone(response.config) self.assertEqual(10, response.config.maxConcurrentRequests) self.assertEqual(100, response.config.totalRequests) @@ -824,7 +1074,7 @@ def test_get_performance_test_results(self): 'https://local-testing-base-url/inApps/v1/messaging/performanceTest/result/c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', {}, None) - response = client.get_performance_test_results('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d') + response = client.get_performance_test_results(UUID('C4B87A1D-2E3F-4A5B-9C6D-7E8F9A0B1C2D')) self.assertIsNotNone(response) self.assertIsNotNone(response.config) self.assertEqual(10, response.config.maxConcurrentRequests) @@ -887,7 +1137,7 @@ def test_get_app_transaction_info_app_transaction_does_not_exist(self): except APIException as e: self.assertEqual(404, e.http_status_code) self.assertEqual(4040019, e.raw_api_error) - self.assertEqual(APIError.APP_TRANSACTION_DOES_NOT_EXIST_ERROR, e.api_error) + self.assertEqual(APIError.APP_TRANSACTION_DOES_NOT_EXIST, e.api_error) self.assertEqual("No AppTransaction exists for the customer.", e.error_message) return @@ -1003,6 +1253,32 @@ def test_request_transaction_refund(self): self.assertIsNotNone(response) self.assertEqual('signed_transaction_info_value', response.signedTransactionInfo) + def test_request_transaction_refund_with_signed_renewal_info(self): + client = self.get_client_with_body_from_file('tests/resources/models/advancedCommerceRequestRefundResponseWithSignedRenewalInfo.json', + 'POST', + 'https://local-testing-base-url/advancedCommerce/v1/transaction/requestRefund/4124214', + {}, + {'requestInfo': {'requestReferenceId': '932c6903-0ab8-4469-9f21-015f6fab013c'}, 'items': [{'SKU': 'AD_FREE_1M', 'revoke': True, 'refundReason': 'UNSATISFIED_WITH_PURCHASE', 'refundType': 'FULL'}], 'refundRiskingPreference': True, 'currency': 'USD', 'storefront': 'USA'}) + + request_refund_request = AdvancedCommerceRequestRefundRequest( + requestInfo=AdvancedCommerceRequestInfo(requestReferenceId=UUID('932c6903-0ab8-4469-9f21-015f6fab013c')), + items=[AdvancedCommerceRequestRefundItem( + SKU='AD_FREE_1M', + revoke=True, + refundReason=AdvancedCommerceRefundReason.UNSATISFIED_WITH_PURCHASE, + refundType=AdvancedCommerceRefundType.FULL + )], + refundRiskingPreference=True, + currency='USD', + storefront='USA' + ) + + response = client.request_transaction_refund('4124214', request_refund_request) + + self.assertIsNotNone(response) + self.assertEqual('signed_transaction_info_value', response.signedTransactionInfo) + self.assertEqual('signed_renewal_info_value', response.signedRenewalInfo) + def test_change_subscription_metadata(self): client = self.get_client_with_body_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataResponse.json', 'POST', @@ -1061,6 +1337,158 @@ def test_migrate_subscription_to_advanced_commerce_api(self): self.assertEqual('signed_renewal_info_value', response.signedRenewalInfo) self.assertEqual('signed_transaction_info_value', response.signedTransactionInfo) + def test_get_customer_groups(self): + client = self.get_client_with_body_from_file('tests/resources/models/getCustomerGroupsResponse.json', + 'GET', + 'https://local-testing-base-url/groups/v1/currentGroups/1234', + {}, + None) + + response = client.get_customer_groups('1234') + + self.assertIsNotNone(response) + expected_groups = [ + GroupEntry( + groupId='900000000000000000', + groupType=GroupType.ORGANIZATION, + roles=[ + RoleEntry(productId='example.productId.one', role=Role.ADMIN), + RoleEntry(productId='example.productId.two', rawRole='NONE') + ] + ), + GroupEntry( + groupId='900000000000000001', + groupType=GroupType.CONSUMER + ) + ] + self.assertEqual(expected_groups, response.groups) + self.assertEqual('ORGANIZATION', response.groups[0].rawGroupType) + self.assertEqual(Role.NONE, response.groups[0].roles[1].role) + self.assertEqual('ADMIN', response.groups[0].roles[0].rawRole) + self.assertIsNone(response.groups[1].roles) + + def test_get_customer_groups_with_unknown_values(self): + client = self.get_client_with_body_from_file('tests/resources/models/getCustomerGroupsResponseWithUnknownValues.json', + 'GET', + 'https://local-testing-base-url/groups/v1/currentGroups/1234', + {}, + None) + + response = client.get_customer_groups('1234') + + self.assertIsNotNone(response) + self.assertEqual(1, len(response.groups)) + self.assertEqual('900000000000000000', response.groups[0].groupId) + self.assertIsNone(response.groups[0].groupType) + self.assertEqual('UNKNOWN_GROUP_TYPE', response.groups[0].rawGroupType) + self.assertEqual(1, len(response.groups[0].roles)) + self.assertEqual('example.productId.one', response.groups[0].roles[0].productId) + self.assertIsNone(response.groups[0].roles[0].role) + self.assertEqual('UNKNOWN_ROLE', response.groups[0].roles[0].rawRole) + + def test_get_customer_groups_transaction_id_not_found(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionIdNotFoundError.json', + 'GET', + 'https://local-testing-base-url/groups/v1/currentGroups/not_found_id', + {}, + None, + 404) + try: + client.get_customer_groups('not_found_id') + except APIException as e: + self.assertEqual(404, e.http_status_code) + self.assertEqual(4040010, e.raw_api_error) + self.assertEqual(APIError.TRANSACTION_ID_NOT_FOUND, e.api_error) + self.assertEqual("Transaction id not found.", e.error_message) + return + + self.assertFalse(True) + + def test_get_group_members(self): + client = self.get_client_with_body_from_file('tests/resources/models/getGroupMembersResponse.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/900000000000000000', + {'paginationToken': ['pagination_token_input'], 'limit': ['50']}, + None) + + response = client.get_group_members('900000000000000000', 'pagination_token_input', 50) + + self.assertIsNotNone(response) + expected_members = [ + GroupMemberEntry(appTransactionId='700000000000000000'), + GroupMemberEntry(appTransactionId='700000000000000001') + ] + self.assertEqual(expected_members, response.members) + self.assertEqual('pagination_token_output', response.paginationToken) + self.assertTrue(response.hasMore) + + def test_get_group_members_without_optional_parameters(self): + client = self.get_client_with_body_from_file('tests/resources/models/getGroupMembersResponse.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/900000000000000000', + {}, + None) + + response = client.get_group_members('900000000000000000') + + self.assertIsNotNone(response) + self.assertEqual(2, len(response.members)) + self.assertEqual('700000000000000000', response.members[0].appTransactionId) + + def test_get_group_members_group_not_found(self): + client = self.get_client_with_body_from_file('tests/resources/models/groupNotFoundError.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/900000000000000000', + {}, + None, + 404) + try: + client.get_group_members('900000000000000000') + except APIException as e: + self.assertEqual(404, e.http_status_code) + self.assertEqual(4040022, e.raw_api_error) + self.assertEqual(APIError.GROUP_NOT_FOUND, e.api_error) + self.assertEqual("Group not found.", e.error_message) + return + + self.assertFalse(True) + + def test_get_group_members_invalid_group_id(self): + client = self.get_client_with_body_from_file('tests/resources/models/invalidGroupIdError.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/invalid_id', + {}, + None, + 400) + try: + client.get_group_members('invalid_id') + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000225, e.raw_api_error) + self.assertEqual(APIError.INVALID_GROUP_ID, e.api_error) + self.assertEqual("Invalid request. The group ID is invalid.", e.error_message) + return + + self.assertFalse(True) + + def test_get_group_members_invalid_limit(self): + client = self.get_client_with_body_from_file('tests/resources/models/invalidLimitError.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/900000000000000000', + {'limit': ['101']}, + None, + 400) + try: + client.get_group_members('900000000000000000', limit=101) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000004, e.raw_api_error) + self.assertEqual(APIError.INVALID_LIMIT, e.api_error) + self.assertEqual("Invalid request limit.", e.error_message) + return + + self.assertFalse(True) + def get_signing_key(self): return read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') @@ -1080,6 +1508,9 @@ def fake_execute_and_validate_inputs(method: bytes, url: str, params: Dict[str, self.assertEqual('issuerId', decoded_jwt['payload']['iss']) self.assertEqual('keyId', decoded_jwt['header']['kid']) self.assertEqual('com.example', decoded_jwt['payload']['bid']) + self.assertIsInstance(decoded_jwt['payload']['iat'], int) + self.assertIsInstance(decoded_jwt['payload']['exp'], int) + self.assertEqual(300, decoded_jwt['payload']['exp'] - decoded_jwt['payload']['iat']) # Content-specific validation if expected_data is not None: diff --git a/tests/test_api_client_async.py b/tests/test_api_client_async.py index 2f5a2296..705c2545 100644 --- a/tests/test_api_client_async.py +++ b/tests/test_api_client_async.py @@ -2,7 +2,9 @@ from typing import Any, Dict, List, Union import unittest +import warnings +from cattrs.errors import ClassValidationError from httpx import Response from appstoreserverlibrary.api_client import APIError, APIException, AsyncAppStoreServerAPIClient, GetTransactionHistoryVersion @@ -34,6 +36,9 @@ from appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent from appstoreserverlibrary.models.ExtendReasonCode import ExtendReasonCode from appstoreserverlibrary.models.ExtendRenewalDateRequest import ExtendRenewalDateRequest +from appstoreserverlibrary.models.GroupEntry import GroupEntry +from appstoreserverlibrary.models.GroupMemberEntry import GroupMemberEntry +from appstoreserverlibrary.models.GroupType import GroupType from appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType from appstoreserverlibrary.models.LastTransactionsItem import LastTransactionsItem from appstoreserverlibrary.models.LifetimeDollarsPurchased import LifetimeDollarsPurchased @@ -50,6 +55,8 @@ from appstoreserverlibrary.models.RefundPreference import RefundPreference from appstoreserverlibrary.models.RefundPreferenceV1 import RefundPreferenceV1 from appstoreserverlibrary.models.RevocationReason import RevocationReason +from appstoreserverlibrary.models.Role import Role +from appstoreserverlibrary.models.RoleEntry import RoleEntry from appstoreserverlibrary.models.SendAttemptItem import SendAttemptItem from appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult from appstoreserverlibrary.models.Status import Status @@ -77,7 +84,7 @@ from io import BytesIO -class DecodedPayloads(unittest.IsolatedAsyncioTestCase): +class AsyncAPIClientTest(unittest.IsolatedAsyncioTestCase): async def test_extend_renewal_date_for_all_active_subscribers(self): client = self.get_client_with_body_from_file('tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json', 'POST', @@ -266,7 +273,7 @@ async def test_get_transaction_history_v1(self): 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], - 'revoked': ['False']}, + 'revoked': ['false']}, None) request = TransactionHistoryRequest( @@ -280,7 +287,8 @@ async def test_get_transaction_history_v1(self): subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) - history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1) + with self.assertWarns(DeprecationWarning): + history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1) self.assertIsNotNone(history_response) self.assertEqual('revision_output', history_response.revision) @@ -303,7 +311,7 @@ async def test_get_transaction_history_v2(self): 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], - 'revoked': ['False']}, + 'revoked': ['false']}, None) request = TransactionHistoryRequest( @@ -317,7 +325,10 @@ async def test_get_transaction_history_v2(self): subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) - history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2) + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter('always') + history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2) + self.assertEqual([], [w for w in caught_warnings if issubclass(w.category, DeprecationWarning)]) self.assertIsNotNone(history_response) self.assertEqual('revision_output', history_response.revision) @@ -328,6 +339,75 @@ async def test_get_transaction_history_v2(self): self.assertEqual('LocalTesting', history_response.rawEnvironment) self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions) + async def test_get_transaction_history_default_version(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/history/1234', + {}, + None) + + with self.assertWarns(DeprecationWarning): + history_response = await client.get_transaction_history('1234', None, TransactionHistoryRequest()) + + self.assertIsNotNone(history_response) + self.assertEqual('revision_output', history_response.revision) + + async def test_get_transaction_history_revoked_true(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v2/history/1234', + {'revoked': ['true']}, + None) + + history_response = await client.get_transaction_history('1234', None, TransactionHistoryRequest(revoked=True), GetTransactionHistoryVersion.V2) + + self.assertIsNotNone(history_response) + self.assertEqual('revision_output', history_response.revision) + + async def test_get_transaction_history_with_string_version(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v2/history/1234', + {}, + None) + + history_response = await client.get_transaction_history('1234', None, TransactionHistoryRequest(), 'v2') + + self.assertIsNotNone(history_response) + self.assertEqual('revision_output', history_response.revision) + + async def test_path_parameters_are_percent_encoded(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionInfoResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/transactions/..%2F..%2Fv1%2Fnotifications%2Ftest%3Ffoo%3Dbar%23fragment', + {}, + None) + + await client.get_transaction_info('../../v1/notifications/test?foo=bar#fragment') + + async def test_multiple_path_parameters_are_percent_encoded(self): + client = self.get_client_with_body_from_file('tests/resources/models/getDefaultMessageResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/default/com.example%2Fproduct/en%20US', + {}, + None) + + await client.get_default_message('com.example/product', 'en US') + + async def test_dot_and_empty_path_parameters_are_rejected(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionInfoResponse.json', + 'GET', + 'https://local-testing-base-url/inApps/v1/transactions/1234', + {}, + None) + + for path_parameter in ('..', '.', ''): + with self.subTest(path_parameter=path_parameter): + with self.assertRaises(ValueError): + await client.get_transaction_info(path_parameter) + with self.assertRaises(ValueError): + await client.get_default_message('com.example', path_parameter) + async def test_get_transaction_info(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionInfoResponse.json', 'GET', @@ -390,7 +470,7 @@ async def test_send_consumption_data(self): platform=Platform.NON_APPLE, sampleContentProvided=False, deliveryStatus=DeliveryStatusV1.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE, - appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813', + appAccountToken=UUID('7389a31a-fb6d-4569-a2a6-db7d85d84813'), accountTenure=AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS, playTime=PlayTime.ONE_DAY_TO_FOUR_DAYS, lifetimeDollarsRefunded=LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS, @@ -505,6 +585,62 @@ async def test_api_too_many_requests_with_malformed_retry_after(self): self.assertIsNone(e.retry_after) self.assertEqual([raw_retry_after], e.headers['retry-after']) + async def test_api_error_with_charset_content_type(self): + client = self.get_client_with_body_from_file('tests/resources/models/apiException.json', + 'POST', + 'https://local-testing-base-url/inApps/v1/notifications/test', + {}, + None, + 500, + response_headers={'Content-Type': 'application/json; charset=utf-8'}) + try: + await client.request_test_notification() + except APIException as e: + self.assertEqual(500, e.http_status_code) + self.assertEqual(5000000, e.raw_api_error) + self.assertEqual(APIError.GENERAL_INTERNAL, e.api_error) + self.assertEqual("An unknown error occurred.", e.error_message) + return + + self.assertFalse(True) + + async def test_api_error_with_non_json_content_type(self): + client = self.get_client_with_body_from_file('tests/resources/models/apiException.json', + 'POST', + 'https://local-testing-base-url/inApps/v1/notifications/test', + {}, + None, + 500, + response_headers={'Content-Type': 'text/plain'}) + try: + await client.request_test_notification() + except APIException as e: + self.assertEqual(500, e.http_status_code) + self.assertIsNone(e.raw_api_error) + self.assertIsNone(e.api_error) + self.assertIsNone(e.error_message) + return + + self.assertFalse(True) + + async def test_api_error_without_error_message(self): + client = self.get_client_with_body(b'{"errorCode": 4000006}', + 'GET', + 'https://local-testing-base-url/inApps/v1/transactions/1234', + {}, + None, + 400) + try: + await client.get_transaction_info('1234') + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000006, e.raw_api_error) + self.assertEqual(APIError.INVALID_TRANSACTION_ID, e.api_error) + self.assertIsNone(e.error_message) + return + + self.assertFalse(True) + async def test_unknown_error(self): client = self.get_client_with_body_from_file('tests/resources/models/apiUnknownError.json', 'POST', @@ -535,7 +671,7 @@ async def test_get_transaction_history_with_unknown_environment(self): 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], - 'revoked': ['False']}, + 'revoked': ['false']}, None) request = TransactionHistoryRequest( @@ -566,7 +702,7 @@ async def test_get_transaction_history_with_malformed_app_apple_id(self): 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], - 'revoked': ['False']}, + 'revoked': ['false']}, None) request = TransactionHistoryRequest( @@ -580,12 +716,10 @@ async def test_get_transaction_history_with_malformed_app_apple_id(self): subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) - try: - await client.get_transaction_history('1234', 'revision_input', request) - except Exception: - return - - self.assertFalse(True) + with self.assertWarns(DeprecationWarning): + with self.assertRaises(Exception) as context: + await client.get_transaction_history('1234', 'revision_input', request) + self.assertNotIsInstance(context.exception, AssertionError) async def test_set_app_account_token(self): client = self.get_client_with_body(b'', @@ -595,7 +729,7 @@ async def test_set_app_account_token(self): { 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813' }) - update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813') + update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken=UUID('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): @@ -610,7 +744,7 @@ async def test_invalid_app_account_token_error(self): 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(APIError.INVALID_APP_ACCOUNT_TOKEN_UUID, e.api_error) self.assertEqual("Invalid request. The app account token field must be a valid UUID.", e.error_message) return @@ -629,7 +763,7 @@ async def test_family_transaction_not_supported_error(self): 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(APIError.FAMILY_TRANSACTION_NOT_SUPPORTED, e.api_error) self.assertEqual("Invalid request. Family Sharing transactions aren't supported by this endpoint.", e.error_message) return @@ -649,12 +783,48 @@ async def test_transaction_id_not_original_transaction_id_error(self): 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(APIError.TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID, e.api_error) self.assertEqual("Invalid request. The transaction ID provided is not an original transaction ID.", e.error_message) return self.assertFalse(True) + async def test_consumption_percentage_out_of_range_error(self): + client = self.get_client_with_body_from_file('tests/resources/models/consumptionPercentageOutOfRangeError.json', + 'PUT', + 'https://local-testing-base-url/inApps/v2/transactions/consumption/49571273', + {}, + None, + 400) + try: + await client.send_consumption_information('49571273', None) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000207, e.raw_api_error) + self.assertEqual(APIError.CONSUMPTION_PERCENTAGE_OUT_OF_RANGE, e.api_error) + self.assertEqual("Invalid request. The consumption percentage is outside the valid range.", e.error_message) + return + + self.assertFalse(True) + + async def test_advanced_commerce_subscription_does_not_exist_error(self): + client = self.get_client_with_body_from_file('tests/resources/models/subscriptionDoesNotExistError.json', + 'POST', + 'https://local-testing-base-url/advancedCommerce/v1/subscription/cancel/4124214', + {}, + None, + 403) + try: + await client.cancel_subscription('4124214', None) + except APIException as e: + self.assertEqual(403, e.http_status_code) + self.assertEqual(4030008, e.raw_api_error) + self.assertEqual(APIError.SUBSCRIPTION_DOES_NOT_EXIST, e.api_error) + self.assertEqual("The subscription doesn't exist.", e.error_message) + return + + self.assertFalse(True) + async def test_upload_image(self): client = self.get_client_with_body(b'', 'PUT', @@ -791,6 +961,24 @@ async def test_get_realtime_url(self): self.assertIsNotNone(response) self.assertEqual('https://example.com/realtime', response.realtimeURL) + async def test_get_realtime_url_without_required_field_fails(self): + client = self.get_client_with_body(b'{}', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + None) + with self.assertRaises(ClassValidationError): + await client.get_realtime_url() + + async def test_get_realtime_url_with_null_required_field_fails(self): + client = self.get_client_with_body(b'{"realtimeURL": null}', + 'GET', + 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', + {}, + None) + with self.assertRaises(ClassValidationError): + await client.get_realtime_url() + async def test_upload_message_with_bullet_points(self): client = self.get_client_with_body(b'', 'PUT', @@ -814,7 +1002,7 @@ async def test_initiate_performance_test(self): performance_test_request = PerformanceTestRequest(originalTransactionId='70000500092808') response = await client.initiate_performance_test(performance_test_request) self.assertIsNotNone(response) - self.assertEqual('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', response.requestId) + self.assertEqual(UUID('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d'), response.requestId) self.assertIsNotNone(response.config) self.assertEqual(10, response.config.maxConcurrentRequests) self.assertEqual(100, response.config.totalRequests) @@ -828,7 +1016,7 @@ async def test_get_performance_test_results(self): 'https://local-testing-base-url/inApps/v1/messaging/performanceTest/result/c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', {}, None) - response = await client.get_performance_test_results('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d') + response = await client.get_performance_test_results(UUID('C4B87A1D-2E3F-4A5B-9C6D-7E8F9A0B1C2D')) self.assertIsNotNone(response) self.assertIsNotNone(response.config) self.assertEqual(10, response.config.maxConcurrentRequests) @@ -892,7 +1080,7 @@ async def test_get_app_transaction_info_app_transaction_does_not_exist(self): except APIException as e: self.assertEqual(404, e.http_status_code) self.assertEqual(4040019, e.raw_api_error) - self.assertEqual(APIError.APP_TRANSACTION_DOES_NOT_EXIST_ERROR, e.api_error) + self.assertEqual(APIError.APP_TRANSACTION_DOES_NOT_EXIST, e.api_error) self.assertEqual("No AppTransaction exists for the customer.", e.error_message) return @@ -1008,6 +1196,32 @@ async def test_request_transaction_refund(self): self.assertIsNotNone(response) self.assertEqual('signed_transaction_info_value', response.signedTransactionInfo) + async def test_request_transaction_refund_with_signed_renewal_info(self): + client = self.get_client_with_body_from_file('tests/resources/models/advancedCommerceRequestRefundResponseWithSignedRenewalInfo.json', + 'POST', + 'https://local-testing-base-url/advancedCommerce/v1/transaction/requestRefund/4124214', + {}, + {'requestInfo': {'requestReferenceId': '932c6903-0ab8-4469-9f21-015f6fab013c'}, 'items': [{'SKU': 'AD_FREE_1M', 'revoke': True, 'refundReason': 'UNSATISFIED_WITH_PURCHASE', 'refundType': 'FULL'}], 'refundRiskingPreference': True, 'currency': 'USD', 'storefront': 'USA'}) + + request_refund_request = AdvancedCommerceRequestRefundRequest( + requestInfo=AdvancedCommerceRequestInfo(requestReferenceId=UUID('932c6903-0ab8-4469-9f21-015f6fab013c')), + items=[AdvancedCommerceRequestRefundItem( + SKU='AD_FREE_1M', + revoke=True, + refundReason=AdvancedCommerceRefundReason.UNSATISFIED_WITH_PURCHASE, + refundType=AdvancedCommerceRefundType.FULL + )], + refundRiskingPreference=True, + currency='USD', + storefront='USA' + ) + + response = await client.request_transaction_refund('4124214', request_refund_request) + + self.assertIsNotNone(response) + self.assertEqual('signed_transaction_info_value', response.signedTransactionInfo) + self.assertEqual('signed_renewal_info_value', response.signedRenewalInfo) + async def test_change_subscription_metadata(self): client = self.get_client_with_body_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataResponse.json', 'POST', @@ -1066,6 +1280,158 @@ async def test_migrate_subscription_to_advanced_commerce_api(self): self.assertEqual('signed_renewal_info_value', response.signedRenewalInfo) self.assertEqual('signed_transaction_info_value', response.signedTransactionInfo) + async def test_get_customer_groups(self): + client = self.get_client_with_body_from_file('tests/resources/models/getCustomerGroupsResponse.json', + 'GET', + 'https://local-testing-base-url/groups/v1/currentGroups/1234', + {}, + None) + + response = await client.get_customer_groups('1234') + + self.assertIsNotNone(response) + expected_groups = [ + GroupEntry( + groupId='900000000000000000', + groupType=GroupType.ORGANIZATION, + roles=[ + RoleEntry(productId='example.productId.one', role=Role.ADMIN), + RoleEntry(productId='example.productId.two', rawRole='NONE') + ] + ), + GroupEntry( + groupId='900000000000000001', + groupType=GroupType.CONSUMER + ) + ] + self.assertEqual(expected_groups, response.groups) + self.assertEqual('ORGANIZATION', response.groups[0].rawGroupType) + self.assertEqual(Role.NONE, response.groups[0].roles[1].role) + self.assertEqual('ADMIN', response.groups[0].roles[0].rawRole) + self.assertIsNone(response.groups[1].roles) + + async def test_get_customer_groups_with_unknown_values(self): + client = self.get_client_with_body_from_file('tests/resources/models/getCustomerGroupsResponseWithUnknownValues.json', + 'GET', + 'https://local-testing-base-url/groups/v1/currentGroups/1234', + {}, + None) + + response = await client.get_customer_groups('1234') + + self.assertIsNotNone(response) + self.assertEqual(1, len(response.groups)) + self.assertEqual('900000000000000000', response.groups[0].groupId) + self.assertIsNone(response.groups[0].groupType) + self.assertEqual('UNKNOWN_GROUP_TYPE', response.groups[0].rawGroupType) + self.assertEqual(1, len(response.groups[0].roles)) + self.assertEqual('example.productId.one', response.groups[0].roles[0].productId) + self.assertIsNone(response.groups[0].roles[0].role) + self.assertEqual('UNKNOWN_ROLE', response.groups[0].roles[0].rawRole) + + async def test_get_customer_groups_transaction_id_not_found(self): + client = self.get_client_with_body_from_file('tests/resources/models/transactionIdNotFoundError.json', + 'GET', + 'https://local-testing-base-url/groups/v1/currentGroups/not_found_id', + {}, + None, + 404) + try: + await client.get_customer_groups('not_found_id') + except APIException as e: + self.assertEqual(404, e.http_status_code) + self.assertEqual(4040010, e.raw_api_error) + self.assertEqual(APIError.TRANSACTION_ID_NOT_FOUND, e.api_error) + self.assertEqual("Transaction id not found.", e.error_message) + return + + self.assertFalse(True) + + async def test_get_group_members(self): + client = self.get_client_with_body_from_file('tests/resources/models/getGroupMembersResponse.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/900000000000000000', + {'paginationToken': ['pagination_token_input'], 'limit': ['50']}, + None) + + response = await client.get_group_members('900000000000000000', 'pagination_token_input', 50) + + self.assertIsNotNone(response) + expected_members = [ + GroupMemberEntry(appTransactionId='700000000000000000'), + GroupMemberEntry(appTransactionId='700000000000000001') + ] + self.assertEqual(expected_members, response.members) + self.assertEqual('pagination_token_output', response.paginationToken) + self.assertTrue(response.hasMore) + + async def test_get_group_members_without_optional_parameters(self): + client = self.get_client_with_body_from_file('tests/resources/models/getGroupMembersResponse.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/900000000000000000', + {}, + None) + + response = await client.get_group_members('900000000000000000') + + self.assertIsNotNone(response) + self.assertEqual(2, len(response.members)) + self.assertEqual('700000000000000000', response.members[0].appTransactionId) + + async def test_get_group_members_group_not_found(self): + client = self.get_client_with_body_from_file('tests/resources/models/groupNotFoundError.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/900000000000000000', + {}, + None, + 404) + try: + await client.get_group_members('900000000000000000') + except APIException as e: + self.assertEqual(404, e.http_status_code) + self.assertEqual(4040022, e.raw_api_error) + self.assertEqual(APIError.GROUP_NOT_FOUND, e.api_error) + self.assertEqual("Group not found.", e.error_message) + return + + self.assertFalse(True) + + async def test_get_group_members_invalid_group_id(self): + client = self.get_client_with_body_from_file('tests/resources/models/invalidGroupIdError.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/invalid_id', + {}, + None, + 400) + try: + await client.get_group_members('invalid_id') + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000225, e.raw_api_error) + self.assertEqual(APIError.INVALID_GROUP_ID, e.api_error) + self.assertEqual("Invalid request. The group ID is invalid.", e.error_message) + return + + self.assertFalse(True) + + async def test_get_group_members_invalid_limit(self): + client = self.get_client_with_body_from_file('tests/resources/models/invalidLimitError.json', + 'GET', + 'https://local-testing-base-url/groups/v1/group/900000000000000000', + {'limit': ['101']}, + None, + 400) + try: + await client.get_group_members('900000000000000000', limit=101) + except APIException as e: + self.assertEqual(400, e.http_status_code) + self.assertEqual(4000004, e.raw_api_error) + self.assertEqual(APIError.INVALID_LIMIT, e.api_error) + self.assertEqual("Invalid request limit.", e.error_message) + return + + self.assertFalse(True) + def get_signing_key(self): return read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') @@ -1084,6 +1450,9 @@ async def fake_execute_and_validate_inputs(method: bytes, url: str, params: Dict self.assertEqual('issuerId', decoded_jwt['payload']['iss']) self.assertEqual('keyId', decoded_jwt['header']['kid']) self.assertEqual('com.example', decoded_jwt['payload']['bid']) + self.assertIsInstance(decoded_jwt['payload']['iat'], int) + self.assertIsInstance(decoded_jwt['payload']['exp'], int) + self.assertEqual(300, decoded_jwt['payload']['exp'] - decoded_jwt['payload']['iat']) # Content-specific validation if expected_data is not None: diff --git a/tests/test_api_models.py b/tests/test_api_models.py new file mode 100644 index 00000000..2a7a876c --- /dev/null +++ b/tests/test_api_models.py @@ -0,0 +1,501 @@ +# Copyright (c) 2026 Apple Inc. Licensed under MIT License. + +from abc import ABC +import importlib +import inspect +import json +import pkgutil +from typing import Optional, Union, get_args, get_origin +import unittest +from uuid import UUID + +import attr +from cattrs.errors import ClassValidationError +import jwt +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + +from appstoreserverlibrary.models.AccountTenure import AccountTenure +from appstoreserverlibrary.models.ConsumptionRequest import ConsumptionRequest +from appstoreserverlibrary.models.ConsumptionRequestV1 import ConsumptionRequestV1 +from appstoreserverlibrary.models.ConsumptionStatus import ConsumptionStatus +from appstoreserverlibrary.models.DeliveryStatus import DeliveryStatus +from appstoreserverlibrary.models.DeliveryStatusV1 import DeliveryStatusV1 +from appstoreserverlibrary.models.ExtendReasonCode import ExtendReasonCode +from appstoreserverlibrary.models.ExtendRenewalDateRequest import ExtendRenewalDateRequest +from appstoreserverlibrary.models.JWSTransactionDecodedPayload import JWSTransactionDecodedPayload +from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter, metadata_required_key, metadata_type_key +from appstoreserverlibrary.models.LifetimeDollarsPurchased import LifetimeDollarsPurchased +from appstoreserverlibrary.models.LifetimeDollarsRefunded import LifetimeDollarsRefunded +from appstoreserverlibrary.models.MassExtendRenewalDateRequest import MassExtendRenewalDateRequest +from appstoreserverlibrary.models.MassExtendRenewalDateResponse import MassExtendRenewalDateResponse +from appstoreserverlibrary.models.MassExtendRenewalDateStatusResponse import MassExtendRenewalDateStatusResponse +from appstoreserverlibrary.models.NotificationHistoryRequest import NotificationHistoryRequest +from appstoreserverlibrary.models.NotificationTypeV2 import NotificationTypeV2 +from appstoreserverlibrary.models.Platform import Platform +from appstoreserverlibrary.models.PlayTime import PlayTime +from appstoreserverlibrary.models.RealtimeUrlResponse import RealtimeUrlResponse +from appstoreserverlibrary.models.RefundPreferenceV1 import RefundPreferenceV1 +from appstoreserverlibrary.models.Subtype import Subtype +from appstoreserverlibrary.models.Summary import Summary +from appstoreserverlibrary.models.TransactionCommitmentInfo import TransactionCommitmentInfo +from appstoreserverlibrary.models.TransactionHistoryRequest import Order, ProductType +from appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest +from appstoreserverlibrary.models.UserStatus import UserStatus + +import appstoreserverlibrary.models +from appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus +from tests.util import get_default_signed_data_verifier, read_data_from_file + + +def create_consumption_request_v1(**kwargs) -> ConsumptionRequestV1: + values = dict( + customerConsented=True, + consumptionStatus=ConsumptionStatus.NOT_CONSUMED, + platform=Platform.NON_APPLE, + sampleContentProvided=False, + deliveryStatus=DeliveryStatusV1.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE, + appAccountToken=UUID('7389a31a-fb6d-4569-a2a6-db7d85d84813'), + accountTenure=AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS, + playTime=PlayTime.ONE_DAY_TO_FOUR_DAYS, + lifetimeDollarsRefunded=LifetimeDollarsRefunded.ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS, + lifetimeDollarsPurchased=LifetimeDollarsPurchased.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS, + userStatus=UserStatus.LIMITED_ACCESS, + ) + values.update(kwargs) + return ConsumptionRequestV1(**values) + + +EXTEND_RENEWAL_DATE_REQUESTS = [ + (ExtendRenewalDateRequest, dict(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac')), + (MassExtendRenewalDateRequest, dict(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', productId='com.example.productId')), +] + + +class APIModels(unittest.TestCase): + def test_consumption_request_v1_sends_empty_app_account_token_when_none(self): + consumption_request = create_consumption_request_v1(appAccountToken=None) + + json_dict = _get_cattrs_converter(ConsumptionRequestV1).unstructure(consumption_request) + + self.assertIsNone(consumption_request.appAccountToken) + self.assertEqual('', json_dict['appAccountToken']) + + def test_consumption_request_v1_sends_empty_app_account_token_when_set_to_none(self): + consumption_request = create_consumption_request_v1(appAccountToken=UUID('7389a31a-fb6d-4569-a2a6-db7d85d84813')) + consumption_request.appAccountToken = None + + json_dict = _get_cattrs_converter(ConsumptionRequestV1).unstructure(consumption_request) + + self.assertEqual('', json_dict['appAccountToken']) + + def test_consumption_request_v1_sends_provided_app_account_token(self): + consumption_request = create_consumption_request_v1(appAccountToken=UUID('7389A31A-FB6D-4569-A2A6-DB7D85D84813')) + + json_dict = _get_cattrs_converter(ConsumptionRequestV1).unstructure(consumption_request) + + self.assertEqual(UUID('7389a31a-fb6d-4569-a2a6-db7d85d84813'), consumption_request.appAccountToken) + self.assertEqual('7389a31a-fb6d-4569-a2a6-db7d85d84813', json_dict['appAccountToken']) + + def test_consumption_request_v1_takes_keyword_arguments_only(self): + parameters = inspect.signature(ConsumptionRequestV1).parameters + self.assertEqual([], [name for name, p in parameters.items() if p.kind != inspect.Parameter.KEYWORD_ONLY]) + + values = dict( + customerConsented=True, + consumptionStatus=ConsumptionStatus.NOT_CONSUMED, + platform=Platform.NON_APPLE, + sampleContentProvided=False, + deliveryStatus=DeliveryStatusV1.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE, + appAccountToken=UUID('7389a31a-fb6d-4569-a2a6-db7d85d84813'), + accountTenure=AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS, + playTime=PlayTime.ONE_DAY_TO_FOUR_DAYS, + lifetimeDollarsRefunded=LifetimeDollarsRefunded.ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS, + lifetimeDollarsPurchased=LifetimeDollarsPurchased.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS, + userStatus=UserStatus.LIMITED_ACCESS, + refundPreference=RefundPreferenceV1.NO_PREFERENCE, + ) + with self.assertRaisesRegex(TypeError, 'positional argument'): + ConsumptionRequestV1(*values.values()) + enums = ['consumptionStatus', 'platform', 'deliveryStatus'] + with self.assertRaisesRegex(TypeError, 'positional argument'): + ConsumptionRequestV1(*[v for k, v in values.items() if k not in enums], **{k: values[k] for k in enums}) + with self.assertRaisesRegex(TypeError, 'positional argument'): + ConsumptionRequestV1(True, **{k: v for k, v in values.items() if k != 'customerConsented'}) + + consumption_request = ConsumptionRequestV1(**values) + + for name, value in values.items(): + self.assertEqual(value, getattr(consumption_request, name)) + self.assertEqual(1, consumption_request.rawConsumptionStatus) + self.assertEqual(2, consumption_request.rawPlatform) + self.assertEqual(3, consumption_request.rawDeliveryStatus) + self.assertEqual(3, consumption_request.rawRefundPreference) + self.assertEqual(7, create_consumption_request_v1(platform=None, rawPlatform=7).rawPlatform) + + def test_update_app_account_token_request_sends_lowercase_uuid(self): + request = UpdateAppAccountTokenRequest(appAccountToken=UUID('7389A31A-FB6D-4569-A2A6-DB7D85D84813')) + + self.assertEqual({'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813'}, + _get_cattrs_converter(UpdateAppAccountTokenRequest).unstructure(request)) + with self.assertRaises(TypeError): + UpdateAppAccountTokenRequest() + + def test_consumption_request_required_enum_set_to_none_error_names_field(self): + consumption_request = ConsumptionRequest(customerConsented=True, sampleContentProvided=False, deliveryStatus=DeliveryStatus.DELIVERED) + + with self.assertRaises(ValueError) as context: + consumption_request.deliveryStatus = None + + self.assertEqual('deliveryStatus cannot be set to None when field is required', str(context.exception)) + + def test_consumption_request_v1_serializes_every_required_field(self): + json_dict = _get_cattrs_converter(ConsumptionRequestV1).unstructure(create_consumption_request_v1()) + + self.assertEqual({ + 'customerConsented': True, + 'consumptionStatus': 1, + 'platform': 2, + 'sampleContentProvided': False, + 'deliveryStatus': 3, + 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813', + 'accountTenure': 4, + 'playTime': 5, + 'lifetimeDollarsRefunded': 4, + 'lifetimeDollarsPurchased': 6, + 'userStatus': 4, + }, json_dict) + + def test_consumption_request_v1_requires_documented_fields(self): + for field in ['customerConsented', 'sampleContentProvided', 'appAccountToken']: + values = dict(customerConsented=True, sampleContentProvided=False, appAccountToken=None) + del values[field] + with self.subTest(field=field), self.assertRaises(TypeError): + ConsumptionRequestV1(consumptionStatus=ConsumptionStatus.NOT_CONSUMED, platform=Platform.NON_APPLE, + deliveryStatus=DeliveryStatusV1.DELIVERED_AND_WORKING_PROPERLY, + accountTenure=AccountTenure.UNDECLARED, playTime=PlayTime.UNDECLARED, + lifetimeDollarsRefunded=LifetimeDollarsRefunded.UNDECLARED, + lifetimeDollarsPurchased=LifetimeDollarsPurchased.UNDECLARED, + userStatus=UserStatus.UNDECLARED, **values) + + for field in ['consumptionStatus', 'platform', 'deliveryStatus', 'accountTenure', 'playTime', 'lifetimeDollarsRefunded', 'lifetimeDollarsPurchased', 'userStatus']: + with self.subTest(field=field), self.assertRaises(ValueError) as context: + create_consumption_request_v1(**{field: None}) + self.assertEqual(f'Either {field} or raw{field[0].upper() + field[1:]} must be provided', str(context.exception)) + + def test_consumption_request_v1_accepts_raw_values_for_required_enums(self): + consumption_request = create_consumption_request_v1(platform=None, rawPlatform=7) + + self.assertIsNone(consumption_request.platform) + self.assertEqual(7, consumption_request.rawPlatform) + self.assertEqual(7, _get_cattrs_converter(ConsumptionRequestV1).unstructure(consumption_request)['platform']) + + def test_consumption_request_requires_documented_fields(self): + with self.assertRaises(TypeError): + ConsumptionRequest(sampleContentProvided=False, deliveryStatus=DeliveryStatus.DELIVERED) + with self.assertRaises(TypeError): + ConsumptionRequest(customerConsented=True, deliveryStatus=DeliveryStatus.DELIVERED) + with self.assertRaises(ValueError): + ConsumptionRequest(customerConsented=True, sampleContentProvided=False) + + def test_extend_renewal_date_request_requires_documented_fields(self): + request = ExtendRenewalDateRequest(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac') + self.assertEqual({'extendByDays': 45, 'extendReasonCode': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac'}, + _get_cattrs_converter(ExtendRenewalDateRequest).unstructure(request)) + + with self.assertRaises(TypeError): + ExtendRenewalDateRequest(extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac') + with self.assertRaises(TypeError): + ExtendRenewalDateRequest(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION) + with self.assertRaises(ValueError) as context: + ExtendRenewalDateRequest(extendByDays=45, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac') + self.assertEqual('Either extendReasonCode or rawExtendReasonCode must be provided', str(context.exception)) + + def test_mass_extend_renewal_date_request_requires_documented_fields(self): + request = MassExtendRenewalDateRequest(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', productId='com.example.productId') + self.assertEqual({'extendByDays': 45, 'extendReasonCode': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac', 'productId': 'com.example.productId'}, + _get_cattrs_converter(MassExtendRenewalDateRequest).unstructure(request)) + + with self.assertRaises(TypeError): + MassExtendRenewalDateRequest(extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', productId='com.example.productId') + with self.assertRaises(TypeError): + MassExtendRenewalDateRequest(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, productId='com.example.productId') + with self.assertRaises(TypeError): + MassExtendRenewalDateRequest(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', storefrontCountryCodes=['USA']) + with self.assertRaises(ValueError) as context: + MassExtendRenewalDateRequest(extendByDays=45, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', productId='com.example.productId') + self.assertEqual('Either extendReasonCode or rawExtendReasonCode must be provided', str(context.exception)) + + def test_extend_renewal_date_requests_take_keyword_arguments_only(self): + for cls, values in EXTEND_RENEWAL_DATE_REQUESTS: + with self.subTest(cls=cls.__name__): + parameters = inspect.signature(cls).parameters + self.assertEqual([], [name for name, p in parameters.items() if p.kind != inspect.Parameter.KEYWORD_ONLY]) + + with self.assertRaisesRegex(TypeError, 'positional argument'): + cls(*values.values()) + with self.assertRaisesRegex(TypeError, 'positional argument'): + cls(45, **{k: v for k, v in values.items() if k != 'extendByDays'}) + + request = cls(**values) + for name, value in values.items(): + self.assertEqual(value, getattr(request, name)) + + def test_extend_renewal_date_requests_send_unknown_raw_extend_reason_code(self): + for cls, values in EXTEND_RENEWAL_DATE_REQUESTS: + values = {k: v for k, v in values.items() if k != 'extendReasonCode'} + with self.subTest(cls=cls.__name__): + request = cls(rawExtendReasonCode=99, **values) + + self.assertIsNone(request.extendReasonCode) + self.assertEqual(99, request.rawExtendReasonCode) + self.assertEqual(dict(values, extendReasonCode=99), _get_cattrs_converter(cls).unstructure(request)) + + self.assertEqual(ExtendReasonCode.SERVICE_ISSUE_OR_OUTAGE, cls(rawExtendReasonCode=3, **values).extendReasonCode) + + request.rawExtendReasonCode = 100 + self.assertIsNone(request.extendReasonCode) + self.assertEqual(100, _get_cattrs_converter(cls).unstructure(request)['extendReasonCode']) + with self.assertRaises(ValueError): + request.rawExtendReasonCode = None + self.assertEqual(100, request.rawExtendReasonCode) + + def test_extend_renewal_date_requests_send_typed_extend_reason_code(self): + for cls, values in EXTEND_RENEWAL_DATE_REQUESTS: + with self.subTest(cls=cls.__name__): + request = cls(**values) + + self.assertEqual(ExtendReasonCode.CUSTOMER_SATISFACTION, request.extendReasonCode) + self.assertEqual(1, request.rawExtendReasonCode) + self.assertEqual(dict(values, extendReasonCode=1), _get_cattrs_converter(cls).unstructure(request)) + + request.extendReasonCode = ExtendReasonCode.OTHER + self.assertEqual(2, request.rawExtendReasonCode) + self.assertEqual(2, _get_cattrs_converter(cls).unstructure(request)['extendReasonCode']) + with self.assertRaises(ValueError): + request.extendReasonCode = None + self.assertEqual(ExtendReasonCode.OTHER, request.extendReasonCode) + + def test_notification_history_request_requires_documented_fields(self): + with self.assertRaises(TypeError): + NotificationHistoryRequest(startDate=1698148900000) + with self.assertRaises(TypeError): + NotificationHistoryRequest(endDate=1698148950000) + + def test_notification_history_request_takes_keyword_arguments_only(self): + parameters = inspect.signature(NotificationHistoryRequest).parameters + self.assertEqual(['startDate', 'endDate', 'notificationType', 'rawNotificationType', 'notificationSubtype', 'rawNotificationSubtype', 'onlyFailures', 'transactionId'], + list(parameters)) + self.assertEqual([], [name for name, p in parameters.items() if p.kind != inspect.Parameter.KEYWORD_ONLY]) + with self.assertRaisesRegex(TypeError, 'positional argument'): + NotificationHistoryRequest(1698148900000, 1698148950000) + + def test_notification_history_request_sends_unknown_raw_notification_type_and_subtype(self): + request = NotificationHistoryRequest(startDate=1698148900000, endDate=1698148950000, rawNotificationType='NEW_NOTIFICATION_TYPE', rawNotificationSubtype='NEW_SUBTYPE') + + self.assertIsNone(request.notificationType) + self.assertEqual('NEW_NOTIFICATION_TYPE', request.rawNotificationType) + self.assertIsNone(request.notificationSubtype) + self.assertEqual('NEW_SUBTYPE', request.rawNotificationSubtype) + self.assertEqual({'startDate': 1698148900000, 'endDate': 1698148950000, 'notificationType': 'NEW_NOTIFICATION_TYPE', 'notificationSubtype': 'NEW_SUBTYPE'}, + _get_cattrs_converter(NotificationHistoryRequest).unstructure(request)) + + request = NotificationHistoryRequest(startDate=1698148900000, endDate=1698148950000, rawNotificationType='SUBSCRIBED', rawNotificationSubtype='INITIAL_BUY') + self.assertEqual(NotificationTypeV2.SUBSCRIBED, request.notificationType) + self.assertEqual(Subtype.INITIAL_BUY, request.notificationSubtype) + + request.rawNotificationType = 'ANOTHER_NOTIFICATION_TYPE' + request.rawNotificationSubtype = 'ANOTHER_SUBTYPE' + self.assertIsNone(request.notificationType) + self.assertIsNone(request.notificationSubtype) + self.assertEqual({'startDate': 1698148900000, 'endDate': 1698148950000, 'notificationType': 'ANOTHER_NOTIFICATION_TYPE', 'notificationSubtype': 'ANOTHER_SUBTYPE'}, + _get_cattrs_converter(NotificationHistoryRequest).unstructure(request)) + + def test_notification_history_request_sends_typed_notification_type_and_subtype(self): + request = NotificationHistoryRequest(startDate=1698148900000, endDate=1698148950000, notificationType=NotificationTypeV2.SUBSCRIBED, notificationSubtype=Subtype.INITIAL_BUY, transactionId='999733843', onlyFailures=True) + + self.assertEqual(NotificationTypeV2.SUBSCRIBED, request.notificationType) + self.assertEqual('SUBSCRIBED', request.rawNotificationType) + self.assertEqual(Subtype.INITIAL_BUY, request.notificationSubtype) + self.assertEqual('INITIAL_BUY', request.rawNotificationSubtype) + self.assertEqual('999733843', request.transactionId) + self.assertTrue(request.onlyFailures) + self.assertEqual({'startDate': 1698148900000, 'endDate': 1698148950000, 'notificationType': 'SUBSCRIBED', 'notificationSubtype': 'INITIAL_BUY', 'transactionId': '999733843', 'onlyFailures': True}, + _get_cattrs_converter(NotificationHistoryRequest).unstructure(request)) + + request.notificationType = None + request.notificationSubtype = None + self.assertIsNone(request.rawNotificationType) + self.assertIsNone(request.rawNotificationSubtype) + self.assertEqual({'startDate': 1698148900000, 'endDate': 1698148950000, 'transactionId': '999733843', 'onlyFailures': True}, + _get_cattrs_converter(NotificationHistoryRequest).unstructure(request)) + + def test_decoded_models_check_their_documented_limits(self): + valid = [(TransactionCommitmentInfo, {'billingPeriodNumber': 1}), (TransactionCommitmentInfo, {'billingPeriodNumber': 12}), + (JWSTransactionDecodedPayload, {'revocationPercentage': 0}), (JWSTransactionDecodedPayload, {'revocationPercentage': 100000}), + (Summary, {'storefrontCountryCodes': ['USA']}), + (MassExtendRenewalDateResponse, {'requestIdentifier': 'a' * 128}), + (MassExtendRenewalDateStatusResponse, {'requestIdentifier': 'a' * 128}), + (RealtimeUrlResponse, {'realtimeURL': 'a' * 256})] + invalid = [(TransactionCommitmentInfo, {'billingPeriodNumber': 0}), (TransactionCommitmentInfo, {'billingPeriodNumber': 13}), + (JWSTransactionDecodedPayload, {'revocationPercentage': -1}), (JWSTransactionDecodedPayload, {'revocationPercentage': 100001}), + (Summary, {'storefrontCountryCodes': ['USAA']}), + (MassExtendRenewalDateResponse, {'requestIdentifier': 'a' * 129}), + (MassExtendRenewalDateStatusResponse, {'requestIdentifier': 'a' * 129}), + (RealtimeUrlResponse, {'realtimeURL': 'a' * 257})] + for model, value in valid: + with self.subTest(model=model.__name__, value=value): + decoded = _get_cattrs_converter(model).structure(value, model) + key, expected = next(iter(value.items())) + self.assertEqual(expected, getattr(decoded, key)) + for model, value in invalid: + with self.subTest(model=model.__name__, value=value), self.assertRaises(ClassValidationError): + _get_cattrs_converter(model).structure(value, model) + + def test_transaction_with_out_of_range_billing_period_number_fails_to_decode(self): + transaction_json = json.loads(read_data_from_file('tests/resources/models/signedTransaction.json')) + transaction_json['commitmentInfo']['billingPeriodNumber'] = 13 + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + signed_transaction = jwt.encode(payload=transaction_json, key=private_key, algorithm='ES256') + + with self.assertRaises(VerificationException) as context: + get_default_signed_data_verifier().verify_and_decode_transaction(signed_transaction) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_transaction_history_request_enums(self): + self.assertTrue('AUTO_RENEWABLE' in ProductType) + self.assertFalse('INVALID' in ProductType) + self.assertTrue('DESCENDING' in Order) + self.assertFalse('INVALID' in Order) + + def test_required_fields_reject_none_when_built(self): + cases = [ + (ExtendRenewalDateRequest, dict(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac')), + (MassExtendRenewalDateRequest, dict(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', productId='com.example.productId')), + (NotificationHistoryRequest, dict(startDate=1698148900000, endDate=1698148950000)), + (UpdateAppAccountTokenRequest, dict(appAccountToken=UUID('7389a31a-fb6d-4569-a2a6-db7d85d84813'))), + (ConsumptionRequest, dict(customerConsented=True, sampleContentProvided=False, deliveryStatus=DeliveryStatus.DELIVERED)), + ] + for cls, values in cases: + for field in values: + raw_backed = attr.fields_dict(cls)[field].metadata.get(metadata_type_key) == 'main' + expected = f'Either {field} or raw{field[0].upper() + field[1:]} must be provided' if raw_backed else f'{field} cannot be None' + with self.subTest(field=f'{cls.__name__}.{field}'): + with self.assertRaises(ValueError) as context: + cls(**dict(values, **{field: None})) + self.assertEqual(expected, str(context.exception)) + + model = cls(**values) + with self.assertRaises(ValueError): + setattr(model, field, None) + self.assertEqual(values[field], getattr(model, field)) + + for field in ['customerConsented', 'sampleContentProvided']: + with self.subTest(field=f'ConsumptionRequestV1.{field}'), self.assertRaises(ValueError): + create_consumption_request_v1(**{field: None}) + + def test_consumption_request_v1_app_account_token_is_mandatory_and_nullable(self): + self.assertEqual(Optional[UUID], attr.fields(ConsumptionRequestV1).appAccountToken.type) + self.assertIs(inspect.Parameter.empty, inspect.signature(ConsumptionRequestV1).parameters['appAccountToken'].default) + consumption_request = create_consumption_request_v1(appAccountToken=None) + self.assertIsNone(consumption_request.appAccountToken) + + def test_every_required_attribute_rejects_none(self): + nullable = {'ConsumptionRequestV1.appAccountToken'} + checked = 0 + for module_info in pkgutil.iter_modules(appstoreserverlibrary.models.__path__): + module = importlib.import_module(f'appstoreserverlibrary.models.{module_info.name}') + for cls in list(vars(module).values()): + if not inspect.isclass(cls) or not attr.has(cls) or cls.__module__ != module.__name__ or ABC in cls.__bases__: + continue + attr.resolve_types(cls) + for attribute in attr.fields(cls): + if attribute.metadata.get(metadata_type_key) == 'raw': + required = attribute.metadata.get(metadata_required_key, False) + else: + required = attribute.init and attribute.default is attr.NOTHING + name = f'{cls.__name__}.{attribute.name}' + if not required or name in nullable: + continue + checked += 1 + with self.subTest(attribute=name): + self.assertIsNotNone(attribute.validator) + with self.assertRaises(ValueError): + attribute.validator(None, attribute, None) + if not (get_origin(attribute.type) is Union and type(None) in get_args(attribute.type)): + with self.assertRaises(Exception): + _get_cattrs_converter(cls).structure(None, attribute.type) + self.assertGreater(checked, 150) + + def test_every_model_takes_keyword_arguments_only(self): + hand_written = {'AbstractAdvancedCommerceBaseItem', 'AbstractAdvancedCommerceInAppRequest', 'AbstractAdvancedCommerceItem', + 'AbstractAdvancedCommerceResponse', 'AdvancedCommerceRequest', 'TransactionHistoryRequest'} + positional_kinds = (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.VAR_POSITIONAL) + checked = 0 + for module_info in pkgutil.iter_modules(appstoreserverlibrary.models.__path__): + module = importlib.import_module(f'appstoreserverlibrary.models.{module_info.name}') + for name, cls in list(vars(module).items()): + if not inspect.isclass(cls) or not attr.has(cls) or cls.__module__ != module.__name__ or name.startswith('_') \ + or name in hand_written: + continue + checked += 1 + with self.subTest(model=name): + parameters = inspect.signature(cls).parameters + self.assertEqual([], [p for p, parameter in parameters.items() if parameter.kind in positional_kinds]) + self.assertGreater(checked, 100) + + def test_extend_renewal_date_request_limits(self): + for cls, extra in ((ExtendRenewalDateRequest, {}), (MassExtendRenewalDateRequest, {'productId': 'com.example.productId'})): + with self.subTest(model=cls.__name__): + for days in (1, 90): + request = cls(extendByDays=days, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='a' * 128, **extra) + self.assertEqual(days, request.extendByDays) + request.extendByDays = days + self.assertEqual(days, cls(extendByDays=days, rawExtendReasonCode=99, requestIdentifier='id', **extra).extendByDays) + request = cls(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='id', **extra) + for days in (0, 91): + with self.assertRaises(ValueError): + cls(extendByDays=days, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='id', **extra) + with self.assertRaises(ValueError): + cls(extendByDays=days, rawExtendReasonCode=99, requestIdentifier='id', **extra) + with self.assertRaises(ValueError): + request.extendByDays = days + self.assertEqual(45, request.extendByDays) + request.requestIdentifier = 'a' * 128 + with self.assertRaises(ValueError): + cls(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='a' * 129, **extra) + with self.assertRaises(ValueError): + request.requestIdentifier = 'a' * 129 + self.assertEqual('a' * 128, request.requestIdentifier) + + def test_mass_extend_renewal_date_request_storefront_country_code_limits(self): + request = MassExtendRenewalDateRequest(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='id', + productId='com.example.productId', storefrontCountryCodes=['USA', 'CA']) + self.assertEqual(['USA', 'CA'], request.storefrontCountryCodes) + request.storefrontCountryCodes = None + with self.assertRaises(ValueError): + MassExtendRenewalDateRequest(extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='id', + productId='com.example.productId', storefrontCountryCodes=['USA', 'ABCD']) + with self.assertRaises(ValueError): + request.storefrontCountryCodes = ['ABCD'] + self.assertIsNone(request.storefrontCountryCodes) + + def test_consumption_request_consumption_percentage_limits(self): + request = ConsumptionRequest(customerConsented=True, sampleContentProvided=False, deliveryStatus=DeliveryStatus.DELIVERED) + self.assertIsNone(request.consumptionPercentage) + for percentage in (0, 100000): + self.assertEqual(percentage, ConsumptionRequest(customerConsented=True, sampleContentProvided=False, deliveryStatus=DeliveryStatus.DELIVERED, + consumptionPercentage=percentage).consumptionPercentage) + request.consumptionPercentage = percentage + for percentage in (-1, 100001): + with self.assertRaises(ValueError): + ConsumptionRequest(customerConsented=True, sampleContentProvided=False, deliveryStatus=DeliveryStatus.DELIVERED, + consumptionPercentage=percentage) + with self.assertRaises(ValueError): + request.consumptionPercentage = percentage + self.assertEqual(100000, request.consumptionPercentage) + request.consumptionPercentage = None + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_decoded_payloads.py b/tests/test_decoded_payloads.py index 92199294..e6707d08 100644 --- a/tests/test_decoded_payloads.py +++ b/tests/test_decoded_payloads.py @@ -1,23 +1,34 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +import json from typing import Optional import unittest from uuid import UUID + +from cattrs.errors import ClassValidationError +import jwt +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus from appstoreserverlibrary.models.BillingPlanType import BillingPlanType from appstoreserverlibrary.models.ConsumptionRequestReason import ConsumptionRequestReason from appstoreserverlibrary.models.Environment import Environment from appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent from appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType +from appstoreserverlibrary.models.JWSTransactionDecodedPayload import JWSTransactionDecodedPayload +from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter from appstoreserverlibrary.models.NotificationTypeV2 import NotificationTypeV2 from appstoreserverlibrary.models.OfferDiscountType import OfferDiscountType from appstoreserverlibrary.models.OfferType import OfferType from appstoreserverlibrary.models.PriceIncreaseStatus import PriceIncreaseStatus from appstoreserverlibrary.models.PurchasePlatform import PurchasePlatform from appstoreserverlibrary.models.RenewalBillingPlanType import RenewalBillingPlanType +from appstoreserverlibrary.models.ResponseBodyV2DecodedPayload import ResponseBodyV2DecodedPayload from appstoreserverlibrary.models.RevocationReason import RevocationReason from appstoreserverlibrary.models.RevocationType import RevocationType from appstoreserverlibrary.models.Status import Status +from appstoreserverlibrary.models.StoreType import StoreType from appstoreserverlibrary.models.Subtype import Subtype from appstoreserverlibrary.models.TokenType import TokenType from appstoreserverlibrary.models.TransactionReason import TransactionReason @@ -26,8 +37,9 @@ from appstoreserverlibrary.models.AdvancedCommercePriceIncreaseInfoStatus import AdvancedCommercePriceIncreaseInfoStatus from appstoreserverlibrary.models.AdvancedCommerceRefundReason import AdvancedCommerceRefundReason from appstoreserverlibrary.models.AdvancedCommerceRefundType import AdvancedCommerceRefundType +from appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus -from tests.util import create_signed_data_from_json, get_default_signed_data_verifier, get_signed_data_verifier +from tests.util import create_signed_data_from_json, get_default_signed_data_verifier, get_signed_data_verifier, read_data_from_file class DecodedPayloads(unittest.TestCase): def test_app_transaction_decoding(self): @@ -46,18 +58,20 @@ def test_app_transaction_decoding(self): self.assertEqual(1698148800000, app_transaction.originalPurchaseDate) self.assertEqual("1.1.2", app_transaction.originalApplicationVersion) self.assertEqual("device_verification_value", app_transaction.deviceVerification) - self.assertEqual("48ccfa42-7431-4f22-9908-7e88983e105a", app_transaction.deviceVerificationNonce) + self.assertEqual(UUID("48ccfa42-7431-4f22-9908-7e88983e105a"), app_transaction.deviceVerificationNonce) self.assertEqual(1698148700000, app_transaction.preorderDate) self.assertEqual("71134", app_transaction.appTransactionId) self.assertEqual(PurchasePlatform.IOS, app_transaction.originalPlatform) self.assertEqual("iOS", app_transaction.rawOriginalPlatform) + self.assertEqual(StoreType.CONSUMER, app_transaction.storeType) + self.assertEqual("CONSUMER", app_transaction.rawStoreType) def test_transaction_decoding(self): signed_transaction = create_signed_data_from_json('tests/resources/models/signedTransaction.json') signed_data_verifier = get_default_signed_data_verifier() - transaction = signed_data_verifier.verify_and_decode_signed_transaction(signed_transaction) + transaction = signed_data_verifier.verify_and_decode_transaction(signed_transaction) self.assertEqual("12345", transaction.originalTransactionId) self.assertEqual("23456", transaction.transactionId) @@ -72,7 +86,7 @@ def test_transaction_decoding(self): self.assertEqual(1, transaction.quantity) self.assertEqual(Type.AUTO_RENEWABLE_SUBSCRIPTION, transaction.type) self.assertEqual("Auto-Renewable Subscription", transaction.rawType) - self.assertEqual("7e3fb20b-4cdb-47cc-936d-99d65f608138", transaction.appAccountToken) + self.assertEqual(UUID("7e3fb20b-4cdb-47cc-936d-99d65f608138"), transaction.appAccountToken) self.assertEqual(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType) self.assertEqual("PURCHASED", transaction.rawInAppOwnershipType) self.assertEqual(1698148900000, transaction.signedDate) @@ -131,6 +145,7 @@ def test_transaction_decoding(self): self.assertEqual(1698150000000, commitment.commitmentExpiresDate) self.assertEqual(119880, commitment.commitmentPrice) self.assertEqual(12, commitment.totalBillingPeriods) + self.assertEqual("11111", transaction.previousOriginalTransactionId) def test_non_ascii_data_decoding_is_independent_of_the_default_charset(self): """ @@ -142,7 +157,7 @@ def test_non_ascii_data_decoding_is_independent_of_the_default_charset(self): signed_data_verifier = get_default_signed_data_verifier() - transaction = signed_data_verifier.verify_and_decode_signed_transaction(signed_transaction) + transaction = signed_data_verifier.verify_and_decode_transaction(signed_transaction) advanced_commerce_info = transaction.advancedCommerceInfo self.assertEqual("Abonnement Caf\u00e9 \u2014 5,99 \u20ac par mois", advanced_commerce_info.descriptors.description) @@ -155,7 +170,7 @@ def test_transaction_with_revocation_decoding(self): signed_data_verifier = get_default_signed_data_verifier() - transaction = signed_data_verifier.verify_and_decode_signed_transaction(signed_transaction) + transaction = signed_data_verifier.verify_and_decode_transaction(signed_transaction) self.assertEqual("12345", transaction.originalTransactionId) self.assertEqual("23456", transaction.transactionId) @@ -170,7 +185,7 @@ def test_transaction_with_revocation_decoding(self): self.assertEqual(1, transaction.quantity) self.assertEqual(Type.AUTO_RENEWABLE_SUBSCRIPTION, transaction.type) self.assertEqual("Auto-Renewable Subscription", transaction.rawType) - self.assertEqual("7e3fb20b-4cdb-47cc-936d-99d65f608138", transaction.appAccountToken) + self.assertEqual(UUID("7e3fb20b-4cdb-47cc-936d-99d65f608138"), transaction.appAccountToken) self.assertEqual(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType) self.assertEqual("PURCHASED", transaction.rawInAppOwnershipType) self.assertEqual(1698148900000, transaction.signedDate) @@ -196,6 +211,18 @@ def test_transaction_with_revocation_decoding(self): self.assertEqual("REFUND_PRORATED", transaction.rawRevocationType) self.assertEqual(50000, transaction.revocationPercentage) + def test_transaction_with_assigned_ownership_and_assignment_revoke_decoding(self): + transaction_dict = json.loads(read_data_from_file('tests/resources/models/signedTransactionWithRevocation.json')) + transaction_dict['inAppOwnershipType'] = 'ASSIGNED' + transaction_dict['revocationType'] = 'ASSIGNMENT_REVOKE' + + transaction = _get_cattrs_converter(JWSTransactionDecodedPayload).structure(transaction_dict, JWSTransactionDecodedPayload) + + self.assertEqual(InAppOwnershipType.ASSIGNED, transaction.inAppOwnershipType) + self.assertEqual("ASSIGNED", transaction.rawInAppOwnershipType) + self.assertEqual(RevocationType.ASSIGNMENT_REVOKE, transaction.revocationType) + self.assertEqual("ASSIGNMENT_REVOKE", transaction.rawRevocationType) + def test_renewal_info_decoding(self): @@ -231,7 +258,7 @@ def test_renewal_info_decoding(self): self.assertEqual(['eligible1', 'eligible2'], renewal_info.eligibleWinBackOfferIds) self.assertEqual("71134", renewal_info.appTransactionId) self.assertEqual("P1Y", renewal_info.offerPeriod) - self.assertEqual("7e3fb20b-4cdb-47cc-936d-99d65f608138", renewal_info.appAccountToken) + self.assertEqual(UUID("7e3fb20b-4cdb-47cc-936d-99d65f608138"), renewal_info.appAccountToken) self.assertIsNotNone(renewal_info.advancedCommerceInfo) acInfo = renewal_info.advancedCommerceInfo self.assertEqual("token-abc-123", acInfo.consistencyToken) @@ -346,7 +373,7 @@ def test_summary_notification_decoding(self): self.assertEqual(41234, notification.summary.appAppleId) self.assertEqual("com.example", notification.summary.bundleId) self.assertEqual("com.example.product", notification.summary.productId) - self.assertEqual("efb27071-45a4-4aca-9854-2a1e9146f265", notification.summary.requestIdentifier) + self.assertEqual(UUID("efb27071-45a4-4aca-9854-2a1e9146f265"), notification.summary.requestIdentifier) self.assertEqual(["CAN", "USA", "MEX"], notification.summary.storefrontCountryCodes) self.assertEqual(5, notification.summary.succeededCount) self.assertEqual(2, notification.summary.failedCount) @@ -414,7 +441,147 @@ def check_environment_and_bundle_id(bundle_id: Optional[str], app_apple_id: Opti self.assertEqual(TokenType.ACQUISITION, notification.externalPurchaseToken.tokenType) self.assertEqual("ACQUISITION", notification.externalPurchaseToken.rawTokenType) self.assertEqual(1698149000000, notification.externalPurchaseToken.tokenExpirationDate) + + def test_external_purchase_token_core_technology_decoding(self): + notification_dict = json.loads(read_data_from_file('tests/resources/models/signedExternalPurchaseTokenNotification.json')) + notification_dict['externalPurchaseToken']['tokenType'] = 'CORE_TECHNOLOGY' + + notification = _get_cattrs_converter(ResponseBodyV2DecodedPayload).structure(notification_dict, ResponseBodyV2DecodedPayload) + + self.assertEqual(TokenType.CORE_TECHNOLOGY, notification.externalPurchaseToken.tokenType) + self.assertEqual("CORE_TECHNOLOGY", notification.externalPurchaseToken.rawTokenType) + def test_uuid_fields_decode_uppercase_values(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + verifier = get_default_signed_data_verifier() + + transaction_dict = json.loads(read_data_from_file('tests/resources/models/signedTransaction.json')) + transaction_dict['appAccountToken'] = '7E3FB20B-4CDB-47CC-936D-99D65F608138' + transaction = verifier.verify_and_decode_transaction(jwt.encode(payload=transaction_dict, key=private_key, algorithm='ES256')) + self.assertEqual(UUID('7e3fb20b-4cdb-47cc-936d-99d65f608138'), transaction.appAccountToken) + + renewal_info_dict = json.loads(read_data_from_file('tests/resources/models/signedRenewalInfo.json')) + renewal_info_dict['appAccountToken'] = '7E3FB20B-4CDB-47CC-936D-99D65F608138' + renewal_info = verifier.verify_and_decode_renewal_info(jwt.encode(payload=renewal_info_dict, key=private_key, algorithm='ES256')) + self.assertEqual(UUID('7e3fb20b-4cdb-47cc-936d-99d65f608138'), renewal_info.appAccountToken) + + app_transaction_dict = json.loads(read_data_from_file('tests/resources/models/appTransaction.json')) + app_transaction_dict['deviceVerificationNonce'] = '48CCFA42-7431-4F22-9908-7E88983E105A' + app_transaction = verifier.verify_and_decode_app_transaction(jwt.encode(payload=app_transaction_dict, key=private_key, algorithm='ES256')) + self.assertEqual(UUID('48ccfa42-7431-4f22-9908-7e88983e105a'), app_transaction.deviceVerificationNonce) + + def test_uuid_fields_are_optional_when_decoding(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + verifier = get_default_signed_data_verifier() + + transaction_dict = json.loads(read_data_from_file('tests/resources/models/signedTransaction.json')) + del transaction_dict['appAccountToken'] + transaction = verifier.verify_and_decode_transaction(jwt.encode(payload=transaction_dict, key=private_key, algorithm='ES256')) + self.assertIsNone(transaction.appAccountToken) + + renewal_info_dict = json.loads(read_data_from_file('tests/resources/models/signedRenewalInfo.json')) + del renewal_info_dict['appAccountToken'] + renewal_info = verifier.verify_and_decode_renewal_info(jwt.encode(payload=renewal_info_dict, key=private_key, algorithm='ES256')) + self.assertIsNone(renewal_info.appAccountToken) + + def test_summary_request_identifier_decodes_into_uuid(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + verifier = get_default_signed_data_verifier() + + notification_dict = json.loads(read_data_from_file('tests/resources/models/signedSummaryNotification.json')) + notification_dict['summary']['requestIdentifier'] = 'EFB27071-45A4-4ACA-9854-2A1E9146F265' + notification = verifier.verify_and_decode_notification(jwt.encode(payload=notification_dict, key=private_key, algorithm='ES256')) + self.assertIsInstance(notification.summary.requestIdentifier, UUID) + self.assertEqual(UUID('efb27071-45a4-4aca-9854-2a1e9146f265'), notification.summary.requestIdentifier) + + notification_dict = json.loads(read_data_from_file('tests/resources/models/signedSummaryNotification.json')) + notification = _get_cattrs_converter(ResponseBodyV2DecodedPayload).structure(notification_dict, ResponseBodyV2DecodedPayload) + self.assertEqual(UUID('efb27071-45a4-4aca-9854-2a1e9146f265'), notification.summary.requestIdentifier) + + del notification_dict['summary']['requestIdentifier'] + notification = _get_cattrs_converter(ResponseBodyV2DecodedPayload).structure(notification_dict, ResponseBodyV2DecodedPayload) + self.assertIsNone(notification.summary.requestIdentifier) + + def test_summary_request_identifier_rejects_non_uuid_value(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + notification_dict = json.loads(read_data_from_file('tests/resources/models/signedSummaryNotification.json')) + notification_dict['summary']['requestIdentifier'] = 'not-a-uuid' + + with self.assertRaises(VerificationException) as context: + get_default_signed_data_verifier().verify_and_decode_notification(jwt.encode(payload=notification_dict, key=private_key, algorithm='ES256')) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + with self.assertRaises(ClassValidationError) as context: + _get_cattrs_converter(ResponseBodyV2DecodedPayload).structure(notification_dict, ResponseBodyV2DecodedPayload) + summary_error = context.exception.exceptions[0] + self.assertIsInstance(summary_error, ClassValidationError) + self.assertIsInstance(summary_error.exceptions[0], ValueError) + + def test_uuid_fields_reject_non_uuid_values_when_decoding(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + verifier = get_default_signed_data_verifier() + cases = [ + ('JWSTransactionDecodedPayload.appAccountToken', 'tests/resources/models/signedTransaction.json', [], 'appAccountToken', verifier.verify_and_decode_transaction), + ('JWSRenewalInfoDecodedPayload.appAccountToken', 'tests/resources/models/signedRenewalInfo.json', [], 'appAccountToken', verifier.verify_and_decode_renewal_info), + ('AppTransaction.deviceVerificationNonce', 'tests/resources/models/appTransaction.json', [], 'deviceVerificationNonce', verifier.verify_and_decode_app_transaction), + ('DecodedRealtimeRequestBody.requestIdentifier', 'tests/resources/models/decodedRealtimeRequest.json', [], 'requestIdentifier', verifier.verify_and_decode_realtime_request), + ('Summary.requestIdentifier', 'tests/resources/models/signedSummaryNotification.json', ['summary'], 'requestIdentifier', verifier.verify_and_decode_notification), + ] + for name, path, parents, key, decode in cases: + payload = json.loads(read_data_from_file(path)) + target = payload + for parent in parents: + target = target[parent] + target[key] = 'not-a-uuid' + with self.subTest(field=name): + with self.assertRaises(VerificationException) as context: + decode(jwt.encode(payload=payload, key=private_key, algorithm='ES256')) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_external_purchase_token_without_required_field_fails_decoding(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + for field in ['externalPurchaseId', 'tokenCreationDate', 'appAppleId', 'bundleId']: + notification_dict = json.loads(read_data_from_file('tests/resources/models/signedExternalPurchaseTokenNotification.json')) + del notification_dict['externalPurchaseToken'][field] + signed_notification = jwt.encode(payload=notification_dict, key=private_key, algorithm='ES256') + + with self.subTest(field=field), self.assertRaises(VerificationException) as context: + get_default_signed_data_verifier().verify_and_decode_notification(signed_notification) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_realtime_request_without_required_field_fails_decoding(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + for field in ['originalTransactionId', 'appAppleId', 'productId', 'userLocale', 'requestIdentifier', 'environment', 'signedDate']: + request_dict = json.loads(read_data_from_file('tests/resources/models/decodedRealtimeRequest.json')) + del request_dict[field] + signed_realtime_request = jwt.encode(payload=request_dict, key=private_key, algorithm='ES256') + + with self.subTest(field=field), self.assertRaises(VerificationException) as context: + get_default_signed_data_verifier().verify_and_decode_realtime_request(signed_realtime_request) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_external_purchase_token_with_null_required_field_fails_decoding(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + for field in ['externalPurchaseId', 'tokenCreationDate', 'appAppleId', 'bundleId']: + notification_dict = json.loads(read_data_from_file('tests/resources/models/signedExternalPurchaseTokenNotification.json')) + notification_dict['externalPurchaseToken'][field] = None + signed_notification = jwt.encode(payload=notification_dict, key=private_key, algorithm='ES256') + + with self.subTest(field=field), self.assertRaises(VerificationException) as context: + get_default_signed_data_verifier().verify_and_decode_notification(signed_notification) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_realtime_request_with_null_required_field_fails_decoding(self): + private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() + for field in ['originalTransactionId', 'appAppleId', 'productId', 'userLocale', 'requestIdentifier', 'environment', 'signedDate']: + request_dict = json.loads(read_data_from_file('tests/resources/models/decodedRealtimeRequest.json')) + request_dict[field] = None + signed_realtime_request = jwt.encode(payload=request_dict, key=private_key, algorithm='ES256') + + with self.subTest(field=field), self.assertRaises(VerificationException) as context: + get_default_signed_data_verifier().verify_and_decode_realtime_request(signed_realtime_request) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + def test_realtime_request_decoding(self): signed_realtime_request = create_signed_data_from_json('tests/resources/models/decodedRealtimeRequest.json') diff --git a/tests/test_jws_signature_creator.py b/tests/test_jws_signature_creator.py index 857c83a3..0e04f9f5 100644 --- a/tests/test_jws_signature_creator.py +++ b/tests/test_jws_signature_creator.py @@ -6,11 +6,17 @@ import json import jwt import unittest -from appstoreserverlibrary.jws_signature_creator import AdvancedCommerceAPIInAppRequest, AdvancedCommerceAPIInAppSignatureCreator, IntroductoryOfferEligibilitySignatureCreator, PromotionalOfferV2SignatureCreator +from uuid import UUID +from cryptography.hazmat.primitives import serialization +from appstoreserverlibrary.jws_signature_creator import AdvancedCommerceInAppRequest, AdvancedCommerceInAppSignatureCreator, IntroductoryOfferEligibilitySignatureCreator, PromotionalOfferV2SignatureCreator +from appstoreserverlibrary.models.AdvancedCommerceEffective import AdvancedCommerceEffective +from appstoreserverlibrary.models.AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo +from appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyDescriptors import AdvancedCommerceSubscriptionModifyDescriptors +from appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyInAppRequest import AdvancedCommerceSubscriptionModifyInAppRequest from tests.util import read_data_from_binary_file @define -class TestInAppRequest(AdvancedCommerceAPIInAppRequest): +class TestInAppRequest(AdvancedCommerceInAppRequest): test_value: str class JWSSignatureCreatorTest(unittest.TestCase): @@ -44,6 +50,15 @@ def test_promotional_offer_signature_creator_transaction_id_missing(self): payload = jwt.decode(signature, options={"verify_signature": False}) self.assertFalse('transactionId' in payload) + def test_promotional_offer_signature_creator_transaction_id_omitted(self): + signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') + signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') + signature = signature_creator.create_signature("productId", "offerIdentifier") + payload = jwt.decode(signature, options={"verify_signature": False}) + self.assertEqual('productId', payload['productId']) + self.assertEqual('offerIdentifier', payload['offerIdentifier']) + self.assertFalse('transactionId' in payload) + def test_promotional_offer_signature_creator_offer_identifier_missing(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') @@ -99,7 +114,7 @@ def test_introductory_offer_eligibility_signature_creator_product_id_missing(sel def test_advanced_commerce_api_in_app_signature_creator(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') - signature_creator = AdvancedCommerceAPIInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') + signature_creator = AdvancedCommerceInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') request = TestInAppRequest("testValue") signature = signature_creator.create_signature(request) self.assertIsNotNone(signature) @@ -123,6 +138,42 @@ def test_advanced_commerce_api_in_app_signature_creator(self): def test_advanced_commerce_api_in_app_signature_creator_request_missing(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') - signature_creator = AdvancedCommerceAPIInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') + signature_creator = AdvancedCommerceInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') with self.assertRaises(ValueError): - signature_creator.create_signature(None) \ No newline at end of file + signature_creator.create_signature(None) + + def test_advanced_commerce_in_app_signature_creator_with_subscription_modify_in_app_request(self): + signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') + signature_creator = AdvancedCommerceInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') + request = AdvancedCommerceSubscriptionModifyInAppRequest( + requestInfo=AdvancedCommerceRequestInfo(requestReferenceId=UUID("550e8400-e29b-41d4-a716-446655440007")), + transactionId="transactionId", + retainBillingCycle=True, + descriptors=AdvancedCommerceSubscriptionModifyDescriptors(effective=AdvancedCommerceEffective.IMMEDIATELY, description="description", displayName="display name"), + taxCode="taxCode", + currency="USD" + ) + signature = signature_creator.create_signature(request) + public_key = serialization.load_pem_private_key(signing_key, password=None).public_key() + payload = jwt.decode(signature, public_key, algorithms=['ES256'], audience='advanced-commerce-api') + self.assertEqual('advanced-commerce-api', payload['aud']) + decode_json = json.loads(base64.b64decode(payload['request']).decode('utf-8')) + self.assertEqual({ + "operation": "MODIFY_SUBSCRIPTION", + "version": "1", + "requestInfo": { + "requestReferenceId": "550e8400-e29b-41d4-a716-446655440007" + }, + "transactionId": "transactionId", + "retainBillingCycle": True, + "descriptors": { + "effective": "IMMEDIATELY", + "description": "description", + "displayName": "display name" + }, + "taxCode": "taxCode", + "currency": "USD" + }, decode_json) + + def test_advanced_commerce_in_app_requests_extend_advanced_commerce_in_app_request(self): + self.assertTrue(issubclass(AdvancedCommerceSubscriptionModifyInAppRequest, AdvancedCommerceInAppRequest)) diff --git a/tests/test_payload_verification.py b/tests/test_payload_verification.py index 331d745f..2222326c 100644 --- a/tests/test_payload_verification.py +++ b/tests/test_payload_verification.py @@ -1,15 +1,36 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +from typing import Any, Dict import unittest from base64 import b64decode +from unittest.mock import MagicMock + +import jwt +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + from appstoreserverlibrary.models.Environment import Environment from appstoreserverlibrary.models.NotificationHistoryRequest import NotificationTypeV2 from appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus, SignedDataVerifier -from tests.util import get_signed_data_verifier, read_data_from_file +from tests.util import get_signed_data_verifier, read_data_from_binary_file, read_data_from_file + +def sign_with_mocked_chain(verifier: SignedDataVerifier, payload: Dict[str, Any]) -> str: + private_key = ec.generate_private_key(ec.SECP256R1()) + public_key = private_key.public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode() + verifier._chain_verifier.verify_chain = MagicMock(return_value=public_key) + return jwt.encode(payload, private_key, algorithm='ES256', headers={'x5c': ['leaf', 'intermediate', 'root']}) + +def sign_without_chain(payload: Dict[str, Any]) -> str: + private_key = ec.generate_private_key(ec.SECP256R1()) + return jwt.encode(payload, private_key, algorithm='ES256') class PayloadVerification(unittest.TestCase): + def test_production_environment_requires_app_apple_id(self): + with self.assertRaises(ValueError): + SignedDataVerifier([read_data_from_binary_file('tests/resources/certs/testCA.der')], False, Environment.PRODUCTION, "com.example", None) + def test_app_store_server_notification_decoding(self): verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification') @@ -53,9 +74,135 @@ def test_renewal_info_decoding(self): def test_transaction_info_decoding(self): verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") transaction_info = read_data_from_file('tests/resources/mock_signed_data/transactionInfo') - notification = verifier.verify_and_decode_signed_transaction(transaction_info) + notification = verifier.verify_and_decode_transaction(transaction_info) self.assertEqual(notification.environment, Environment.SANDBOX) + def test_wrong_bundle_id_for_transaction(self): + verifier = get_signed_data_verifier(Environment.SANDBOX, "com.examplex") + transaction_info = read_data_from_file('tests/resources/mock_signed_data/transactionInfo') + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_transaction(transaction_info) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) + + def test_wrong_environment_for_transaction(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example") + transaction_info = read_data_from_file('tests/resources/mock_signed_data/transactionInfo') + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_transaction(transaction_info) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT) + + def test_wrong_environment_for_renewal_info(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example") + renewal_info = read_data_from_file('tests/resources/mock_signed_data/renewalInfo') + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_renewal_info(renewal_info) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT) + + def test_app_transaction_decoding_production(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1234) + signed_app_transaction = sign_with_mocked_chain(verifier, {"receiptType": "Production", "appAppleId": 1234, "bundleId": "com.example", "receiptCreationDate": 1698148900000}) + app_transaction = verifier.verify_and_decode_app_transaction(signed_app_transaction) + self.assertEqual(Environment.PRODUCTION, app_transaction.receiptType) + self.assertEqual(1234, app_transaction.appAppleId) + self.assertEqual(1, verifier._chain_verifier.verify_chain.call_count) + + def test_wrong_bundle_id_for_app_transaction(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1234) + signed_app_transaction = sign_with_mocked_chain(verifier, {"receiptType": "Production", "appAppleId": 1234, "bundleId": "com.examplex", "receiptCreationDate": 1698148900000}) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_app_transaction(signed_app_transaction) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) + + def test_wrong_app_apple_id_for_app_transaction(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1234) + signed_app_transaction = sign_with_mocked_chain(verifier, {"receiptType": "Production", "appAppleId": 1235, "bundleId": "com.example", "receiptCreationDate": 1698148900000}) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_app_transaction(signed_app_transaction) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) + + def test_wrong_environment_for_app_transaction(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1234) + signed_app_transaction = sign_with_mocked_chain(verifier, {"receiptType": "Sandbox", "appAppleId": 1234, "bundleId": "com.example", "receiptCreationDate": 1698148900000}) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_app_transaction(signed_app_transaction) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT) + + def test_realtime_request_decoding_production(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1234) + signed_realtime_request = sign_with_mocked_chain(verifier, self._realtime_request(1234, "Production")) + realtime_request = verifier.verify_and_decode_realtime_request(signed_realtime_request) + self.assertEqual(Environment.PRODUCTION, realtime_request.environment) + self.assertEqual(1234, realtime_request.appAppleId) + + def test_wrong_app_apple_id_for_realtime_request(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1234) + signed_realtime_request = sign_with_mocked_chain(verifier, self._realtime_request(1235, "Production")) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_realtime_request(signed_realtime_request) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) + + def test_wrong_environment_for_realtime_request(self): + verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1234) + signed_realtime_request = sign_with_mocked_chain(verifier, self._realtime_request(1234, "Sandbox")) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_realtime_request(signed_realtime_request) + self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT) + + def test_algorithm_other_than_es256(self): + verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") + signed_transaction = jwt.encode({"bundleId": "com.example", "environment": "Sandbox", "signedDate": 1672956154000}, "a-shared-secret-of-at-least-thirty-two-bytes", algorithm='HS256', headers={'x5c': ['leaf', 'intermediate', 'root']}) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_transaction(signed_transaction) + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + + def test_malformed_transaction(self): + verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example") + signed_transaction = sign_without_chain({"bundleId": "com.example", "environment": "LocalTesting", "signedDate": "notanumber"}) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_transaction(signed_transaction) + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + + def test_malformed_renewal_info(self): + verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example") + signed_renewal_info = sign_without_chain({"environment": "LocalTesting", "signedDate": "notanumber"}) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_renewal_info(signed_renewal_info) + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + + def test_malformed_notification(self): + verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example") + signed_notification = sign_without_chain({"notificationType": "TEST", "signedDate": "notanumber"}) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_notification(signed_notification) + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + + def test_malformed_app_transaction(self): + verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example") + signed_app_transaction = sign_without_chain({"receiptType": "LocalTesting", "bundleId": "com.example", "receiptCreationDate": "notanumber"}) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_app_transaction(signed_app_transaction) + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + + def test_malformed_realtime_request(self): + verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example") + realtime_request = self._realtime_request(1234, "LocalTesting") + realtime_request["signedDate"] = "notanumber" + signed_realtime_request = sign_without_chain(realtime_request) + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_realtime_request(signed_realtime_request) + self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) + + def _realtime_request(self, app_apple_id: int, environment: str) -> Dict[str, Any]: + return { + "originalTransactionId": "99371282", + "appAppleId": app_apple_id, + "productId": "com.example.product", + "userLocale": "en-US", + "requestIdentifier": "3db5c98d-8acf-4e29-831e-8e1f82f9f6e9", + "environment": environment, + "signedDate": 1698148900000 + } + def test_malformed_jwt_with_too_many_parts(self): verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") with self.assertRaises(VerificationException) as context: diff --git a/tests/test_promotional_offer_signature_creator.py b/tests/test_promotional_offer_signature_creator.py index 3b73310a..174c846a 100644 --- a/tests/test_promotional_offer_signature_creator.py +++ b/tests/test_promotional_offer_signature_creator.py @@ -1,6 +1,10 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +import base64 import unittest +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ec import ECDSA +from cryptography.hazmat.primitives.hashes import SHA256 from appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator from tests.util import read_data_from_binary_file @@ -12,4 +16,7 @@ def test_signature_creator(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') signature_creator = PromotionalOfferSignatureCreator(signing_key, 'keyId', 'bundleId') signature = signature_creator.create_signature("productId", "offerId", "appAccountToken", UUID("20fba8a0-2b80-4a7d-a17f-85c1854727f8"), 1698148900000) - self.assertIsNotNone(signature) \ No newline at end of file + self.assertIsNotNone(signature) + expected_payload = '\u2063'.join(['bundleId', 'keyId', 'productId', 'offerId', 'appaccounttoken', '20fba8a0-2b80-4a7d-a17f-85c1854727f8', '1698148900000']) + public_key = serialization.load_pem_private_key(signing_key, password=None).public_key() + public_key.verify(base64.b64decode(signature), expected_payload.encode('utf-8'), ECDSA(SHA256())) diff --git a/tests/test_receipt_utility.py b/tests/test_receipt_utility.py index 97d3a7d4..2d585383 100644 --- a/tests/test_receipt_utility.py +++ b/tests/test_receipt_utility.py @@ -1,6 +1,10 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +import base64 import unittest + +import asn1 + from appstoreserverlibrary.receipt_utility import ReceiptUtility from tests.util import read_data_from_file @@ -34,4 +38,99 @@ def test_transaction_receipt_extraction(self): extracted_transaction_id = receipt_util.extract_transaction_id_from_transaction_receipt(receipt) - self.assertEqual(TRANSACTION_RECEIPT_EXPECTED_TRANSACTION_ID, extracted_transaction_id) \ No newline at end of file + self.assertEqual(TRANSACTION_RECEIPT_EXPECTED_TRANSACTION_ID, extracted_transaction_id) + + def test_app_receipt_extraction_with_unexpected_content_type(self): + encoder = asn1.Encoder() + encoder.start() + encoder.enter(asn1.Numbers.Sequence) + encoder.write("1.2.840.113549.1.7.1", asn1.Numbers.ObjectIdentifier) + encoder.leave() + receipt = base64.b64encode(encoder.output()).decode('utf-8') + + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_app_receipt(receipt) + + def test_app_receipt_extraction_with_invalid_base64(self): + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_app_receipt("not a receipt") + + def test_app_receipt_extraction_with_valid_base64_that_is_not_asn1(self): + receipt = base64.b64encode(b'not a PKCS#7 container').decode('utf-8') + + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_app_receipt(receipt) + + def test_app_receipt_extraction_with_empty_receipt(self): + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_app_receipt("") + + def test_app_receipt_extraction_with_transaction_receipt(self): + receipt = read_data_from_file("tests/resources/mock_signed_data/legacyTransaction") + + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_app_receipt(receipt) + + def test_transaction_receipt_extraction_without_purchase_info(self): + receipt = base64.b64encode(b'{\n\t"signature" = "c2lnbmF0dXJl";\n}').decode('utf-8') + + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_transaction_receipt(receipt) + + def test_transaction_receipt_extraction_with_invalid_base64(self): + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_transaction_receipt("not a receipt") + + def test_transaction_receipt_extraction_with_empty_receipt(self): + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_transaction_receipt("") + + def test_transaction_receipt_extraction_with_non_utf8_content(self): + receipt = base64.b64encode(b'\xff\xfe\x00"purchase-info"').decode('utf-8') + + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_transaction_receipt(receipt) + + def test_transaction_receipt_extraction_with_malformed_purchase_info(self): + receipt = base64.b64encode(b'{\n\t"purchase-info" = "abc";\n}').decode('utf-8') + + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_transaction_receipt(receipt) + + def test_transaction_receipt_extraction_with_app_receipt(self): + receipt = read_data_from_file("tests/resources/xcode/xcode-app-receipt-with-transaction") + + receipt_util = ReceiptUtility() + + with self.assertRaises(ValueError): + receipt_util.extract_transaction_id_from_transaction_receipt(receipt) + + def test_transaction_receipt_extraction_without_transaction_id(self): + purchase_info = base64.b64encode(b'{\n\t"product-id" = "com.example.product";\n}').decode('utf-8') + receipt = base64.b64encode(('{\n\t"purchase-info" = "' + purchase_info + '";\n}').encode('utf-8')).decode('utf-8') + + receipt_util = ReceiptUtility() + + extracted_transaction_id = receipt_util.extract_transaction_id_from_transaction_receipt(receipt) + + self.assertIsNone(extracted_transaction_id) diff --git a/tests/test_retention_messaging.py b/tests/test_retention_messaging.py index 45b66f67..bfdaac92 100644 --- a/tests/test_retention_messaging.py +++ b/tests/test_retention_messaging.py @@ -1,18 +1,54 @@ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. +import inspect +import json import unittest +import warnings from uuid import UUID +from cattrs.errors import ClassValidationError + from appstoreserverlibrary.models.AlternateProduct import AlternateProduct from appstoreserverlibrary.models.AdvancedCommerceInfo import AdvancedCommerceInfo from appstoreserverlibrary.models.BillingPlanType import BillingPlanType +from appstoreserverlibrary.models.BulletPoint import BulletPoint from appstoreserverlibrary.models.DecodedRealtimeRequestBody import DecodedRealtimeRequestBody +from appstoreserverlibrary.models.DefaultConfigurationRequest import DefaultConfigurationRequest +from appstoreserverlibrary.models.DefaultConfigurationResponse import DefaultConfigurationResponse from appstoreserverlibrary.models.Environment import Environment from appstoreserverlibrary.models.Message import Message +from appstoreserverlibrary.models.PerformanceTestConfig import PerformanceTestConfig +from appstoreserverlibrary.models.PerformanceTestResponse import PerformanceTestResponse +from appstoreserverlibrary.models.PerformanceTestResponseTimes import PerformanceTestResponseTimes +from appstoreserverlibrary.models.PerformanceTestResultResponse import PerformanceTestResultResponse +from appstoreserverlibrary.models.PerformanceTestStatus import PerformanceTestStatus from appstoreserverlibrary.models.PromotionalOffer import PromotionalOffer from appstoreserverlibrary.models.PromotionalOfferSignatureV1 import PromotionalOfferSignatureV1 from appstoreserverlibrary.models.RealtimeResponseBody import RealtimeResponseBody +from appstoreserverlibrary.models.RealtimeUrlRequest import RealtimeUrlRequest +from appstoreserverlibrary.models.RealtimeUrlResponse import RealtimeUrlResponse +from appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter +from tests.util import read_data_from_file + + +def performance_test_result_json(**kwargs) -> dict: + values = json.loads(read_data_from_file('tests/resources/models/performanceTestResultResponse.json')) + values.update(kwargs) + return values + + +def create_performance_test_result_response(**kwargs) -> PerformanceTestResultResponse: + values = dict( + config=PerformanceTestConfig(maxConcurrentRequests=10, totalRequests=100, totalDuration=60000, responseTimeThreshold=500, successRateThreshold=95), + target='https://example.com/retention', + result=PerformanceTestStatus.PASS, + successRate=98, + numPending=0, + responseTimes=PerformanceTestResponseTimes(average=120, p50=100, p90=200, p95=250, p99=400), + ) + values.update(kwargs) + return PerformanceTestResultResponse(**values) class RetentionMessaging(unittest.TestCase): @@ -199,3 +235,231 @@ def test_realtime_response_body_with_advanced_commerce_info(self): self.assertIsNotNone(deserialized.advancedCommerceInfo) self.assertEqual(message_id, deserialized.advancedCommerceInfo.messageIdentifier) self.assertEqual(ac_data, deserialized.advancedCommerceInfo.advancedCommerceData) + + def test_performance_test_result_response_with_empty_failures(self): + c = _get_cattrs_converter(PerformanceTestResultResponse) + deserialized = c.structure(performance_test_result_json(failures={}), PerformanceTestResultResponse) + + self.assertEqual({}, deserialized.failures) + self.assertEqual({}, deserialized.rawFailures) + self.assertEqual({}, c.unstructure(deserialized)['failures']) + + def test_performance_test_result_response_with_only_unknown_failures(self): + c = _get_cattrs_converter(PerformanceTestResultResponse) + deserialized = c.structure(performance_test_result_json(failures={'NEW_REASON': 2}), PerformanceTestResultResponse) + + self.assertEqual({}, deserialized.failures) + self.assertEqual({'NEW_REASON': 2}, deserialized.rawFailures) + self.assertEqual({'NEW_REASON': 2}, c.unstructure(deserialized)['failures']) + + def test_performance_test_result_response_with_known_and_unknown_failures(self): + c = _get_cattrs_converter(PerformanceTestResultResponse) + deserialized = c.structure(performance_test_result_json(failures={'TIMED_OUT': 1, 'NEW_REASON': 2}), PerformanceTestResultResponse) + + self.assertEqual({SendAttemptResult.TIMED_OUT: 1}, deserialized.failures) + self.assertEqual({'TIMED_OUT': 1, 'NEW_REASON': 2}, deserialized.rawFailures) + + def test_performance_test_result_response_round_trip(self): + c = _get_cattrs_converter(PerformanceTestResultResponse) + response_json = performance_test_result_json() + + deserialized = c.structure(response_json, PerformanceTestResultResponse) + + self.assertEqual(PerformanceTestStatus.PASS, deserialized.result) + self.assertEqual('PASS', deserialized.rawResult) + self.assertEqual(response_json, c.unstructure(deserialized)) + + def test_performance_test_result_response_with_unknown_result(self): + c = _get_cattrs_converter(PerformanceTestResultResponse) + deserialized = c.structure(performance_test_result_json(result='NEW_RESULT'), PerformanceTestResultResponse) + + self.assertIsNone(deserialized.result) + self.assertEqual('NEW_RESULT', deserialized.rawResult) + + def test_performance_test_result_response_without_required_field_fails_decoding(self): + c = _get_cattrs_converter(PerformanceTestResultResponse) + for field in ['config', 'target', 'successRate', 'numPending', 'responseTimes', 'result', 'failures']: + response_json = performance_test_result_json() + del response_json[field] + with self.subTest(field=field), self.assertRaises(ClassValidationError): + c.structure(response_json, PerformanceTestResultResponse) + + def test_performance_test_result_response_with_null_required_field_fails_decoding(self): + c = _get_cattrs_converter(PerformanceTestResultResponse) + for field in ['config', 'target', 'successRate', 'numPending', 'responseTimes', 'result', 'failures']: + with self.subTest(field=field), self.assertRaises(ClassValidationError): + c.structure(performance_test_result_json(**{field: None}), PerformanceTestResultResponse) + + def test_performance_test_result_response_without_nested_required_field_fails_decoding(self): + c = _get_cattrs_converter(PerformanceTestResultResponse) + response_json = performance_test_result_json() + del response_json['config']['totalRequests'] + with self.assertRaises(ClassValidationError): + c.structure(response_json, PerformanceTestResultResponse) + + response_json = performance_test_result_json() + del response_json['responseTimes']['p99'] + with self.assertRaises(ClassValidationError): + c.structure(response_json, PerformanceTestResultResponse) + + def test_performance_test_result_response_requires_documented_fields(self): + with self.assertRaises(TypeError): + PerformanceTestResultResponse(target='https://example.com/retention', result=PerformanceTestStatus.PASS, successRate=98, numPending=0, + responseTimes=PerformanceTestResponseTimes(average=120, p50=100, p90=200, p95=250, p99=400), failures={}) + with self.assertRaises(ValueError) as context: + create_performance_test_result_response(result=None, failures={}) + self.assertEqual('Either result or rawResult must be provided', str(context.exception)) + with self.assertRaises(ValueError) as context: + create_performance_test_result_response() + self.assertEqual('Either failures or rawFailures must be provided', str(context.exception)) + + def test_performance_test_result_response_takes_keyword_arguments_only(self): + parameters = inspect.signature(PerformanceTestResultResponse).parameters + self.assertEqual([], [name for name, p in parameters.items() if p.kind != inspect.Parameter.KEYWORD_ONLY]) + + values = dict( + config=PerformanceTestConfig(maxConcurrentRequests=10, totalRequests=100, totalDuration=60000, responseTimeThreshold=500, successRateThreshold=95), + target='https://example.com/retention', + result=PerformanceTestStatus.PASS, + successRate=98, + numPending=0, + responseTimes=PerformanceTestResponseTimes(average=120, p50=100, p90=200, p95=250, p99=400), + failures={SendAttemptResult.TIMED_OUT: 2}, + ) + with self.assertRaisesRegex(TypeError, 'positional argument'): + PerformanceTestResultResponse(*values.values()) + with self.assertRaisesRegex(TypeError, 'positional argument'): + PerformanceTestResultResponse(*[v for k, v in values.items() if k != 'result'], result=values['result']) + with self.assertRaisesRegex(TypeError, 'positional argument'): + PerformanceTestResultResponse(values['config'], **{k: v for k, v in values.items() if k != 'config'}) + + response = PerformanceTestResultResponse(**values) + + for name, value in values.items(): + self.assertEqual(value, getattr(response, name)) + self.assertEqual('PASS', response.rawResult) + self.assertEqual({'TIMED_OUT': 2}, response.rawFailures) + response = create_performance_test_result_response(result=None, rawResult='NEW_STATUS', rawFailures={'NEW_REASON': 1}) + self.assertEqual('NEW_STATUS', response.rawResult) + self.assertEqual({}, response.failures) + + def test_performance_test_result_response_failures_assignment(self): + response = create_performance_test_result_response(failures={SendAttemptResult.TIMED_OUT: 2}) + self.assertEqual({'TIMED_OUT': 2}, response.rawFailures) + + response.failures = {} + self.assertEqual({}, response.rawFailures) + + response.rawFailures = {'NO_RESPONSE': 3, 'NEW_REASON': 4} + self.assertEqual({SendAttemptResult.NO_RESPONSE: 3}, response.failures) + + with self.assertRaises(ValueError): + response.rawFailures = None + with self.assertRaises(ValueError): + response.failures = None + self.assertEqual({SendAttemptResult.NO_RESPONSE: 3}, response.failures) + self.assertEqual({'NO_RESPONSE': 3, 'NEW_REASON': 4}, response.rawFailures) + + def test_performance_test_result_response_accepts_raw_failures(self): + response = create_performance_test_result_response(rawFailures={'TIMED_OUT': 1, 'NEW_REASON': 2}) + + self.assertEqual({SendAttemptResult.TIMED_OUT: 1}, response.failures) + self.assertEqual({'TIMED_OUT': 1, 'NEW_REASON': 2}, response.rawFailures) + + def test_performance_test_response_request_id_is_uuid(self): + c = _get_cattrs_converter(PerformanceTestResponse) + response_json = json.loads(read_data_from_file('tests/resources/models/performanceTestResponse.json')) + response_json['requestId'] = 'C4B87A1D-2E3F-4A5B-9C6D-7E8F9A0B1C2D' + + response = c.structure(response_json, PerformanceTestResponse) + + self.assertEqual(UUID('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d'), response.requestId) + self.assertEqual('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', c.unstructure(response)['requestId']) + + def test_performance_test_response_without_required_field_fails_decoding(self): + c = _get_cattrs_converter(PerformanceTestResponse) + for field in ['config', 'requestId']: + response_json = json.loads(read_data_from_file('tests/resources/models/performanceTestResponse.json')) + del response_json[field] + with self.subTest(field=field), self.assertRaises(ClassValidationError): + c.structure(response_json, PerformanceTestResponse) + + def test_performance_test_response_with_null_required_field_fails_decoding(self): + c = _get_cattrs_converter(PerformanceTestResponse) + for field in ['config', 'requestId']: + response_json = json.loads(read_data_from_file('tests/resources/models/performanceTestResponse.json')) + response_json[field] = None + with self.subTest(field=field), self.assertRaises(ClassValidationError): + c.structure(response_json, PerformanceTestResponse) + + def test_realtime_url_response(self): + c = _get_cattrs_converter(RealtimeUrlResponse) + deserialized = c.structure({'realtimeURL': 'https://example.com/realtime'}, RealtimeUrlResponse) + self.assertEqual('https://example.com/realtime', deserialized.realtimeURL) + + def test_realtime_url_response_without_realtime_url_fails(self): + c = _get_cattrs_converter(RealtimeUrlResponse) + with self.assertRaises(ClassValidationError): + c.structure({}, RealtimeUrlResponse) + with self.assertRaises(TypeError): + RealtimeUrlResponse() + + def test_realtime_url_response_with_null_realtime_url_fails(self): + c = _get_cattrs_converter(RealtimeUrlResponse) + with self.assertRaises(ClassValidationError): + c.structure({'realtimeURL': None}, RealtimeUrlResponse) + with self.assertRaises(ValueError): + RealtimeUrlResponse(realtimeURL=None) + + def test_default_configuration_response_requires_message_identifier(self): + c = _get_cattrs_converter(DefaultConfigurationResponse) + response = c.structure({'messageIdentifier': 'A1B2C3D4-E5F6-7890-A1B2-C3D4E5F67890'}, DefaultConfigurationResponse) + self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifier) + with self.assertRaises(ClassValidationError): + c.structure({'messageIdentifier': None}, DefaultConfigurationResponse) + with self.assertRaises(ClassValidationError): + c.structure({}, DefaultConfigurationResponse) + + def test_retention_messaging_required_fields_reject_none_when_built(self): + with self.assertRaises(ValueError) as context: + AlternateProduct(messageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), productId=None) + self.assertEqual('productId cannot be None', str(context.exception)) + with self.assertRaises(ValueError) as context: + DefaultConfigurationRequest(messageIdentifier=None) + self.assertEqual('messageIdentifier cannot be None', str(context.exception)) + with self.assertRaises(ValueError) as context: + RealtimeUrlRequest(realtimeURL=None) + self.assertEqual('realtimeURL cannot be None', str(context.exception)) + with self.assertRaises(ValueError) as context: + BulletPoint(text=None, imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt') + self.assertEqual('text cannot be None', str(context.exception)) + with self.assertRaises(ValueError): + BulletPoint(text='a' * 67, imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt') + + bullet_point = BulletPoint(text='Bullet 1', imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt') + with self.assertRaises(ValueError): + bullet_point.imageIdentifier = None + self.assertEqual(UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), bullet_point.imageIdentifier) + + def test_default_configuration_request_with_message_identifier(self): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + request = DefaultConfigurationRequest(messageIdentifier=UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890')) + self.assertEqual([], [w for w in caught if issubclass(w.category, DeprecationWarning)]) + + c = _get_cattrs_converter(DefaultConfigurationRequest) + self.assertEqual({'messageIdentifier': 'a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'}, c.unstructure(request)) + + def test_default_configuration_request_requires_message_identifier(self): + with self.assertRaises(TypeError): + DefaultConfigurationRequest() + + def test_alternate_product_requires_documented_fields(self): + with self.assertRaises(TypeError): + AlternateProduct(productId='com.example.alternate.product') + with self.assertRaises(TypeError): + AlternateProduct(messageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901')) + + alternate_product = AlternateProduct(messageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), productId='com.example.alternate.product') + self.assertEqual({'messageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'productId': 'com.example.alternate.product'}, + _get_cattrs_converter(AlternateProduct).unstructure(alternate_product)) diff --git a/tests/test_x509_verifiction.py b/tests/test_x509_verification.py similarity index 60% rename from tests/test_x509_verifiction.py rename to tests/test_x509_verification.py index 048d58c7..ed2eaf82 100644 --- a/tests/test_x509_verifiction.py +++ b/tests/test_x509_verification.py @@ -1,9 +1,19 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. +import datetime +import time import unittest +from typing import List, Optional from unittest import mock from unittest.mock import MagicMock, patch +import requests +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.x509 import ocsp +from cryptography.x509.oid import AuthorityInformationAccessOID, ExtendedKeyUsageOID, NameOID + from appstoreserverlibrary.signed_data_verifier import _ChainVerifier, VerificationException, VerificationStatus from base64 import b64decode, b64encode @@ -24,6 +34,78 @@ EFFECTIVE_DATE = 1761962975 CLOCK_DATE = 41231 +def _create_certificate(subject: str, issuer: x509.Name, public_key, signing_key, is_ca: bool, extensions: List[x509.ExtensionType]) -> x509.Certificate: + now = datetime.datetime.now(datetime.timezone.utc) + builder = x509.CertificateBuilder() \ + .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, subject)])) \ + .issuer_name(issuer) \ + .public_key(public_key) \ + .serial_number(x509.random_serial_number()) \ + .not_valid_before(now - datetime.timedelta(days=1)) \ + .not_valid_after(now + datetime.timedelta(days=365)) \ + .add_extension(x509.BasicConstraints(ca=is_ca, path_length=None), critical=True) + for extension in extensions: + builder = builder.add_extension(extension, critical=False) + return builder.sign(signing_key, hashes.SHA256()) + +def _ocsp_access(url: str) -> x509.AuthorityInformationAccess: + return x509.AuthorityInformationAccess([x509.AccessDescription(AuthorityInformationAccessOID.OCSP, x509.UniformResourceIdentifier(url))]) + +class _GeneratedChain: + def __init__(self): + self.root_key = ec.generate_private_key(ec.SECP256R1()) + self.intermediate_key = ec.generate_private_key(ec.SECP256R1()) + self.leaf_key = ec.generate_private_key(ec.SECP256R1()) + root_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Root")]) + self.root = _create_certificate("Root", root_name, self.root_key.public_key(), self.root_key, True, []) + self.intermediate = _create_certificate("Intermediate", root_name, self.intermediate_key.public_key(), self.root_key, True, [ + x509.UnrecognizedExtension(x509.ObjectIdentifier("1.2.840.113635.100.6.2.1"), b"\x05\x00"), + _ocsp_access("http://ocsp.example.com/root"), + ]) + self.leaf = _create_certificate("Leaf", self.intermediate.subject, self.leaf_key.public_key(), self.intermediate_key, False, [ + x509.UnrecognizedExtension(x509.ObjectIdentifier("1.2.840.113635.100.6.11.1"), b"\x05\x00"), + _ocsp_access("http://ocsp.example.com/intermediate"), + ]) + + def root_bytes(self) -> bytes: + return self.root.public_bytes(serialization.Encoding.DER) + + def chain(self) -> List[str]: + return [b64encode(cert.public_bytes(serialization.Encoding.DER)).decode() for cert in [self.leaf, self.intermediate, self.root]] + + def leaf_public_key(self) -> str: + return self.leaf_key.public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode() + + def ocsp_responder(self, responder_encoding: ocsp.OCSPResponderEncoding = ocsp.OCSPResponderEncoding.HASH, delegated_signer_usages: Optional[List[x509.ObjectIdentifier]] = None, cert_status: ocsp.OCSPCertStatus = ocsp.OCSPCertStatus.GOOD): + def post(url, headers, data, timeout): + request = ocsp.load_der_ocsp_request(data) + if request.serial_number == self.leaf.serial_number: + cert, issuer, issuer_key = self.leaf, self.intermediate, self.intermediate_key + else: + cert, issuer, issuer_key = self.intermediate, self.root, self.root_key + now = datetime.datetime.now(datetime.timezone.utc) + builder = ocsp.OCSPResponseBuilder().add_response( + cert=cert, + issuer=issuer, + algorithm=hashes.SHA256(), + cert_status=cert_status, + this_update=now - datetime.timedelta(minutes=1), + next_update=now + datetime.timedelta(hours=1), + revocation_time=now - datetime.timedelta(days=1) if cert_status == ocsp.OCSPCertStatus.REVOKED else None, + revocation_reason=None + ) + signing_cert, signing_key = issuer, issuer_key + if delegated_signer_usages is not None: + signing_key = ec.generate_private_key(ec.SECP256R1()) + signing_cert = _create_certificate("Delegated OCSP Signer", issuer.subject, signing_key.public_key(), issuer_key, False, [x509.ExtendedKeyUsage(delegated_signer_usages)]) + builder = builder.certificates([signing_cert]) + builder = builder.responder_id(responder_encoding, signing_cert) + response = MagicMock() + response.status_code = 200 + response.content = builder.sign(signing_key, hashes.SHA256()).public_bytes(serialization.Encoding.DER) + return response + return post + class X509Verification(unittest.TestCase): def test_valid_chain_without_ocsp(self): verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False) @@ -194,6 +276,96 @@ def test_ocsp_response_caching_with_slightly_different_chain(self): ], True, EFFECTIVE_DATE) self.assertEqual(2, magic_mock.call_count) + def test_ocsp_response_caching_evicts_expired_entries(self): + verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)]) + with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): + for i in range(_ChainVerifier.MAXIMUM_CACHE_SIZE): + verifier.put_verified_public_key(["leaf" + str(i), "intermediate", "root"], "key" + str(i)) + self.assertEqual(_ChainVerifier.MAXIMUM_CACHE_SIZE, len(verifier.verified_certificates_cache)) + with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE + 1)): + verifier.put_verified_public_key(["leaf", "intermediate", "root"], "key") + self.assertEqual(_ChainVerifier.MAXIMUM_CACHE_SIZE + 1, len(verifier.verified_certificates_cache)) + with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE + 900)): + verifier.put_verified_public_key(["newLeaf", "intermediate", "root"], "newKey") + self.assertEqual(2, len(verifier.verified_certificates_cache)) + self.assertEqual("key", verifier.get_cached_public_key(["leaf", "intermediate", "root"])) + self.assertEqual("newKey", verifier.get_cached_public_key(["newLeaf", "intermediate", "root"])) + self.assertIsNone(verifier.get_cached_public_key(["leaf0", "intermediate", "root"])) + + def test_empty_root_certificates(self): + verifier = _ChainVerifier([], False) + with self.assertRaises(VerificationException) as context: + verifier.verify_chain([ + LEAF_CERT_BASE64_ENCODED, + INTERMEDIATE_CA_BASE64_ENCODED, + ROOT_CA_BASE64_ENCODED + ], False, EFFECTIVE_DATE) + self.assertEqual(VerificationStatus.INVALID_CERTIFICATE, context.exception.status) + + def test_ocsp_responder_identified_by_key_hash(self): + chain = _GeneratedChain() + verifier = _ChainVerifier([chain.root_bytes()], False) + with patch('requests.post', side_effect=chain.ocsp_responder(responder_encoding=ocsp.OCSPResponderEncoding.HASH)) as post: + public_key = verifier.verify_chain(chain.chain(), True, time.time()) + self.assertEqual(chain.leaf_public_key(), public_key) + self.assertEqual(2, post.call_count) + + def test_ocsp_responder_identified_by_name(self): + chain = _GeneratedChain() + verifier = _ChainVerifier([chain.root_bytes()], False) + with patch('requests.post', side_effect=chain.ocsp_responder(responder_encoding=ocsp.OCSPResponderEncoding.NAME)) as post: + public_key = verifier.verify_chain(chain.chain(), True, time.time()) + self.assertEqual(chain.leaf_public_key(), public_key) + self.assertEqual(2, post.call_count) + + def test_ocsp_delegated_signer_identified_by_key_hash(self): + chain = _GeneratedChain() + verifier = _ChainVerifier([chain.root_bytes()], False) + with patch('requests.post', side_effect=chain.ocsp_responder(responder_encoding=ocsp.OCSPResponderEncoding.HASH, delegated_signer_usages=[ExtendedKeyUsageOID.OCSP_SIGNING])): + public_key = verifier.verify_chain(chain.chain(), True, time.time()) + self.assertEqual(chain.leaf_public_key(), public_key) + + def test_ocsp_delegated_signer_identified_by_name(self): + chain = _GeneratedChain() + verifier = _ChainVerifier([chain.root_bytes()], False) + with patch('requests.post', side_effect=chain.ocsp_responder(responder_encoding=ocsp.OCSPResponderEncoding.NAME, delegated_signer_usages=[ExtendedKeyUsageOID.OCSP_SIGNING])): + public_key = verifier.verify_chain(chain.chain(), True, time.time()) + self.assertEqual(chain.leaf_public_key(), public_key) + + def test_ocsp_delegated_signer_without_ocsp_signing_usage(self): + chain = _GeneratedChain() + verifier = _ChainVerifier([chain.root_bytes()], False) + with patch('requests.post', side_effect=chain.ocsp_responder(delegated_signer_usages=[ExtendedKeyUsageOID.SERVER_AUTH])): + with self.assertRaises(VerificationException) as context: + verifier.verify_chain(chain.chain(), True, time.time()) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_ocsp_revoked_certificate(self): + chain = _GeneratedChain() + verifier = _ChainVerifier([chain.root_bytes()], False) + with patch('requests.post', side_effect=chain.ocsp_responder(cert_status=ocsp.OCSPCertStatus.REVOKED)): + with self.assertRaises(VerificationException) as context: + verifier.verify_chain(chain.chain(), True, time.time()) + self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) + + def test_ocsp_network_failure_is_retryable(self): + chain = _GeneratedChain() + verifier = _ChainVerifier([chain.root_bytes()], False) + with patch('requests.post', side_effect=requests.exceptions.ConnectionError()): + with self.assertRaises(VerificationException) as context: + verifier.verify_chain(chain.chain(), True, time.time()) + self.assertEqual(VerificationStatus.RETRYABLE_VERIFICATION_FAILURE, context.exception.status) + + def test_ocsp_unsuccessful_http_status_is_retryable(self): + chain = _GeneratedChain() + verifier = _ChainVerifier([chain.root_bytes()], False) + response = MagicMock() + response.status_code = 503 + with patch('requests.post', return_value=response): + with self.assertRaises(VerificationException) as context: + verifier.verify_chain(chain.chain(), True, time.time()) + self.assertEqual(VerificationStatus.RETRYABLE_VERIFICATION_FAILURE, context.exception.status) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_xcode_signed_data.py b/tests/test_xcode_signed_data.py index c11d2b36..c1ccf9aa 100644 --- a/tests/test_xcode_signed_data.py +++ b/tests/test_xcode_signed_data.py @@ -1,20 +1,20 @@ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. import unittest +from uuid import UUID from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus from appstoreserverlibrary.models.Environment import Environment from appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType from appstoreserverlibrary.models.OfferType import OfferType from appstoreserverlibrary.models.TransactionReason import TransactionReason from appstoreserverlibrary.models.Type import Type -from appstoreserverlibrary.receipt_utility import ReceiptUtility -from appstoreserverlibrary.signed_data_verifier import VerificationException +from appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus from tests.util import get_signed_data_verifier, read_data_from_file XCODE_BUNDLE_ID = "com.example.naturelab.backyardbirds.example" -class ReceiptUtilityTest(unittest.TestCase): +class XcodeSignedDataTest(unittest.TestCase): def test_xcode_signed_app_transaction(self): verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID) encoded_app_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-app-transaction") @@ -29,7 +29,7 @@ def test_xcode_signed_app_transaction(self): self.assertEqual(-62135769600000, app_transaction.originalPurchaseDate) self.assertEqual("1", app_transaction.originalApplicationVersion) self.assertEqual("cYUsXc53EbYc0pOeXG5d6/31LGHeVGf84sqSN0OrJi5u/j2H89WWKgS8N0hMsMlf", app_transaction.deviceVerification) - self.assertEqual("48c8b92d-ce0d-4229-bedf-e61b4f9cfc92", app_transaction.deviceVerificationNonce) + self.assertEqual(UUID("48c8b92d-ce0d-4229-bedf-e61b4f9cfc92"), app_transaction.deviceVerificationNonce) self.assertIsNone(app_transaction.preorderDate) self.assertEqual(Environment.XCODE, app_transaction.receiptType) self.assertEqual("Xcode", app_transaction.rawReceiptType) @@ -38,7 +38,7 @@ def test_xcode_signed_transaction(self): verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID) encoded_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-transaction") - transaction = verifier.verify_and_decode_signed_transaction(encoded_transaction) + transaction = verifier.verify_and_decode_transaction(encoded_transaction) self.assertEqual("0", transaction.originalTransactionId) self.assertEqual("0", transaction.transactionId) @@ -95,9 +95,35 @@ def test_xcode_signed_renewal_info(self): def test_xcode_signed_app_transaction_with_production_environment(self): verifier = get_signed_data_verifier(Environment.PRODUCTION, XCODE_BUNDLE_ID) encoded_app_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-app-transaction") - try: + with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_app_transaction(encoded_app_transaction) - except VerificationException: - return - self.assertFalse(True) + self.assertEqual(VerificationStatus.INVALID_CHAIN_LENGTH, context.exception.status) + + def test_xcode_signed_app_transaction_with_local_testing_environment(self): + verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, XCODE_BUNDLE_ID) + encoded_app_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-app-transaction") + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_app_transaction(encoded_app_transaction) + self.assertEqual(VerificationStatus.INVALID_ENVIRONMENT, context.exception.status) + + def test_xcode_signed_app_transaction_with_wrong_bundle_id(self): + verifier = get_signed_data_verifier(Environment.XCODE, "com.example") + encoded_app_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-app-transaction") + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_app_transaction(encoded_app_transaction) + self.assertEqual(VerificationStatus.INVALID_APP_IDENTIFIER, context.exception.status) + + def test_xcode_signed_transaction_with_local_testing_environment(self): + verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, XCODE_BUNDLE_ID) + encoded_transaction = read_data_from_file("tests/resources/xcode/xcode-signed-transaction") + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_transaction(encoded_transaction) + self.assertEqual(VerificationStatus.INVALID_ENVIRONMENT, context.exception.status) + + def test_xcode_signed_renewal_info_with_local_testing_environment(self): + verifier = get_signed_data_verifier(Environment.LOCAL_TESTING, XCODE_BUNDLE_ID) + encoded_renewal_info = read_data_from_file("tests/resources/xcode/xcode-signed-renewal-info") + with self.assertRaises(VerificationException) as context: + verifier.verify_and_decode_renewal_info(encoded_renewal_info) + self.assertEqual(VerificationStatus.INVALID_ENVIRONMENT, context.exception.status) diff --git a/tests/util.py b/tests/util.py index f1d8bd61..d5834e99 100644 --- a/tests/util.py +++ b/tests/util.py @@ -13,10 +13,11 @@ from appstoreserverlibrary.signed_data_verifier import SignedDataVerifier def create_signed_data_from_json(path: str) -> str: - data = read_data_from_file(path) - decoded_data = json.loads(data) + return create_signed_data_from_dict(json.loads(read_data_from_file(path))) + +def create_signed_data_from_dict(payload: Dict[str, Any]) -> str: private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode() - return jwt.encode(payload=decoded_data, key=private_key, algorithm='ES256') + return jwt.encode(payload=payload, key=private_key, algorithm='ES256') def decode_json_from_signed_date(data: str) -> Dict[str, Any]: public_key = ec.generate_private_key(ec.SECP256R1()).public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode()