Repository: apple/app-store-server-library-python Branch: main Commit: ce1c9e0ced6e Files: 265 Total size: 589.5 KB Directory structure: gitextract_u4cbit1j/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci-prb.yml │ ├── ci-release-docs.yml │ ├── ci-release.yml │ └── ci-snapshot.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── NOTICE.txt ├── README.md ├── appstoreserverlibrary/ │ ├── __init__.py │ ├── api_client.py │ ├── jws_signature_creator.py │ ├── models/ │ │ ├── AbstractAdvancedCommerceBaseItem.py │ │ ├── AbstractAdvancedCommerceInAppRequest.py │ │ ├── AbstractAdvancedCommerceItem.py │ │ ├── AbstractAdvancedCommerceResponse.py │ │ ├── AccountTenure.py │ │ ├── AdvancedCommerceDescriptors.py │ │ ├── AdvancedCommerceEffective.py │ │ ├── AdvancedCommerceOffer.py │ │ ├── AdvancedCommerceOfferPeriod.py │ │ ├── AdvancedCommerceOfferReason.py │ │ ├── AdvancedCommerceOneTimeChargeCreateRequest.py │ │ ├── AdvancedCommerceOneTimeChargeItem.py │ │ ├── AdvancedCommercePeriod.py │ │ ├── AdvancedCommerceReason.py │ │ ├── AdvancedCommerceRefundReason.py │ │ ├── AdvancedCommerceRefundType.py │ │ ├── AdvancedCommerceRequest.py │ │ ├── AdvancedCommerceRequestInfo.py │ │ ├── AdvancedCommerceRequestRefundItem.py │ │ ├── AdvancedCommerceRequestRefundRequest.py │ │ ├── AdvancedCommerceRequestRefundResponse.py │ │ ├── AdvancedCommerceSubscriptionCancelRequest.py │ │ ├── AdvancedCommerceSubscriptionCancelResponse.py │ │ ├── AdvancedCommerceSubscriptionChangeMetadataDescriptors.py │ │ ├── AdvancedCommerceSubscriptionChangeMetadataItem.py │ │ ├── AdvancedCommerceSubscriptionChangeMetadataRequest.py │ │ ├── AdvancedCommerceSubscriptionChangeMetadataResponse.py │ │ ├── AdvancedCommerceSubscriptionCreateItem.py │ │ ├── AdvancedCommerceSubscriptionCreateRequest.py │ │ ├── AdvancedCommerceSubscriptionMigrateDescriptors.py │ │ ├── AdvancedCommerceSubscriptionMigrateItem.py │ │ ├── AdvancedCommerceSubscriptionMigrateRenewalItem.py │ │ ├── AdvancedCommerceSubscriptionMigrateRequest.py │ │ ├── AdvancedCommerceSubscriptionMigrateResponse.py │ │ ├── AdvancedCommerceSubscriptionModifyAddItem.py │ │ ├── AdvancedCommerceSubscriptionModifyChangeItem.py │ │ ├── AdvancedCommerceSubscriptionModifyDescriptors.py │ │ ├── AdvancedCommerceSubscriptionModifyInAppRequest.py │ │ ├── AdvancedCommerceSubscriptionModifyPeriodChange.py │ │ ├── AdvancedCommerceSubscriptionModifyRemoveItem.py │ │ ├── AdvancedCommerceSubscriptionPriceChangeItem.py │ │ ├── AdvancedCommerceSubscriptionPriceChangeRequest.py │ │ ├── AdvancedCommerceSubscriptionPriceChangeResponse.py │ │ ├── AdvancedCommerceSubscriptionReactivateInAppRequest.py │ │ ├── AdvancedCommerceSubscriptionReactivateItem.py │ │ ├── AdvancedCommerceSubscriptionRevokeRequest.py │ │ ├── AdvancedCommerceSubscriptionRevokeResponse.py │ │ ├── AdvancedCommerceValidationUtils.py │ │ ├── AlternateProduct.py │ │ ├── AppData.py │ │ ├── AppTransaction.py │ │ ├── AppTransactionInfoResponse.py │ │ ├── AutoRenewStatus.py │ │ ├── BulletPoint.py │ │ ├── CheckTestNotificationResponse.py │ │ ├── ConsumptionRequest.py │ │ ├── ConsumptionRequestReason.py │ │ ├── ConsumptionRequestV1.py │ │ ├── ConsumptionStatus.py │ │ ├── Data.py │ │ ├── DecodedRealtimeRequestBody.py │ │ ├── DefaultConfigurationRequest.py │ │ ├── DefaultConfigurationResponse.py │ │ ├── DeliveryStatus.py │ │ ├── DeliveryStatusV1.py │ │ ├── Environment.py │ │ ├── ExpirationIntent.py │ │ ├── ExtendReasonCode.py │ │ ├── ExtendRenewalDateRequest.py │ │ ├── ExtendRenewalDateResponse.py │ │ ├── ExternalPurchaseToken.py │ │ ├── FirstSendAttemptResult.py │ │ ├── GetImageListResponse.py │ │ ├── GetImageListResponseItem.py │ │ ├── GetMessageListResponse.py │ │ ├── GetMessageListResponseItem.py │ │ ├── HeaderPosition.py │ │ ├── HistoryResponse.py │ │ ├── ImageSize.py │ │ ├── ImageState.py │ │ ├── InAppOwnershipType.py │ │ ├── JWSRenewalInfoDecodedPayload.py │ │ ├── JWSTransactionDecodedPayload.py │ │ ├── LastTransactionsItem.py │ │ ├── LibraryUtility.py │ │ ├── LifetimeDollarsPurchased.py │ │ ├── LifetimeDollarsRefunded.py │ │ ├── MassExtendRenewalDateRequest.py │ │ ├── MassExtendRenewalDateResponse.py │ │ ├── MassExtendRenewalDateStatusResponse.py │ │ ├── Message.py │ │ ├── MessageState.py │ │ ├── NotificationHistoryRequest.py │ │ ├── NotificationHistoryResponse.py │ │ ├── NotificationHistoryResponseItem.py │ │ ├── NotificationTypeV2.py │ │ ├── OfferDiscountType.py │ │ ├── OfferType.py │ │ ├── OrderLookupResponse.py │ │ ├── OrderLookupStatus.py │ │ ├── PerformanceTestConfig.py │ │ ├── PerformanceTestRequest.py │ │ ├── PerformanceTestResponse.py │ │ ├── PerformanceTestResponseTimes.py │ │ ├── PerformanceTestResultResponse.py │ │ ├── PerformanceTestStatus.py │ │ ├── Platform.py │ │ ├── PlayTime.py │ │ ├── PriceIncreaseStatus.py │ │ ├── PromotionalOffer.py │ │ ├── PromotionalOfferSignatureV1.py │ │ ├── PurchasePlatform.py │ │ ├── RealtimeRequestBody.py │ │ ├── RealtimeResponseBody.py │ │ ├── RealtimeUrlRequest.py │ │ ├── RealtimeUrlResponse.py │ │ ├── RefundHistoryResponse.py │ │ ├── RefundPreference.py │ │ ├── RefundPreferenceV1.py │ │ ├── ResponseBodyV2.py │ │ ├── ResponseBodyV2DecodedPayload.py │ │ ├── RevocationReason.py │ │ ├── RevocationType.py │ │ ├── SendAttemptItem.py │ │ ├── SendAttemptResult.py │ │ ├── SendTestNotificationResponse.py │ │ ├── Status.py │ │ ├── StatusResponse.py │ │ ├── SubscriptionGroupIdentifierItem.py │ │ ├── Subtype.py │ │ ├── Summary.py │ │ ├── TransactionHistoryRequest.py │ │ ├── TransactionInfoResponse.py │ │ ├── TransactionReason.py │ │ ├── Type.py │ │ ├── UpdateAppAccountTokenRequest.py │ │ ├── UploadMessageImage.py │ │ ├── UploadMessageRequestBody.py │ │ ├── UserStatus.py │ │ └── __init__.py │ ├── promotional_offer.py │ ├── py.typed │ ├── receipt_utility.py │ └── signed_data_verifier.py ├── docs/ │ └── requirements.txt ├── pyproject.toml ├── requirements.txt └── tests/ ├── __init__.py ├── resources/ │ ├── certs/ │ │ ├── testCA.der │ │ └── testSigningKey.p8 │ ├── mock_signed_data/ │ │ ├── legacyTransaction │ │ ├── missingX5CHeaderClaim │ │ ├── renewalInfo │ │ ├── testNotification │ │ ├── transactionInfo │ │ └── wrongBundleId │ ├── models/ │ │ ├── advancedCommerceDescriptors.json │ │ ├── advancedCommerceOffer.json │ │ ├── advancedCommerceOneTimeChargeCreateRequest.json │ │ ├── advancedCommerceOneTimeChargeItem.json │ │ ├── advancedCommerceRequestInfo.json │ │ ├── advancedCommerceRequestRefundItem.json │ │ ├── advancedCommerceRequestRefundRequest.json │ │ ├── advancedCommerceRequestRefundResponse.json │ │ ├── advancedCommerceSubscriptionCancelRequest.json │ │ ├── advancedCommerceSubscriptionCancelResponse.json │ │ ├── advancedCommerceSubscriptionChangeMetadataDescriptors.json │ │ ├── advancedCommerceSubscriptionChangeMetadataItem.json │ │ ├── advancedCommerceSubscriptionChangeMetadataRequest.json │ │ ├── advancedCommerceSubscriptionChangeMetadataResponse.json │ │ ├── advancedCommerceSubscriptionCreateItem.json │ │ ├── advancedCommerceSubscriptionCreateRequest.json │ │ ├── advancedCommerceSubscriptionMigrateDescriptors.json │ │ ├── advancedCommerceSubscriptionMigrateItem.json │ │ ├── advancedCommerceSubscriptionMigrateRenewalItem.json │ │ ├── advancedCommerceSubscriptionMigrateRequest.json │ │ ├── advancedCommerceSubscriptionMigrateResponse.json │ │ ├── advancedCommerceSubscriptionModifyAddItem.json │ │ ├── advancedCommerceSubscriptionModifyChangeItem.json │ │ ├── advancedCommerceSubscriptionModifyDescriptors.json │ │ ├── advancedCommerceSubscriptionModifyInAppRequest.json │ │ ├── advancedCommerceSubscriptionModifyPeriodChange.json │ │ ├── advancedCommerceSubscriptionModifyRemoveItem.json │ │ ├── advancedCommerceSubscriptionPriceChangeItem.json │ │ ├── advancedCommerceSubscriptionPriceChangeRequest.json │ │ ├── advancedCommerceSubscriptionPriceChangeResponse.json │ │ ├── advancedCommerceSubscriptionReactivateInAppRequest.json │ │ ├── advancedCommerceSubscriptionReactivateItem.json │ │ ├── advancedCommerceSubscriptionRevokeRequest.json │ │ ├── advancedCommerceSubscriptionRevokeResponse.json │ │ ├── apiException.json │ │ ├── apiTooManyRequestsException.json │ │ ├── apiUnknownError.json │ │ ├── appData.json │ │ ├── appTransaction.json │ │ ├── appTransactionDoesNotExistError.json │ │ ├── appTransactionInfoResponse.json │ │ ├── decodedRealtimeRequest.json │ │ ├── extendRenewalDateForAllActiveSubscribersResponse.json │ │ ├── extendSubscriptionRenewalDateResponse.json │ │ ├── familyTransactionNotSupportedError.json │ │ ├── getAllSubscriptionStatusesResponse.json │ │ ├── getDefaultMessageResponse.json │ │ ├── getImageListResponse.json │ │ ├── getMessageListResponse.json │ │ ├── getNotificationHistoryResponse.json │ │ ├── getRealtimeUrlResponse.json │ │ ├── getRefundHistoryResponse.json │ │ ├── getStatusOfSubscriptionRenewalDateExtensionsResponse.json │ │ ├── getTestNotificationStatusResponse.json │ │ ├── invalidAppAccountTokenUUIDError.json │ │ ├── invalidTransactionIdError.json │ │ ├── lookupOrderIdResponse.json │ │ ├── performanceTestResponse.json │ │ ├── performanceTestResultResponse.json │ │ ├── requestTestNotificationResponse.json │ │ ├── signedConsumptionRequestNotification.json │ │ ├── signedExternalPurchaseTokenNotification.json │ │ ├── signedExternalPurchaseTokenSandboxNotification.json │ │ ├── signedNotification.json │ │ ├── signedRenewalInfo.json │ │ ├── signedRescindConsentNotification.json │ │ ├── signedSummaryNotification.json │ │ ├── signedTransaction.json │ │ ├── signedTransactionWithRevocation.json │ │ ├── transactionHistoryResponse.json │ │ ├── transactionHistoryResponseWithMalformedAppAppleId.json │ │ ├── transactionHistoryResponseWithMalformedEnvironment.json │ │ ├── transactionIdNotFoundError.json │ │ ├── transactionIdNotOriginalTransactionId.json │ │ └── transactionInfoResponse.json │ └── xcode/ │ ├── xcode-app-receipt-empty │ ├── xcode-app-receipt-with-transaction │ ├── xcode-signed-app-transaction │ ├── xcode-signed-renewal-info │ └── xcode-signed-transaction ├── test_advanced_commerce_models.py ├── test_api_client.py ├── test_api_client_async.py ├── test_app_data.py ├── test_decoded_payloads.py ├── test_jws_signature_creator.py ├── test_payload_verification.py ├── test_promotional_offer_signature_creator.py ├── test_receipt_utility.py ├── test_retention_messaging.py ├── test_x509_verifiction.py ├── test_xcode_signed_data.py └── util.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" time: "02:00" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" time: "02:00" ================================================ FILE: .github/workflows/ci-prb.yml ================================================ name: PR Builder permissions: contents: read on: pull_request: branches: [ main ] push: branches: [ main ] jobs: build: name: Python ${{ matrix.python }} ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: python: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] os: [ ubuntu-latest ] steps: - name: Checkout Code uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run tests run: | python -m unittest ================================================ FILE: .github/workflows/ci-release-docs.yml ================================================ name: Doc Builder permissions: contents: read on: release: types: [published] jobs: build: name: Python Doc Builder runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.14" - name: Install dependencies run: | python -m pip install --upgrade pip 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 - name: Spinx build run: sphinx-build -b html _staging _build - name: Upload docs uses: actions/upload-pages-artifact@v4 with: path: _build deploy: permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} needs: build runs-on: ubuntu-latest name: Deploy docs steps: - name: Deploy id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/ci-release.yml ================================================ name: Release Builder permissions: contents: read on: release: types: [published] jobs: build: # Only non-pre-release builds trigger a release if: "!github.event.release.prerelease" name: Python Release Builder runs-on: ubuntu-latest environment: pypi permissions: id-token: write steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.14" - name: Install build run: >- python3 -m pip install build --user - name: Build the sdist and wheel run: >- python3 -m build - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 ================================================ FILE: .github/workflows/ci-snapshot.yml ================================================ name: Snapshot Builder permissions: contents: read on: release: types: [published] jobs: build: # Pre-release builds trigger a snapshot being created if: "github.event.release.prerelease" name: Python Snapshot Builder runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.14" - name: Install build run: >- python3 -m pip install build --user - name: Build the sdist and wheel run: >- python3 -m build - name: Publish to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository-url: https://test.pypi.org/legacy/ ================================================ FILE: .gitignore ================================================ __pycache__/ build/ dist/ *.egg-info/ .pytest_cache/ # pyenv .python-version # Environments .env .venv venv # mypy .mypy_cache/ .dmypy.json dmypy.json # JetBrains .idea/ /coverage.xml /.coverage .DS_Store _build _staging ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## Version 3.0.0 - Incorporate changes for App Store Server API v1.19 [https://github.com/apple/app-store-server-library-python/pull/172] from @riyazpanjwani - This changes ConsumptionRequest and several dependent types to the V2 variant, while the V1 version was created as a new type, to align with documentation, which is a breaking change ## Version 2.0.0 - Support Retention Messaging API [https://github.com/apple/app-store-server-library-python/pull/160] - This changes internal details of BaseAppStoreServerAPIClient, which is a breaking change for subclassing clients - Incorporate changes for App Store Server API v1.17 [https://github.com/apple/app-store-server-library-python/pull/162] from @riyazpanjwani - Add a new VerificationStatus case for retryable OCSP network failures [https://github.com/apple/app-store-server-library-python/pull/163] - Add timeout to the AppStoreServerAPIClient [https://github.com/apple/app-store-server-library-python/pull/164] - Incorporate changes for App Store Server API v1.18 [https://github.com/apple/app-store-server-library-python/pull/166] from @izanger - This changes OfferType's case SUBSCRIPTION_OFFER_CODE to OFFER_CODE, which is a breaking change ## Version 1.9.0 - Incorporate changes for App Store Server API v1.16 [https://github.com/apple/app-store-server-library-python/pull/141] from @riyazpanjwani - Fix SyntaxWarning in regex pattern string [https://github.com/apple/app-store-server-library-python/pull/138] from @krepe90 ## Version 1.8.0 - Incorporate changes for App Store Server API v1.15 and App Store Server Notifications v2.15 [https://github.com/apple/app-store-server-library-python/pull/134] ## Version 1.7.0 - Update the SignedDataVerifier to cache verified certificate chains, improving performance [https://github.com/apple/app-store-server-library-python/pull/122] ## Version 1.6.0 - Update README to improve Dependabot link discovery [https://github.com/apple/app-store-server-library-python/pull/119] ## Version 1.5.0 - Add an async client built on httpx [https://github.com/apple/app-store-server-library-python/pull/105] - Drop Python 3.7 support [https://github.com/apple/app-store-server-library-python/pull/106] - Add check for original transaction id in legacy receipts [https://github.com/apple/app-store-server-library-python/pull/104] from @willhnation ## Version 1.4.0 - Incorporate changes for App Store Server API v1.13 and App Store Server Notifications v2.13 [https://github.com/apple/app-store-server-library-python/pull/102] - Remove the upper limit on libraries that are unlikely to break upon upgrade [https://github.com/apple/app-store-server-library-python/pull/101] ## Version 1.3.0 - Incorporate changes for App Store Server API v1.12 and App Store Server Notifications v2.12 [https://github.com/apple/app-store-server-library-python/pull/95] - Fix deprecation warnings from cryptography [https://github.com/apple/app-store-server-library-python/pull/94] from @WFT - Replace use of deprecated datetime.utcnow() [https://github.com/apple/app-store-server-library-python/pull/93] from @WFT - Cache cattrs values to prevent memory leak [https://github.com/apple/app-store-server-library-python/pull/92] from @Reskov ## Version 1.2.1 - Fix issue with OfferType in JWSTransactionDecodedPayload [https://github.com/apple/app-store-server-library-python/pull/88] from @devinwang ## Version 1.2.0 - Incorporate changes for App Store Server API v1.11 and App Store Server Notifications v2.11 [https://github.com/apple/app-store-server-library-python/pull/85] - Various documentation and quality of life improvements, including contributions from @CallumWatkins, @hakusai22, and @sunny-dubey ## Version 1.1.0 - Support App Store Server Notifications v2.10 [https://github.com/apple/app-store-server-library-python/pull/65] - Bump cryptography and pyOpenSSL maximum versions [https://github.com/apple/app-store-server-library-python/pull/61]/[https://github.com/apple/app-store-server-library-python/pull/63] - Require appAppleId in SignedDataVerifier for the Production environment [https://github.com/apple/app-store-server-library-python/pull/60] ## 1.0.0 - Add error message to APIException [https://github.com/apple/app-store-server-library-python/pull/52] ## 0.3.0 - Add missing status field to the Data model [https://github.com/apple/app-store-server-library-python/pull/33] - Add error codes from App Store Server API v1.9 [https://github.com/apple/app-store-server-library-python/pull/39] - Add new fields from App Store Server API v1.10 [https://github.com/apple/app-store-server-library-python/pull/46] - Add support for reading unknown enum values [https://github.com/apple/app-store-server-library-python/pull/45] - Add support for Xcode and LocalTesting environments [https://github.com/apple/app-store-server-library-python/pull/44] ## 0.2.1 - Add py.typed file [https://github.com/apple/app-store-server-library-python/pull/19] - Correct type annotation in PromotionalOfferSignatureCreator [https://github.com/apple/app-store-server-library-python/pull/17] ## 0.2.0 - Correct type in LastTransactionsItem's status field [https://github.com/apple/app-store-server-library-python/pull/11] - Fix default value None for fields should require an Optional type [https://github.com/apple/app-store-server-library-python/pull/6] ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [opensource-conduct@group.apple.com](mailto:opensource-conduct@group.apple.com). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thank you for your interest in contributing! ## Reporting Bugs Please report bugs by creating [Github issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/about-issues). To help the community understand the bug and get it fixed faster, please provide the following information when creating a new issue: - A clear and descriptive title - The exact steps to reproduce the bug - The observed behavior and expected behavior If possible, also include payloads, commands, screenshots, etc to help the community identify the problem. Do not include any personal or sensitive data. ## Suggesting Improvements You can suggest improvements also by creating Github issues. When creating a new suggestion, please provide the following information: - A clear and descriptive title - A description of the proposed improvement in as many details as possible - Explain why the improvement is important ## Documentation Contribution Documentation contribution will make it easier for the community to work on the project. You may add README/diagrams to the components, or improve the existing docs. For major doc changes, we encourage you to create issues before contributing. Let us know what you are planning to change before the contribution. ## Code Contribution For minor changes (like small bug fixes or typo correction), feel free to open up a PR directly. For new features or major changes, we encourage you to create a Github issue first, and get agreement before starting on the implementation. This is to save you time in case there's duplicate effort or unforeseen risk. ## Project Licensing All contributions (including Pull Requests) to this project are provided under the terms of the project’s [LICENSE](LICENSE.txt) ================================================ FILE: LICENSE.txt ================================================ Copyright 2023 Apple Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include README.md ================================================ FILE: NOTICE.txt ================================================ Acknowledgements Portions of this App Store Server Library software may utilize the following copyrighted material, the use of which is hereby acknowledged. _____________________ Hynek Schlawack and the attrs contributors (attrs) The MIT License (MIT) Copyright (c) 2015 Hynek Schlawack and the attrs contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. _____________________ José Padilla (pyjwt) The MIT License (MIT) Copyright (c) 2015-2022 José Padilla Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. _____________________ requests contributors (requests) Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. _____________________ cryptography contributors (pyca/cryptography) Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. _____________________ pyOpenSSL contributors (pyOpenSSL) Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. _____________________ The Python-ASN1 authors. (python-asn1) Copyright (c) 2007-2021 the Python-ASN1 authors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. _____________________ Tin Tvrtković (cattrs) MIT License Copyright (c) 2016, Tin Tvrtković Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. _____________________ Encode OSS Ltd. (httpx) Copyright © 2019, Encode OSS Ltd. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # 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). ## Table of Contents 1. [Installation](#installation) 2. [Documentation](#documentation) 3. [Usage](#usage) 4. [Support](#support) ## Installation #### Requirements - Python 3.8+ ### pip ```sh pip install app-store-server-library ``` ## Documentation [Documentation](https://apple.github.io/app-store-server-library-python/) [WWDC Video](https://developer.apple.com/videos/play/wwdc2023/10143/) ### Obtaining an In-App Purchase key from App Store Connect To use the App Store Server API or create promotional offer signatures, a signing key downloaded from App Store Connect is required. To obtain this key, you must have the Admin role. Go to Users and Access > Integrations > In-App Purchase. Here you can create and manage keys, as well as find your issuer ID. When using a key, you'll need the key ID and issuer ID as well. ### Obtaining Apple Root Certificates Download and store the root certificates found in the Apple Root Certificates section of the [Apple PKI](https://www.apple.com/certificateauthority/) site. Provide these certificates as an array to a SignedDataVerifier to allow verifying the signed data comes from Apple. ## Usage ### API Usage ```python from appstoreserverlibrary.api_client import AppStoreServerAPIClient, APIException from appstoreserverlibrary.models.Environment import Environment private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary key_id = "ABCDEFGHIJ" issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55" bundle_id = "com.example" environment = Environment.SANDBOX client = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment) try: response = client.request_test_notification() print(response) except APIException as e: print(e) ``` ### Verification Usage ```python from appstoreserverlibrary.models.Environment import Environment from appstoreserverlibrary.signed_data_verifier import VerificationException, SignedDataVerifier root_certificates = load_root_certificates() enable_online_checks = True bundle_id = "com.example" environment = Environment.SANDBOX app_apple_id = None # appAppleId must be provided for the Production environment signed_data_verifier = SignedDataVerifier(root_certificates, enable_online_checks, environment, bundle_id, app_apple_id) try: signed_notification = "ey.." payload = signed_data_verifier.verify_and_decode_notification(signed_notification) print(payload) except VerificationException as e: print(e) ``` ### Receipt Usage ```python from appstoreserverlibrary.api_client import AppStoreServerAPIClient, APIException, GetTransactionHistoryVersion from appstoreserverlibrary.models.Environment import Environment from appstoreserverlibrary.receipt_utility import ReceiptUtility from appstoreserverlibrary.models.HistoryResponse import HistoryResponse from appstoreserverlibrary.models.TransactionHistoryRequest import TransactionHistoryRequest, ProductType, Order private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary key_id = "ABCDEFGHIJ" issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55" bundle_id = "com.example" environment = Environment.SANDBOX client = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment) receipt_util = ReceiptUtility() app_receipt = "MI.." try: transaction_id = receipt_util.extract_transaction_id_from_app_receipt(app_receipt) if transaction_id != None: transactions = [] response: HistoryResponse = None request: TransactionHistoryRequest = TransactionHistoryRequest( sort=Order.ASCENDING, revoked=False, productTypes=[ProductType.AUTO_RENEWABLE] ) while response == None or response.hasMore: revision = response.revision if response != None else None response = client.get_transaction_history(transaction_id, revision, request, GetTransactionHistoryVersion.V2) for transaction in response.signedTransactions: transactions.append(transaction) print(transactions) except APIException as e: print(e) ``` ### Promotional Offer Signature Creation ```python from appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator import time private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary key_id = "ABCDEFGHIJ" bundle_id = "com.example" promotion_code_signature_generator = PromotionalOfferSignatureCreator(private_key, key_id, bundle_id) product_id = "" subscription_offer_id = "" application_username = "" nonce = "" timestamp = round(time.time()*1000) base64_encoded_signature = promotion_code_signature_generator.create_signature(product_id, subscription_offer_id, application_username, nonce, timestamp) ``` ### Async HTTPX Support #### Pip Include the optional async dependency ```sh pip install app-store-server-library[async] ``` #### API Usage ```python from appstoreserverlibrary.api_client import AsyncAppStoreServerAPIClient, APIException from appstoreserverlibrary.models.Environment import Environment private_key = read_private_key("/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8") # Implementation will vary key_id = "ABCDEFGHIJ" issuer_id = "99b16628-15e4-4668-972b-eeff55eeff55" bundle_id = "com.example" environment = Environment.SANDBOX client = AsyncAppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment) try: response = await client.request_test_notification() print(response) except APIException as e: print(e) # Once client use is finished await client.async_close() ``` ## Support Only the latest major version of the library will receive updates, including security updates. Therefore, it is recommended to update to new major versions. ================================================ FILE: appstoreserverlibrary/__init__.py ================================================ ================================================ FILE: appstoreserverlibrary/api_client.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. import calendar import datetime import warnings from enum import IntEnum, Enum from typing import Any, Dict, List, MutableMapping, Optional, Type, TypeVar, Union from attr import define import requests import jwt from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter from .models.CheckTestNotificationResponse import CheckTestNotificationResponse from .models.ConsumptionRequest import ConsumptionRequest from .models.ConsumptionRequestV1 import ConsumptionRequestV1 from .models.DefaultConfigurationRequest import DefaultConfigurationRequest from .models.DefaultConfigurationResponse import DefaultConfigurationResponse from .models.Environment import Environment from .models.ExtendRenewalDateRequest import ExtendRenewalDateRequest from .models.ExtendRenewalDateResponse import ExtendRenewalDateResponse from .models.GetImageListResponse import GetImageListResponse from .models.GetMessageListResponse import GetMessageListResponse from .models.HistoryResponse import HistoryResponse from .models.ImageSize import ImageSize from .models.MassExtendRenewalDateRequest import MassExtendRenewalDateRequest from .models.MassExtendRenewalDateResponse import MassExtendRenewalDateResponse from .models.MassExtendRenewalDateStatusResponse import MassExtendRenewalDateStatusResponse from .models.NotificationHistoryRequest import NotificationHistoryRequest from .models.NotificationHistoryResponse import NotificationHistoryResponse from .models.OrderLookupResponse import OrderLookupResponse from .models.PerformanceTestRequest import PerformanceTestRequest from .models.PerformanceTestResponse import PerformanceTestResponse from .models.PerformanceTestResultResponse import PerformanceTestResultResponse from .models.RealtimeUrlRequest import RealtimeUrlRequest from .models.RealtimeUrlResponse import RealtimeUrlResponse from .models.RefundHistoryResponse import RefundHistoryResponse from .models.SendTestNotificationResponse import SendTestNotificationResponse from .models.Status import Status from .models.StatusResponse import StatusResponse from .models.TransactionHistoryRequest import TransactionHistoryRequest from .models.TransactionInfoResponse import TransactionInfoResponse from .models.AppTransactionInfoResponse import AppTransactionInfoResponse from .models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest from .models.UploadMessageRequestBody import UploadMessageRequestBody from uuid import UUID T = TypeVar('T') class APIError(IntEnum): 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_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. 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. 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. 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. https://developer.apple.com/documentation/appstoreserverapi/invalidsorterror """ INVALID_PRODUCT_TYPE = 4000022 """ An error that indicates an invalid product type parameter. 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. 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 """ 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 """ 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.11 """ 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_ERROR = 4000048 """ An error that indicates the endpoint doesn't support an app transaction ID. https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror """ 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 """ ALT_TEXT_TOO_LONG = 4000175 """ An error that indicates the alternative text for an image is too long. https://developer.apple.com/documentation/retentionmessaging/alttexttoolongerror """ INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR = 4000183 """ An error that indicates the app account token value is not a valid UUID. https://developer.apple.com/documentation/appstoreserverapi/invalidappaccounttokenuuiderror """ FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR = 4000185 """ An error that indicates the transaction is for a product the customer obtains through Family Sharing, which the endpoint doesn’t support. https://developer.apple.com/documentation/appstoreserverapi/familytransactionnotsupportederror """ TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR = 4000187 """ An error that indicates the endpoint expects an original transaction identifier. https://developer.apple.com/documentation/appstoreserverapi/transactionidisnotoriginaltransactioniderror """ INVALID_PERFORMANCE_TEST_REQUEST = 4000211 """ An error the API returns that indicates the performance test request is invalid. https://developer.apple.com/documentation/retentionmessaging/invalidperformancetestrequesterror """ INVALID_REQUEST_ID = 4000212 """ An error that indicates the request ID is invalid. https://developer.apple.com/documentation/retentionmessaging/invalidrequestiderror """ EXISTING_PERFORMANCE_TEST_RUN = 4000213 """ An error that indicates an error with an existing test. https://developer.apple.com/documentation/retentionmessaging/existingperformancetestrunerror """ BAD_REQUEST_REALTIME_URL = 4000215 """ An error that indicates the URL is invalid. https://developer.apple.com/documentation/retentionmessaging/badrequestrealtimeurlerror """ BAD_REQUEST_IMAGE_SIZE = 4000216 """ An error that indicates the image size provided is invalid. https://developer.apple.com/documentation/retentionmessaging/badrequestimagesizeerror """ BAD_REQUEST_TOO_MANY_BULLET_POINTS = 4000218 """ An error that indicates there are too many bullet points. https://developer.apple.com/documentation/retentionmessaging/badrequesttoomanybulletpointserror """ BAD_REQUEST_BULLET_POINT_TEXT_TOO_LONG = 4000219 """ An error that indicates the text for a bullet point is too long. https://developer.apple.com/documentation/retentionmessaging/badrequestbulletpointtexttoolongerror """ 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. https://developer.apple.com/documentation/retentionmessaging/badrequestaboveimagerequiresanimageerror """ SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030004 """ 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. https://developer.apple.com/documentation/appstoreserverapi/familysharedsubscriptionextensionineligibleerror """ MAXIMUM_NUMBER_OF_IMAGES_REACHED = 4030014 """ An error that indicates when you reach the maximum number of uploaded images. https://developer.apple.com/documentation/retentionmessaging/maximumnumberofimagesreachederror """ MAXIMUM_NUMBER_OF_MESSAGES_REACHED = 4030016 """ An error that indicates when you reach the maximum number of uploaded messages. https://developer.apple.com/documentation/retentionmessaging/maximumnumberofmessagesreachederror """ 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. 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. 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. https://developer.apple.com/documentation/retentionmessaging/imageinuseerror """ 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. https://developer.apple.com/documentation/retentionmessaging/forbiddennopassingtesterror """ 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. 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. 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. 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. https://developer.apple.com/documentation/appstoreserverapi/statusrequestnotfounderror """ TRANSACTION_ID_NOT_FOUND = 4040010 """ 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. https://developer.apple.com/documentation/retentionmessaging/imagenotfounderror """ MESSAGE_NOT_FOUND = 4040015 """ An error that indicates the system can't find the message identifier. https://developer.apple.com/documentation/retentionmessaging/messagenotfounderror """ PERFORMANCE_TEST_RUN_NOT_FOUND = 4040018 """ 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 """ An error response that indicates an app transaction doesn’t exist for the specified customer. https://developer.apple.com/documentation/appstoreserverapi/apptransactiondoesnotexisterror """ DEFAULT_MESSAGE_NOT_FOUND = 4040020 """ An error that indicates a default message isn’t configured. https://developer.apple.com/documentation/retentionmessaging/defaultmessagenotfounderror """ REALTIME_URL_NOT_FOUND = 4040021 """ An error that indicates that the URL for your endpoint isn’t configured. https://developer.apple.com/documentation/retentionmessaging/realtimeurlnotfounderror """ IMAGE_ALREADY_EXISTS = 4090000 """ An error that indicates the image identifier already exists. https://developer.apple.com/documentation/retentionmessaging/imagealreadyexistserror """ MESSAGE_ALREADY_EXISTS = 4090001 """ An error that indicates the message identifier already exists. https://developer.apple.com/documentation/retentionmessaging/messagealreadyexistserror """ RATE_LIMIT_EXCEEDED = 4290000 """ An error that indicates that 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 """ @define class APIException(Exception): http_status_code: int api_error: Optional[APIError] raw_api_error: Optional[int] error_message: Optional[str] def __init__(self, http_status_code: int, raw_api_error: Optional[int] = None, error_message: Optional[str] = None): self.http_status_code = http_status_code self.raw_api_error = raw_api_error self.api_error = None self.error_message = error_message try: if raw_api_error is not None: self.api_error = APIError(raw_api_error) except ValueError: pass class GetTransactionHistoryVersion(str, Enum): V1 = "v1" """ .. deprecated:: 1.3.0 """ V2 = "v2" class BaseAppStoreServerAPIClient: def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment): if environment == Environment.XCODE: raise ValueError("Xcode is not a supported environment for an AppStoreServerAPIClient") if environment == Environment.PRODUCTION: self._base_url = "https://api.storekit.itunes.apple.com" elif environment == Environment.LOCAL_TESTING: self._base_url = "https://local-testing-base-url" elif environment == Environment.SANDBOX: self._base_url = "https://api.storekit-sandbox.itunes.apple.com" else: raise ValueError("Invalid environment provided") self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend()) self._key_id = key_id self._issuer_id = issuer_id self._bundle_id = bundle_id def _generate_token(self) -> str: future_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=5) return jwt.encode( { "bid": self._bundle_id, "iss": self._issuer_id, "aud": "appstoreconnect-v1", "exp": calendar.timegm(future_time.timetuple()), }, self._signing_key, algorithm="ES256", headers={"kid": self._key_id}, ) def _get_full_url(self, path) -> str: return self._base_url + path def _get_headers(self) -> Dict[str, str]: return { 'User-Agent': "app-store-server-library/python/3.0.0", 'Authorization': f'Bearer {self._generate_token()}', 'Accept': 'application/json' } def _get_request_json(self, body) -> Dict[str, Any]: c = _get_cattrs_converter(type(body)) if body is not None else None return c.unstructure(body) if body is not None else None def _parse_response(self, status_code: int, headers: MutableMapping, json_supplier, destination_class: Type[T]) -> T: if 200 <= status_code < 300: if destination_class is None: return c = _get_cattrs_converter(destination_class) response_body = json_supplier() return c.structure(response_body, destination_class) else: # Best effort parsing of the response body if not 'content-type' in headers or headers['content-type'] != 'application/json': raise APIException(status_code) try: response_body = json_supplier() raise APIException(status_code, response_body['errorCode'], response_body['errorMessage']) except APIException as e: raise e except Exception as e: raise APIException(status_code) from e class AppStoreServerAPIClient(BaseAppStoreServerAPIClient): def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment): super().__init__(signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id, environment=environment) def _make_request(self, path: str, method: str, queryParameters: Dict[str, Union[str, List[str]]], body, destination_class: Type[T], content_type: Optional[str] = None) -> T: url = self._get_full_url(path) headers = self._get_headers() if isinstance(body, bytes): if content_type: headers['Content-Type'] = content_type response = self._execute_request(method, url, queryParameters, headers, None, body) else: json = self._get_request_json(body) response = self._execute_request(method, url, queryParameters, headers, json, None) return self._parse_response(response.status_code, response.headers, lambda: response.json(), destination_class) def _execute_request(self, method: str, url: str, params: Dict[str, Union[str, List[str]]], headers: Dict[str, str], json: Optional[Dict[str, Any]], data: Optional[bytes]) -> requests.Response: return requests.request(method, url, params=params, headers=headers, json=json, data=data, timeout=30) 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 :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. :throws APIException: If a response was returned indicating the request could not be processed """ return self._make_request("/inApps/v1/subscriptions/extend/mass", "POST", {}, mass_extend_renewal_date_request, MassExtendRenewalDateResponse, None) 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 :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) def get_all_subscription_statuses(self, 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 :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. :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. :return: A response that contains status information for all of a customer's auto-renewable subscriptions in your app. :throws APIException: If a response was returned indicating the request could not be processed """ queryParameters: Dict[str, List[str]] = dict() if status is not None: queryParameters["status"] = [s.value for s in status] return self._make_request(f"/inApps/v1/subscriptions/{transaction_id}", "GET", queryParameters, None, StatusResponse, None) def get_refund_history(self, 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 :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. :param revision: A token you provide to get the next set of up to 20 transactions. All responses include a revision token. 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. :throws APIException: If a response was returned indicating the request could not be processed """ queryParameters: Dict[str, List[str]] = dict() if revision is not None: queryParameters["revision"] = [revision] return self._make_request(f"/inApps/v2/refund/lookup/{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 :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) 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 :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) 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 :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. :return: A response that contains the App Store Server Notifications history for your app. :throws APIException: If a response was returned indicating the request could not be processed """ queryParameters: Dict[str, List[str]] = dict() if pagination_token is not None: queryParameters["paginationToken"] = [pagination_token] return self._make_request("/inApps/v1/notifications/history", "POST", queryParameters, notification_history_request, NotificationHistoryResponse, None) def get_transaction_history(self, transaction_id: str, revision: Optional[str], transaction_history_request: TransactionHistoryRequest, version: GetTransactionHistoryVersion = GetTransactionHistoryVersion.V1) -> HistoryResponse: """ Get a customer's in-app purchase transaction history for your app. https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. :param revision: A token you provide to get the next set of up to 20 transactions. All responses include a revision token. Note: For requests that use the revision token, include the same query parameters from the initial request. Use the revision token from the previous HistoryResponse. :param transaction_history_request: The request parameters that includes the startDate,endDate,productIds,productTypes and optional query constraints. :param version: The version of the Get Transaction History endpoint to use. V2 is recommended. :return: A response that contains the customer's transaction history for an app. :throws APIException: If a response was returned indicating the request could not be processed """ queryParameters: Dict[str, List[str]] = dict() if revision is not None: queryParameters["revision"] = [revision] if transaction_history_request.startDate is not None: queryParameters["startDate"] = [str(transaction_history_request.startDate)] if transaction_history_request.endDate is not None: queryParameters["endDate"] = [str(transaction_history_request.endDate)] if transaction_history_request.productIds is not None: queryParameters["productId"] = transaction_history_request.productIds if transaction_history_request.productTypes is not None: queryParameters["productType"] = [product_type.value for product_type in transaction_history_request.productTypes] if transaction_history_request.sort is not None: queryParameters["sort"] = [transaction_history_request.sort.value] if transaction_history_request.subscriptionGroupIdentifiers is not None: queryParameters["subscriptionGroupIdentifier"] = transaction_history_request.subscriptionGroupIdentifiers if transaction_history_request.inAppOwnershipType is not None: queryParameters["inAppOwnershipType"] = [transaction_history_request.inAppOwnershipType.value] if transaction_history_request.revoked is not None: queryParameters["revoked"] = [str(transaction_history_request.revoked)] return self._make_request("/inApps/{}/history/{}".format(version.value, 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 :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) 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 :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) 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 :return: A response that contains the test notification token. :throws APIException: If a response was returned indicating the request could not be processed """ return self._make_request("/inApps/v1/notifications/test", "POST", {}, None, SendTestNotificationResponse, None) def send_consumption_data(self, transaction_id: str, consumption_request: ConsumptionRequestV1): """ Send consumption information about a consumable in-app purchase to the App Store after your server receives a consumption request notification. https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1 .. deprecated:: Use :func:`send_consumption_information` instead. :param transaction_id: The transaction identifier for which you're providing consumption information. You receive this identifier in the CONSUMPTION_REQUEST notification the App Store sends to your server. :param consumption_request: The request body containing consumption information. :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) def send_consumption_information(self, transaction_id: str, consumption_request: ConsumptionRequest): """ Send consumption information about an In-App Purchase to the App Store after your server receives a consumption request notification. https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information :param transaction_id: The transaction identifier for which you're providing consumption information. You receive this identifier in the CONSUMPTION_REQUEST notification the App Store sends to your server's App Store Server Notifications V2 endpoint. :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) def set_app_account_token(self, original_transaction_id: str, update_app_account_token_request: UpdateAppAccountTokenRequest): """ Sets the app account token value for a purchase the customer makes outside your app, or updates its value in an existing transaction. https://developer.apple.com/documentation/appstoreserverapi/set-app-account-token :param original_transaction_id The original transaction identifier of the transaction to receive the app account token update. :param update_app_account_token_request The request body that contains a valid app account token value. :raises APIException: If a response was returned indicating the request could not be processed """ self._make_request(f"/inApps/v1/transactions/{original_transaction_id}/appAccountToken", "PUT", {}, update_app_account_token_request, None, None) def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optional[ImageSize] = None): """ Upload an image to use for retention messaging. :param image_identifier: A UUID you provide to uniquely identify the image you upload. :param image: The image file to upload. :param image_size: The size of the image you upload. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/upload-image """ 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") def delete_image(self, image_identifier: UUID): """ Delete a previously uploaded image. :param image_identifier: The identifier of the image to delete. :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) def get_image_list(self) -> GetImageListResponse: """ Get the image identifier and state for all uploaded images. :return: A response that contains status information for all images. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-image-list """ return self._make_request("/inApps/v1/messaging/image/list", "GET", {}, None, GetImageListResponse, None) def upload_message(self, message_identifier: UUID, upload_message_request_body: UploadMessageRequestBody): """ Upload a message to use for retention messaging. :param message_identifier: A UUID you provide to uniquely identify the message you upload. :param upload_message_request_body: The message text to upload. :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) def delete_message(self, message_identifier: UUID): """ Delete a previously uploaded message. :param message_identifier: The identifier of the message to delete. :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) def get_message_list(self) -> GetMessageListResponse: """ Get the message identifier and state of all uploaded messages. :return: A response that contains status information for all messages. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-message-list """ return self._make_request("/inApps/v1/messaging/message/list", "GET", {}, None, GetMessageListResponse, None) def configure_default_message(self, product_id: str, locale: str, default_configuration_request: DefaultConfigurationRequest): """ Configure a default message for a specific product in a specific locale. :param product_id: The product identifier for the default configuration. :param locale: The locale for the default configuration. :param default_configuration_request: The request body that includes the message identifier to configure as the default message. :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) def delete_default_message(self, product_id: str, locale: str): """ Delete a default message for a product in a locale. :param product_id: The product ID of the default message configuration. :param locale: The locale of the default message configuration. :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) def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurationResponse: """ Gets the default message for a specific product in a specific locale, if it’s configured. :param product_id: The product identifier of the message. :param locale: The locale of the message. :return: The response body that contains the default configuration information. :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) def configure_realtime_url(self, realtime_url_request: RealtimeUrlRequest): """ Configures the URL for your Get Retention Message endpoint in the sandbox and production environments. :param realtime_url_request: The request body that includes your endpoint’s URL. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/configure-realtime-url """ self._make_request("/inApps/v1/messaging/realtime/url", "PUT", {}, realtime_url_request, None, None) def delete_realtime_url(self): """ Deletes the URL for your Get Retention Message endpoint, in the sandbox or production environments. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/delete-realtime-url """ self._make_request("/inApps/v1/messaging/realtime/url", "DELETE", {}, None, None, None) def get_realtime_url(self) -> RealtimeUrlResponse: """ Gets the URL for real-time messages that points to your Get Retention Message endpoint, which you previously configured. :return: The response body that contains the URL for your Get Retention Message endpoint. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-realtime-url """ return self._make_request("/inApps/v1/messaging/realtime/url", "GET", {}, None, RealtimeUrlResponse, None) def initiate_performance_test(self, performance_test_request: PerformanceTestRequest) -> PerformanceTestResponse: """ Initiates a performance test of your Get Retention Message endpoint in the sandbox environment. :param performance_test_request: The request body which specifies a transaction identifier of an In-App Purchase to use for this test. :return: The performance test response object. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/initiate-performance-test """ return self._make_request("/inApps/v1/messaging/performanceTest", "POST", {}, performance_test_request, PerformanceTestResponse, None) def get_performance_test_results(self, request_id: str) -> PerformanceTestResultResponse: """ Gets the results of the performance test for the specified identifier. :param request_id: The ID of the performance test to return, which you receive in the PerformanceTestResponse when you call Initiate Performance Test. :return: An object the API returns that describes the performance test results. :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) def get_app_transaction_info(self, transaction_id: str) -> AppTransactionInfoResponse: """ Get a customer's app transaction information for your app. :param transaction_id Any originalTransactionId, transactionId or appTransactionId that belongs to the customer for your app. :return: A response that contains signed app transaction information for a customer. :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/{transaction_id}", "GET", {}, None, AppTransactionInfoResponse, None) class AsyncAppStoreServerAPIClient(BaseAppStoreServerAPIClient): def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment): super().__init__(signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id, environment=environment) try: import httpx self.http_client = httpx.AsyncClient() except: raise ModuleNotFoundError("httpx not found but attempting to instantiate an async client") async def async_close(self): await self.http_client.aclose() async def _make_request(self, path: str, method: str, queryParameters: Dict[str, Union[str, List[str]]], body, destination_class: Type[T], content_type: Optional[str] = None) -> T: url = self._get_full_url(path) headers = self._get_headers() if isinstance(body, bytes): # For binary data like images if content_type: headers['Content-Type'] = content_type response = await self._execute_request(method, url, queryParameters, headers, None, body) else: # For JSON data json = self._get_request_json(body) response = await self._execute_request(method, url, queryParameters, headers, json, None) return self._parse_response(response.status_code, response.headers, lambda: response.json(), destination_class) async def _execute_request(self, method: str, url: str, params: Dict[str, Union[str, List[str]]], headers: Dict[str, str], json: Optional[Dict[str, Any]], data: Optional[bytes]): return await self.http_client.request(method, url, params=params, headers=headers, json=json, data=data, timeout=30) 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 :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. :throws APIException: If a response was returned indicating the request could not be processed """ return await self._make_request("/inApps/v1/subscriptions/extend/mass", "POST", {}, mass_extend_renewal_date_request, MassExtendRenewalDateResponse, None) 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 :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) async def get_all_subscription_statuses(self, 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 :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. :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. :return: A response that contains status information for all of a customer's auto-renewable subscriptions in your app. :throws APIException: If a response was returned indicating the request could not be processed """ queryParameters: Dict[str, List[str]] = dict() if status is not None: queryParameters["status"] = [s.value for s in status] return await self._make_request(f"/inApps/v1/subscriptions/{transaction_id}", "GET", queryParameters, None, StatusResponse, None) async def get_refund_history(self, 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 :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. :param revision: A token you provide to get the next set of up to 20 transactions. All responses include a revision token. 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. :throws APIException: If a response was returned indicating the request could not be processed """ queryParameters: Dict[str, List[str]] = dict() if revision is not None: queryParameters["revision"] = [revision] return await self._make_request(f"/inApps/v2/refund/lookup/{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 :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) 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 :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) 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 :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. :return: A response that contains the App Store Server Notifications history for your app. :throws APIException: If a response was returned indicating the request could not be processed """ queryParameters: Dict[str, List[str]] = dict() if pagination_token is not None: queryParameters["paginationToken"] = [pagination_token] return await self._make_request("/inApps/v1/notifications/history", "POST", queryParameters, notification_history_request, NotificationHistoryResponse, None) async def get_transaction_history(self, transaction_id: str, revision: Optional[str], transaction_history_request: TransactionHistoryRequest, version: GetTransactionHistoryVersion = GetTransactionHistoryVersion.V1) -> HistoryResponse: """ Get a customer's in-app purchase transaction history for your app. https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier. :param revision: A token you provide to get the next set of up to 20 transactions. All responses include a revision token. Note: For requests that use the revision token, include the same query parameters from the initial request. Use the revision token from the previous HistoryResponse. :param transaction_history_request: The request parameters that includes the startDate,endDate,productIds,productTypes and optional query constraints. :param version: The version of the Get Transaction History endpoint to use. V2 is recommended. :return: A response that contains the customer's transaction history for an app. :throws APIException: If a response was returned indicating the request could not be processed """ queryParameters: Dict[str, List[str]] = dict() if revision is not None: queryParameters["revision"] = [revision] if transaction_history_request.startDate is not None: queryParameters["startDate"] = [str(transaction_history_request.startDate)] if transaction_history_request.endDate is not None: queryParameters["endDate"] = [str(transaction_history_request.endDate)] if transaction_history_request.productIds is not None: queryParameters["productId"] = transaction_history_request.productIds if transaction_history_request.productTypes is not None: queryParameters["productType"] = [product_type.value for product_type in transaction_history_request.productTypes] if transaction_history_request.sort is not None: queryParameters["sort"] = [transaction_history_request.sort.value] if transaction_history_request.subscriptionGroupIdentifiers is not None: queryParameters["subscriptionGroupIdentifier"] = transaction_history_request.subscriptionGroupIdentifiers if transaction_history_request.inAppOwnershipType is not None: queryParameters["inAppOwnershipType"] = [transaction_history_request.inAppOwnershipType.value] if transaction_history_request.revoked is not None: queryParameters["revoked"] = [str(transaction_history_request.revoked)] return await self._make_request("/inApps/" + version + "/history/" + 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 :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) 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 :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) 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 :return: A response that contains the test notification token. :throws APIException: If a response was returned indicating the request could not be processed """ return await self._make_request("/inApps/v1/notifications/test", "POST", {}, None, SendTestNotificationResponse, None) async def send_consumption_data(self, transaction_id: str, consumption_request: ConsumptionRequestV1): """ Send consumption information about a consumable in-app purchase to the App Store after your server receives a consumption request notification. https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1 .. deprecated:: Use :func:`send_consumption_information` instead. :param transaction_id: The transaction identifier for which you're providing consumption information. You receive this identifier in the CONSUMPTION_REQUEST notification the App Store sends to your server. :param consumption_request: The request body containing consumption information. :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) async def send_consumption_information(self, transaction_id: str, consumption_request: ConsumptionRequest): """ Send consumption information about an In-App Purchase to the App Store after your server receives a consumption request notification. https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information :param transaction_id: The transaction identifier for which you're providing consumption information. You receive this identifier in the CONSUMPTION_REQUEST notification the App Store sends to your server's App Store Server Notifications V2 endpoint. :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) async def set_app_account_token(self, original_transaction_id: str, update_app_account_token_request: UpdateAppAccountTokenRequest): """ Sets the app account token value for a purchase the customer makes outside your app, or updates its value in an existing transaction. https://developer.apple.com/documentation/appstoreserverapi/set-app-account-token :param original_transaction_id The original transaction identifier of the transaction to receive the app account token update. :param update_app_account_token_request The request body that contains a valid app account token value. :raises APIException: If a response was returned indicating the request could not be processed """ await self._make_request(f"/inApps/v1/transactions/{original_transaction_id}/appAccountToken", "PUT", {}, update_app_account_token_request, None, None) async def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optional[ImageSize] = None): """ Upload an image to use for retention messaging. :param image_identifier: A UUID you provide to uniquely identify the image you upload. :param image: The image file to upload. :param image_size: The optional size of the image. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/upload-image """ 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") async def delete_image(self, image_identifier: UUID): """ Delete a previously uploaded image. :param image_identifier: The identifier of the image to delete. :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) async def get_image_list(self) -> GetImageListResponse: """ Get the image identifier and state for all uploaded images. :return: A response that contains status information for all images. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-image-list """ return await self._make_request("/inApps/v1/messaging/image/list", "GET", {}, None, GetImageListResponse, None) async def upload_message(self, message_identifier: UUID, upload_message_request_body: UploadMessageRequestBody): """ Upload a message to use for retention messaging. :param message_identifier: A UUID you provide to uniquely identify the message you upload. :param upload_message_request_body: The message text to upload. :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) async def delete_message(self, message_identifier: UUID): """ Delete a previously uploaded message. :param message_identifier: The identifier of the message to delete. :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) async def get_message_list(self) -> GetMessageListResponse: """ Get the message identifier and state of all uploaded messages. :return: A response that contains status information for all messages. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-message-list """ return await self._make_request("/inApps/v1/messaging/message/list", "GET", {}, None, GetMessageListResponse, None) async def configure_default_message(self, product_id: str, locale: str, default_configuration_request: DefaultConfigurationRequest): """ Configure a default message for a specific product in a specific locale. :param product_id: The product identifier for the default configuration. :param locale: The locale for the default configuration. :param default_configuration_request: The request body that includes the message identifier to configure as the default message. :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) async def delete_default_message(self, product_id: str, locale: str): """ Delete a default message for a product in a locale. :param product_id: The product ID of the default message configuration. :param locale: The locale of the default message configuration. :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) async def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurationResponse: """ Get the default message for a specific product in a specific locale. :param product_id: The product identifier for the default configuration. :param locale: The locale for the default configuration. :return: A response that contains the default configuration information. :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) async def configure_realtime_url(self, realtime_url_request: RealtimeUrlRequest): """ Configure the real-time URL for retention messaging. :param realtime_url_request: The request body that contains the real-time URL. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/configure-realtime-url """ await self._make_request("/inApps/v1/messaging/realtime/url", "PUT", {}, realtime_url_request, None, None) async def delete_realtime_url(self): """ Delete the real-time URL for retention messaging. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/delete-realtime-url """ await self._make_request("/inApps/v1/messaging/realtime/url", "DELETE", {}, None, None, None) async def get_realtime_url(self) -> RealtimeUrlResponse: """ Get the real-time URL for retention messaging. :return: A response that contains the real-time URL information. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/get-realtime-url """ return await self._make_request("/inApps/v1/messaging/realtime/url", "GET", {}, None, RealtimeUrlResponse, None) async def initiate_performance_test(self, performance_test_request: PerformanceTestRequest) -> PerformanceTestResponse: """ Initiates a performance test of your Get Retention Message endpoint in the sandbox environment. :param performance_test_request: The request body which specifies a transaction identifier of an In-App Purchase to use for this test. :return: The performance test response object. :raises APIException: If a response was returned indicating the request could not be processed :see: https://developer.apple.com/documentation/retentionmessaging/initiate-performance-test """ 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: """ Gets the results of the performance test for the specified identifier. :param request_id: The ID of the performance test to return, which you receive in the PerformanceTestResponse when you call Initiate Performance Test. :return: An object the API returns that describes the performance test results. :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) async def get_app_transaction_info(self, transaction_id: str) -> AppTransactionInfoResponse: """ Get a customer's app transaction information for your app. :param transaction_id Any originalTransactionId, transactionId or appTransactionId that belongs to the customer for your app. :return: A response that contains signed app transaction information for a customer. :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/{transaction_id}", "GET", {}, None, AppTransactionInfoResponse, None) ================================================ FILE: appstoreserverlibrary/jws_signature_creator.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. import datetime from typing import Any, Dict, Optional import base64 import json import jwt import uuid from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter class AdvancedCommerceAPIInAppRequest: def __init__(self): pass class JWSSignatureCreator: def __init__(self, audience: str, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): self._audience = audience self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend()) self._key_id = key_id self._issuer_id = issuer_id self._bundle_id = bundle_id def _create_signature(self, feature_specific_claims: Dict[str, Any]) -> str: claims = feature_specific_claims current_time = datetime.datetime.now(datetime.timezone.utc) claims["bid"] = self._bundle_id claims["iss"] = self._issuer_id claims["aud"] = self._audience claims["iat"] = current_time claims["nonce"] = str(uuid.uuid4()) return jwt.encode(claims, self._signing_key, algorithm="ES256", headers={"kid": self._key_id}, ) class PromotionalOfferV2SignatureCreator(JWSSignatureCreator): def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): """ Create a PromotionalOfferV2SignatureCreator :param signing_key: Your private key downloaded from App Store Connect :param key_id: Your private key ID from App Store Connect :param issuer_id: Your issuer ID from the Keys page in App Store Connect :param bundle_id: Your app's bundle ID """ 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: """ Create a promotional offer V2 signature. https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests :param product_id: The unique identifier of the product :param offer_identifier: The promotional offer identifier that you set up in App Store Connect :param transaction_id: The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app. This field is optional, but recommended. :return: The signed JWS. """ if product_id is None: raise ValueError("product_id cannot be null") if offer_identifier is None: raise ValueError("offer_identifier cannot be null") feature_specific_claims = { "productId": product_id, "offerIdentifier": offer_identifier } if transaction_id is not None: feature_specific_claims["transactionId"] = transaction_id return self._create_signature(feature_specific_claims=feature_specific_claims) class IntroductoryOfferEligibilitySignatureCreator(JWSSignatureCreator): def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): """ Create an IntroductoryOfferEligibilitySignatureCreator :param signing_key: Your private key downloaded from App Store Connect :param key_id: Your private key ID from App Store Connect :param issuer_id: Your issuer ID from the Keys page in App Store Connect :param bundle_id: Your app's bundle ID """ super().__init__(audience="introductory-offer-eligibility", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id) def create_signature(self, product_id: str, allow_introductory_offer: bool, transaction_id: str) -> str: """ Create an introductory offer eligibility signature. https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests :param product_id: The unique identifier of the product :param allow_introductory_offer: A boolean value that determines whether the customer is eligible for an introductory offer :param transaction_id: The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app. :return: The signed JWS. """ if product_id is None: raise ValueError("product_id cannot be null") if allow_introductory_offer is None: raise ValueError("allow_introductory_offer cannot be null") if transaction_id is None: raise ValueError("transaction_id cannot be null") feature_specific_claims = { "productId": product_id, "allowIntroductoryOffer": allow_introductory_offer, "transactionId": transaction_id } return self._create_signature(feature_specific_claims=feature_specific_claims) class AdvancedCommerceAPIInAppSignatureCreator(JWSSignatureCreator): def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str): """ Create an AdvancedCommerceAPIInAppSignatureCreator :param signing_key: Your private key downloaded from App Store Connect :param key_id: Your private key ID from App Store Connect :param issuer_id: Your issuer ID from the Keys page in App Store Connect :param bundle_id: Your app's bundle ID """ 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: """ Create an Advanced Commerce in-app signed request. https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests :param advanced_commerce_in_app_request: The request to be signed. :return: The signed JWS. """ if advanced_commerce_in_app_request is None: raise ValueError("advanced_commerce_in_app_request cannot be null") c = _get_cattrs_converter(type(advanced_commerce_in_app_request)) request = c.unstructure(advanced_commerce_in_app_request) encoded_request = base64.b64encode(json.dumps(request).encode(encoding='utf-8')).decode('utf-8') feature_specific_claims = { "request": encoded_request } return self._create_signature(feature_specific_claims=feature_specific_claims) ================================================ FILE: appstoreserverlibrary/models/AbstractAdvancedCommerceBaseItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from abc import ABC from attr import define import attr from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils from .LibraryUtility import AttrsRawValueAware @define class AbstractAdvancedCommerceBaseItem(AttrsRawValueAware, ABC): SKU: str = attr.ib(validator=AdvancedCommerceValidationUtils.sku_validator) """ The product identifier of an in-app purchase product you manage in your own system. https://developer.apple.com/documentation/advancedcommerceapi/sku """ ================================================ FILE: appstoreserverlibrary/models/AbstractAdvancedCommerceInAppRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from abc import ABC from attr import define import attr from .AdvancedCommerceRequest import AdvancedCommerceRequest from ..jws_signature_creator import AdvancedCommerceAPIInAppRequest @define class AbstractAdvancedCommerceInAppRequest(AdvancedCommerceRequest, AdvancedCommerceAPIInAppRequest, ABC): operation: str = attr.ib() version: str = attr.ib() ================================================ FILE: appstoreserverlibrary/models/AbstractAdvancedCommerceItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define import attr from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @define class AbstractAdvancedCommerceItem(AbstractAdvancedCommerceBaseItem): description: str = attr.ib(validator=AdvancedCommerceValidationUtils.description_validator) """ A string you provide that describes a SKU. https://developer.apple.com/documentation/advancedcommerceapi/description """ displayName: str = attr.ib(validator=AdvancedCommerceValidationUtils.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 """ ================================================ FILE: appstoreserverlibrary/models/AbstractAdvancedCommerceResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from abc import ABC from typing import Optional from attr import define import attr @define class AbstractAdvancedCommerceResponse(ABC): signedRenewalInfo: Optional[str] = attr.ib(default=None) """ Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo """ signedTransactionInfo: Optional[str] = attr.ib(default=None) """ Transaction information signed by the App Store, in JSON Web Signature (JWS) Compact Serialization format. https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ ================================================ FILE: appstoreserverlibrary/models/AccountTenure.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class AccountTenure(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The age of the customer's account. https://developer.apple.com/documentation/appstoreserverapi/accounttenure """ UNDECLARED = 0 ZERO_TO_THREE_DAYS = 1 THREE_DAYS_TO_TEN_DAYS = 2 TEN_DAYS_TO_THIRTY_DAYS = 3 THIRTY_DAYS_TO_NINETY_DAYS = 4 NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS = 5 ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS = 6 GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS = 7 ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceDescriptors.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define import attr from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @define class AdvancedCommerceDescriptors: """ The display name and description of a subscription product. https://developer.apple.com/documentation/advancedcommerceapi/descriptors """ description: str = attr.ib(validator=AdvancedCommerceValidationUtils.description_validator) """ A string you provide that describes a SKU. https://developer.apple.com/documentation/advancedcommerceapi/description """ displayName: str = attr.ib(validator=AdvancedCommerceValidationUtils.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 """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceEffective.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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" ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceOffer.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .AdvancedCommerceOfferPeriod import AdvancedCommerceOfferPeriod from .AdvancedCommerceOfferReason import AdvancedCommerceOfferReason from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils from .LibraryUtility import AttrsRawValueAware @define class AdvancedCommerceOffer(AttrsRawValueAware): """ A discount offer for an auto-renewable subscription. https://developer.apple.com/documentation/advancedcommerceapi/offer """ periodCount: int = attr.ib(validator=AdvancedCommerceValidationUtils.period_count_validator) """ The number of periods the offer is active. """ price: int = attr.ib() """ The offer price, in milliunits. https://developer.apple.com/documentation/advancedcommerceapi/price """ period: AdvancedCommerceOfferPeriod = AdvancedCommerceOfferPeriod.create_main_attr('rawPeriod', raw_required=True) """ The period of the offer. """ rawPeriod: str = AdvancedCommerceOfferPeriod.create_raw_attr('period', required=True) """ See period """ reason: AdvancedCommerceOfferReason = AdvancedCommerceOfferReason.create_main_attr('rawReason', raw_required=True) """ The reason for the offer. """ rawReason: str = AdvancedCommerceOfferReason.create_raw_attr('reason', required=True) """ See reason """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceOfferPeriod.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class AdvancedCommerceOfferPeriod(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The period of the offer. https://developer.apple.com/documentation/advancedcommerceapi/offer """ P3D = "P3D" P1W = "P1W" P2W = "P2W" P1M = "P1M" P2M = "P2M" P3M = "P3M" P6M = "P6M" P9M = "P9M" P1Y = "P1Y" ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceOfferReason.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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" ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeCreateRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest from .AdvancedCommerceOneTimeChargeItem import AdvancedCommerceOneTimeChargeItem @define class AdvancedCommerceOneTimeChargeCreateRequest(AbstractAdvancedCommerceInAppRequest): """ The request data your app provides when a customer purchases a one-time-charge product. https://developer.apple.com/documentation/advancedcommerceapi/onetimechargecreaterequest """ currency: str = attr.ib() """ The currency of the price of the product. https://developer.apple.com/documentation/advancedcommerceapi/currency """ item: AdvancedCommerceOneTimeChargeItem = attr.ib() """ The details of the product for purchase. https://developer.apple.com/documentation/advancedcommerceapi/onetimechargeitem """ taxCode: str = attr.ib() """ The tax code for this product. https://developer.apple.com/documentation/advancedcommerceapi/taxCode """ operation: str = attr.ib(init=False, default="CREATE_ONE_TIME_CHARGE", on_setattr=attr.setters.frozen) """ The constant that represents the operation of this request. """ version: str = attr.ib(init=False, default="1", on_setattr=attr.setters.frozen) """ The version number of the API. """ storefront: Optional[str] = attr.ib(default=None) """ The storefront for the transaction. https://developer.apple.com/documentation/advancedcommerceapi/storefront """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define import attr from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem @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() """ The price, in milliunits of the currency, of the one-time charge product. https://developer.apple.com/documentation/advancedcommerceapi/price """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommercePeriod.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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" P1M = "P1M" P2M = "P2M" P3M = "P3M" P6M = "P6M" P1Y = "P1Y" ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceReason.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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" ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceRefundReason.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class AdvancedCommerceRefundReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ A reason to request a refund. https://developer.apple.com/documentation/advancedcommerceapi/refundreason """ UNINTENDED_PURCHASE = "UNINTENDED_PURCHASE" FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE" UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE" LEGAL = "LEGAL" OTHER = "OTHER" MODIFY_ITEMS_REFUND = "MODIFY_ITEMS_REFUND" SIMULATE_REFUND_DECLINE = "SIMULATE_REFUND_DECLINE" ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceRefundType.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class AdvancedCommerceRefundType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ 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 """ FULL = "FULL" PRORATED = "PRORATED" CUSTOM = "CUSTOM" ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from abc import ABC from attr import define import attr from .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo from .LibraryUtility import AttrsRawValueAware @define class AdvancedCommerceRequest(AttrsRawValueAware, ABC): requestInfo: AdvancedCommerceRequestInfo = attr.ib() """ The metadata to include in server requests. https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceRequestInfo.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from uuid import UUID from attr import define import attr @define class AdvancedCommerceRequestInfo: """ The metadata to include in server requests. https://developer.apple.com/documentation/advancedcommerceapi/requestinfo """ requestReferenceId: UUID = attr.ib() """ 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. """ appAccountToken: Optional[UUID] = attr.ib(default=None) """ A UUID that represents an app account token, to associate with the transaction in the request. """ consistencyToken: Optional[str] = attr.ib(default=None) """ 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 """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceRequestRefundItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem from .AdvancedCommerceRefundReason import AdvancedCommerceRefundReason from .AdvancedCommerceRefundType import AdvancedCommerceRefundType @define class AdvancedCommerceRequestRefundItem(AbstractAdvancedCommerceBaseItem): """ 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 """ revoke: bool = attr.ib() refundReason: AdvancedCommerceRefundReason = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', raw_required=True) """ The reason for the refund request. https://developer.apple.com/documentation/advancedcommerceapi/refundreason """ rawRefundReason: str = AdvancedCommerceRefundReason.create_raw_attr('refundReason', required=True) """ See refundReason """ refundType: AdvancedCommerceRefundType = AdvancedCommerceRefundType.create_main_attr('rawRefundType', raw_required=True) """ The type of refund requested. """ rawRefundType: str = AdvancedCommerceRefundType.create_raw_attr('refundType', required=True) """ See refundType """ refundAmount: Optional[int] = attr.ib(default=None) """ The refund amount you're requesting for the SKU, in milliunits of the currency. https://developer.apple.com/documentation/advancedcommerceapi/refundamount """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceRequestRefundRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List from attr import define import attr from .AdvancedCommerceRequest import AdvancedCommerceRequest from .AdvancedCommerceRequestRefundItem import AdvancedCommerceRequestRefundItem from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @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=AdvancedCommerceValidationUtils.items_validator) """ https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem """ refundRiskingPreference: bool = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/refundriskingpreference """ currency: Optional[str] = attr.ib(default=None) """ The currency of the transaction. https://developer.apple.com/documentation/advancedcommerceapi/currency """ storefront: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/storefront """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceRequestRefundResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse class AdvancedCommerceRequestRefundResponse(AbstractAdvancedCommerceResponse): """ The response body for a transaction refund request. https://developer.apple.com/documentation/advancedcommerceapi/requestrefundresponse """ def __init__(self, signedTransactionInfo: str): super().__init__(signedRenewalInfo=None, signedTransactionInfo=signedTransactionInfo) ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .AdvancedCommerceRequest import AdvancedCommerceRequest @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) """ https://developer.apple.com/documentation/advancedcommerceapi/storefront """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse @define class AdvancedCommerceSubscriptionCancelResponse(AbstractAdvancedCommerceResponse): """ The response body for a successful subscription cancellation. https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncancelresponse """ pass ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataDescriptors.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .AdvancedCommerceEffective import AdvancedCommerceEffective from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @define class AdvancedCommerceSubscriptionChangeMetadataDescriptors(): """ 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) """ The string that determines when the metadata change goes into effect. https://developer.apple.com/documentation/advancedcommerceapi/effective """ rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True) """ See effective """ description: Optional[str] = attr.ib( default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.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(AdvancedCommerceValidationUtils.display_name_validator) ) """ The new display name for the subscription. https://developer.apple.com/documentation/advancedcommerceapi/displayname """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .AdvancedCommerceEffective import AdvancedCommerceEffective from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @define class AdvancedCommerceSubscriptionChangeMetadataItem(): """ 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=AdvancedCommerceValidationUtils.sku_validator) """ The original SKU of the item. """ effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True) """ The string that determines when the metadata change goes into effect. https://developer.apple.com/documentation/advancedcommerceapi/effective """ rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True) """ See effective """ description: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.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(AdvancedCommerceValidationUtils.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(AdvancedCommerceValidationUtils.sku_validator)) """ The new SKU of the item. https://developer.apple.com/documentation/advancedcommerceapi/sku """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List from attr import define import attr from .AdvancedCommerceRequest import AdvancedCommerceRequest from .AdvancedCommerceSubscriptionChangeMetadataDescriptors import AdvancedCommerceSubscriptionChangeMetadataDescriptors from .AdvancedCommerceSubscriptionChangeMetadataItem import AdvancedCommerceSubscriptionChangeMetadataItem from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @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) """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadatadescriptors """ items: Optional[List[AdvancedCommerceSubscriptionChangeMetadataItem]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.items_validator)) """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadataitem """ storefront: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/storefront """ taxCode: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/taxcode """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse class AdvancedCommerceSubscriptionChangeMetadataResponse(AbstractAdvancedCommerceResponse): """ The response body for a successful subscription metadata change. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadataresponse """ def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str): super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional import attr from attr import define from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem from .AdvancedCommerceOffer import AdvancedCommerceOffer @define class AdvancedCommerceSubscriptionCreateItem(AbstractAdvancedCommerceItem): """ The data that describes a subscription item. https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncreateitem """ price: int = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/price """ offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/offer """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List import attr from attr import define from .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest from .AdvancedCommerceDescriptors import AdvancedCommerceDescriptors from .AdvancedCommerceSubscriptionCreateItem import AdvancedCommerceSubscriptionCreateItem from .AdvancedCommercePeriod import AdvancedCommercePeriod @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 """ operation: str = attr.ib(init=False, default="CREATE_SUBSCRIPTION", on_setattr=attr.setters.frozen) version: str = attr.ib(init=False, default="1", on_setattr=attr.setters.frozen) currency: str = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/currency """ descriptors: AdvancedCommerceDescriptors = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/descriptors """ items: List[AdvancedCommerceSubscriptionCreateItem] = attr.ib() """ 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) """ https://developer.apple.com/documentation/advancedcommerceapi/period """ rawPeriod: str = AdvancedCommercePeriod.create_raw_attr('period', required=True) """ See period """ previousTransactionId: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/transactionid """ storefront: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/storefront """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateDescriptors.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from .AdvancedCommerceDescriptors import AdvancedCommerceDescriptors @define class AdvancedCommerceSubscriptionMigrateDescriptors(AdvancedCommerceDescriptors): """ The description and display name of the subscription to migrate to that you manage. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigratedescriptors """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem @define class AdvancedCommerceSubscriptionMigrateItem(AbstractAdvancedCommerceItem): """ The SKU, description, and display name to use for a migrated subscription item. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigrateitem """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRenewalItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem @define class AdvancedCommerceSubscriptionMigrateRenewalItem(AbstractAdvancedCommerceItem): """ The item information that replaces a migrated subscription item when the subscription renews. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigraterenewalitem """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List import attr from attr import define from .AdvancedCommerceRequest import AdvancedCommerceRequest from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils from .AdvancedCommerceSubscriptionMigrateDescriptors import AdvancedCommerceSubscriptionMigrateDescriptors from .AdvancedCommerceSubscriptionMigrateItem import AdvancedCommerceSubscriptionMigrateItem from .AdvancedCommerceSubscriptionMigrateRenewalItem import AdvancedCommerceSubscriptionMigrateRenewalItem @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. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigraterequest """ descriptors: AdvancedCommerceSubscriptionMigrateDescriptors = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigratedescriptors """ items: List[AdvancedCommerceSubscriptionMigrateItem] = attr.ib(validator=AdvancedCommerceValidationUtils.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() """ Your generic product ID for an auto-renewable subscription. https://developer.apple.com/documentation/advancedcommerceapi/targetproductid """ taxCode: str = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/taxcode """ renewalItems: Optional[List[AdvancedCommerceSubscriptionMigrateRenewalItem]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.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) """ https://developer.apple.com/documentation/advancedcommerceapi/storefront """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse @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) ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyAddItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional import attr from attr import define from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem from .AdvancedCommerceOffer import AdvancedCommerceOffer @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() """ https://developer.apple.com/documentation/advancedcommerceapi/price """ offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None) """ A discount offer for an auto-renewable subscription. https://developer.apple.com/documentation/advancedcommerceapi/offer """ proratedPrice: Optional[int] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/proratedprice """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyChangeItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional import attr from attr import define from .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem from .AdvancedCommerceEffective import AdvancedCommerceEffective from .AdvancedCommerceOffer import AdvancedCommerceOffer from .AdvancedCommerceReason import AdvancedCommerceReason from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @define class AdvancedCommerceSubscriptionModifyChangeItem(AbstractAdvancedCommerceItem): """ The data your app provides to change an item of an auto-renewable subscription. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem """ currentSKU: str = attr.ib(validator=AdvancedCommerceValidationUtils.sku_validator) """ https://developer.apple.com/documentation/advancedcommerceapi/sku """ price: int = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/price """ reason: AdvancedCommerceReason = AdvancedCommerceReason.create_main_attr('rawReason', raw_required=True) rawReason: str = AdvancedCommerceReason.create_raw_attr('reason', required=True) """ See reason """ effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True) """ https://developer.apple.com/documentation/advancedcommerceapi/effective """ rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True) """ See effective """ offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/offer """ proratedPrice: Optional[int] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/proratedprice """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyDescriptors.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional import attr from attr import define from .AdvancedCommerceEffective import AdvancedCommerceEffective from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @define class AdvancedCommerceSubscriptionModifyDescriptors(): """ 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) """ https://developer.apple.com/documentation/advancedcommerceapi/effective """ rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True) """ See effective """ description: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.description_validator)) """ https://developer.apple.com/documentation/advancedcommerceapi/description """ displayName: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.display_name_validator) ) """ https://developer.apple.com/documentation/advancedcommerceapi/displayname """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyInAppRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List import attr from attr import define from .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils from .AdvancedCommerceSubscriptionModifyAddItem import AdvancedCommerceSubscriptionModifyAddItem from .AdvancedCommerceSubscriptionModifyChangeItem import AdvancedCommerceSubscriptionModifyChangeItem from .AdvancedCommerceSubscriptionModifyDescriptors import AdvancedCommerceSubscriptionModifyDescriptors from .AdvancedCommerceSubscriptionModifyPeriodChange import AdvancedCommerceSubscriptionModifyPeriodChange from .AdvancedCommerceSubscriptionModifyRemoveItem import AdvancedCommerceSubscriptionModifyRemoveItem @define class AdvancedCommerceSubscriptionModifyInAppRequest(AbstractAdvancedCommerceInAppRequest): """ The request data your app provides to make changes to an auto-renewable subscription. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyinapprequest """ operation: str = attr.ib(default="MODIFY_SUBSCRIPTION", init=False, on_setattr=attr.setters.frozen) version: str = attr.ib(default="1", init=False, on_setattr=attr.setters.frozen) transactionId: str = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/transactionid """ retainBillingCycle: bool = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/retainbillingcycle """ addItems: Optional[List[AdvancedCommerceSubscriptionModifyAddItem]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.items_validator)) """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyadditem """ changeItems: Optional[List[AdvancedCommerceSubscriptionModifyChangeItem]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.items_validator)) """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem """ currency: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/currency """ descriptors: Optional[AdvancedCommerceSubscriptionModifyDescriptors] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifydescriptors """ periodChange: Optional[AdvancedCommerceSubscriptionModifyPeriodChange] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyperiodchange """ removeItems: Optional[List[AdvancedCommerceSubscriptionModifyRemoveItem]] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyremoveitem """ storefront: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/storefront """ taxCode: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/taxcode """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyPeriodChange.py ================================================ # 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): """ The data your app provides to change the period of an auto-renewable subscription. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyperiodchange """ effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True) """ https://developer.apple.com/documentation/advancedcommerceapi/effective """ rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True) """ See effective """ period: AdvancedCommercePeriod = AdvancedCommercePeriod.create_main_attr('rawPeriod', raw_required=True) """ https://developer.apple.com/documentation/advancedcommerceapi/period """ rawPeriod: str = AdvancedCommercePeriod.create_raw_attr('period', required=True) """ See period """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyRemoveItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem @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 """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. import attr from attr import define from typing import List, Optional from .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem from .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils @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() """ https://developer.apple.com/documentation/advancedcommerceapi/price """ dependentSKUs: Optional[List[str]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.dependent_skus_validator)) """ https://developer.apple.com/documentation/advancedcommerceapi/dependentsku """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional, List import attr from attr import define from .AdvancedCommerceRequest import AdvancedCommerceRequest from .AdvancedCommerceSubscriptionPriceChangeItem import AdvancedCommerceSubscriptionPriceChangeItem @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() """ 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) """ The currency of the prices. https://developer.apple.com/documentation/advancedcommerceapi/currency """ storefront: Optional[str] = attr.ib(default=None) """ The App Store storefront of the subscription. https://developer.apple.com/documentation/advancedcommerceapi/storefront """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse @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) ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateInAppRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from __future__ import annotations from typing import List, Optional import attr from appstoreserverlibrary.models.AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionReactivateItem import AdvancedCommerceSubscriptionReactivateItem @attr.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 """ operation: str = attr.ib(init=False, default="REACTIVATE_SUBSCRIPTION", on_setattr=attr.setters.frozen) version: str = attr.ib(init=False, default="1", on_setattr=attr.setters.frozen) transactionId: str = attr.ib() """ https://developer.apple.com/documentation/appstoreserverapi/transactionid """ items: Optional[List[AdvancedCommerceSubscriptionReactivateItem]] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateitem """ storefront: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/storefront """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateItem.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from appstoreserverlibrary.models.AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem @define class AdvancedCommerceSubscriptionReactivateItem(AbstractAdvancedCommerceBaseItem): """ An item in a subscription to reactive. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateitem """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from __future__ import annotations from typing import Optional import attr from appstoreserverlibrary.models.AdvancedCommerceRequest import AdvancedCommerceRequest from appstoreserverlibrary.models.AdvancedCommerceRefundReason import AdvancedCommerceRefundReason from appstoreserverlibrary.models.AdvancedCommerceRefundType import AdvancedCommerceRefundType @attr.define class AdvancedCommerceSubscriptionRevokeRequest(AdvancedCommerceRequest): """ The request body you provide to terminate a subscription and all its items immediately. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionrevokerequest """ refundRiskingPreference: bool = attr.ib() """ https://developer.apple.com/documentation/advancedcommerceapi/refundriskingpreference """ refundType: AdvancedCommerceRefundType = AdvancedCommerceRefundType.create_main_attr('rawRefundType', raw_required=True) rawRefundType: str = AdvancedCommerceRefundType.create_raw_attr('refundType', required=True) """ See refundType """ refundReason: AdvancedCommerceRefundReason = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', raw_required=True) """ https://developer.apple.com/documentation/advancedcommerceapi/refundreason """ rawRefundReason: str = AdvancedCommerceRefundReason.create_raw_attr('refundReason', required=True) """ See refundReason """ storefront: Optional[str] = attr.ib(default=None) """ https://developer.apple.com/documentation/advancedcommerceapi/storefront """ ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define from appstoreserverlibrary.models.AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse @define class AdvancedCommerceSubscriptionRevokeResponse(AbstractAdvancedCommerceResponse): """ The response body for a successful revoke-subscription request. https://developer.apple.com/documentation/advancedcommerceapi/subscriptionrevokeresponse """ def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str): super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo) ================================================ FILE: appstoreserverlibrary/models/AdvancedCommerceValidationUtils.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import TypeVar T = TypeVar('T') class AdvancedCommerceValidationUtils: MAXIMUM_DESCRIPTION_LENGTH = 45 MAXIMUM_DISPLAY_NAME_LENGTH = 30 MAXIMUM_SKU_LENGTH = 128 MIN_PERIOD = 1 MAX_PERIOD = 12 @staticmethod def description_validator(instance, attribute, value): """ Validates description is not None and does not exceed maximum length. Raises: ValueError: If description exceeds maximum length """ if len(value) > AdvancedCommerceValidationUtils.MAXIMUM_DESCRIPTION_LENGTH: raise ValueError( f"Description length cannot exceed " f"{AdvancedCommerceValidationUtils.MAXIMUM_DESCRIPTION_LENGTH} characters" ) @staticmethod 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 """ if len(value) > AdvancedCommerceValidationUtils.MAXIMUM_DISPLAY_NAME_LENGTH: raise ValueError( f"Display name length cannot exceed " f"{AdvancedCommerceValidationUtils.MAXIMUM_DISPLAY_NAME_LENGTH} characters" ) @staticmethod def sku_validator(instance, attribute, value): """ Validates SKU does not exceed maximum length. Raises: ValueError: If SKU exceeds maximum length """ if len(value) > AdvancedCommerceValidationUtils.MAXIMUM_SKU_LENGTH: raise ValueError( f"SKU length cannot exceed " f"{AdvancedCommerceValidationUtils.MAXIMUM_SKU_LENGTH} characters" ) @staticmethod 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 """ if (value < AdvancedCommerceValidationUtils.MIN_PERIOD or value > AdvancedCommerceValidationUtils.MAX_PERIOD): raise ValueError( f"Period count must be between " f"{AdvancedCommerceValidationUtils.MIN_PERIOD} and " f"{AdvancedCommerceValidationUtils.MAX_PERIOD}" ) @staticmethod def items_validator(instance, attribute, value): """ Validates a list of items is not None, not empty, and contains no None elements. Raises: ValueError: If list is empty or contains None elements """ if not value: raise ValueError("Items list cannot be empty") for i, item in enumerate(value): if item is None: raise ValueError(f"Item at index {i} in the list cannot be None") @staticmethod def dependent_skus_validator(instance, attribute, value): """ Validates that each SKU in the dependentSKUs list does not exceed maximum length. Raises: ValueError: If any SKU exceeds maximum length """ for sku in value: if len(sku) > AdvancedCommerceValidationUtils.MAXIMUM_SKU_LENGTH: raise ValueError( f"SKU length cannot exceed " f"{AdvancedCommerceValidationUtils.MAXIMUM_SKU_LENGTH} characters" ) ================================================ FILE: appstoreserverlibrary/models/AlternateProduct.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from uuid import UUID from attr import define import attr @define class AlternateProduct: """ A switch-plan message and product ID you provide in a real-time response to your Get Retention Message endpoint. https://developer.apple.com/documentation/retentionmessaging/alternateproduct """ messageIdentifier: Optional[UUID] = attr.ib(default=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) """ The product identifier of the subscription the retention message suggests for your customer to switch to. https://developer.apple.com/documentation/retentionmessaging/productid """ ================================================ FILE: appstoreserverlibrary/models/AppData.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .Environment import Environment from .LibraryUtility import AttrsRawValueAware @define class AppData(AttrsRawValueAware): """ The object that contains the app metadata and signed app transaction information. https://developer.apple.com/documentation/appstoreservernotifications/appdata """ appAppleId: Optional[int] = attr.ib(default=None) """ 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) """ The bundle identifier of the app. https://developer.apple.com/documentation/appstoreservernotifications/bundleid """ environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') """ 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 """ signedAppTransactionInfo: Optional[str] = attr.ib(default=None) """ App transaction information signed by the App Store, in JSON Web Signature (JWS) format. https://developer.apple.com/documentation/appstoreservernotifications/jwsapptransaction """ ================================================ FILE: appstoreserverlibrary/models/AppTransaction.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .LibraryUtility import AttrsRawValueAware from .Environment import Environment from .PurchasePlatform import PurchasePlatform @define class AppTransaction(AttrsRawValueAware): """ A decoded payload that contains app transaction information. https://developer.apple.com/documentation/storekit/apptransaction https://developer.apple.com/documentation/appstoreserverapi/jwsapptransactiondecodedpayload """ receiptType: Optional[Environment] = Environment.create_main_attr('rawReceiptType') """ The date that the App Store signed the JWS app transaction. https://developer.apple.com/documentation/appstoreserverapi/environment """ rawReceiptType: Optional[str] = Environment.create_raw_attr('receiptType') """ See receiptType """ 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 """ bundleId: Optional[str] = attr.ib(default=None) """ The bundle identifier that the app transaction applies to. https://developer.apple.com/documentation/appstoreserverapi/bundleid """ applicationVersion: Optional[str] = attr.ib(default=None) """ The app version that the app transaction applies to. https://developer.apple.com/documentation/storekit/apptransaction/appversion """ versionExternalIdentifier: Optional[int] = attr.ib(default=None) """ The version external identifier of the app https://developer.apple.com/documentation/storekit/apptransaction/appversionid """ receiptCreationDate: Optional[int] = attr.ib(default=None) """ The date that the App Store signed the JWS app transaction. https://developer.apple.com/documentation/appstoreserverapi/receiptcreationdate """ originalPurchaseDate: Optional[int] = attr.ib(default=None) """ 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) """ The app version that the user originally purchased from the App Store. https://developer.apple.com/documentation/appstoreserverapi/originalapplicationversion """ deviceVerification: Optional[str] = attr.ib(default=None) """ 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 """ deviceVerificationNonce: Optional[str] = attr.ib(default=None) """ The UUID used to compute the device verification value. https://developer.apple.com/documentation/storekit/apptransaction/deviceverificationnonce """ preorderDate: Optional[int] = attr.ib(default=None) """ 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 """ appTransactionId: Optional[str] = attr.ib(default=None) """ The unique identifier of the app download transaction. https://developer.apple.com/documentation/appstoreserverapi/apptransactionid """ originalPlatform: Optional[PurchasePlatform] = PurchasePlatform.create_main_attr('rawOriginalPlatform') """ The platform on which the customer originally purchased the app. https://developer.apple.com/documentation/appstoreserverapi/originalplatform """ rawOriginalPlatform: Optional[str] = PurchasePlatform.create_raw_attr('originalPlatform') """ See originalPlatform """ ================================================ FILE: appstoreserverlibrary/models/AppTransactionInfoResponse.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define 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) """ A customer’s app transaction information, signed by Apple, in JSON Web Signature (JWS) format. https://developer.apple.com/documentation/appstoreserverapi/jwsapptransaction """ ================================================ FILE: appstoreserverlibrary/models/AutoRenewStatus.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class AutoRenewStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The renewal status for an auto-renewable subscription. https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus """ OFF = 0 ON = 1 ================================================ FILE: appstoreserverlibrary/models/BulletPoint.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from uuid import UUID from attr import define import attr @define class BulletPoint: """ The text and its bullet-point image to include in a retention message’s bulleted list. https://developer.apple.com/documentation/retentionmessaging/bulletpoint """ text: str = attr.ib(validator=attr.validators.max_len(66)) """ The text of the individual bullet point. https://developer.apple.com/documentation/retentionmessaging/bulletpointtext """ imageIdentifier: UUID = attr.ib() """ 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)) """ The alternative text you provide for the corresponding image of the bullet point. https://developer.apple.com/documentation/retentionmessaging/alttext """ ================================================ FILE: appstoreserverlibrary/models/CheckTestNotificationResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import List, Optional from attr import define import attr from .SendAttemptItem import SendAttemptItem @define class CheckTestNotificationResponse: """ A response that contains the contents of the test notification sent by the App Store server and the result from your server. https://developer.apple.com/documentation/appstoreserverapi/checktestnotificationresponse """ signedPayload: Optional[str] = attr.ib(default=None) """ 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 """ sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None) """ 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 """ ================================================ FILE: appstoreserverlibrary/models/ConsumptionRequest.py ================================================ # 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 .RefundPreference import RefundPreference @define class ConsumptionRequest(AttrsRawValueAware): """ The request body that contains consumption information for an In-App Purchase. https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest """ customerConsented: bool = attr.ib() """ A Boolean value that indicates whether the customer consented to provide consumption data to the App Store. https://developer.apple.com/documentation/appstoreserverapi/customerconsented """ sampleContentProvided: bool = attr.ib() """ 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. https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided """ deliveryStatus: Optional[DeliveryStatus] = DeliveryStatus.create_main_attr('rawDeliveryStatus', raw_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 """ consumptionPercentage: Optional[int] = attr.ib(default=None) """ An integer that indicates the percentage, in milliunits, of the In-App Purchase the customer consumed. https://developer.apple.com/documentation/appstoreserverapi/consumptionpercentage """ rawDeliveryStatus: str = DeliveryStatus.create_raw_attr('deliveryStatus', required=True) """ See deliveryStatus """ refundPreference: Optional[RefundPreference] = RefundPreference.create_main_attr('rawRefundPreference') """ A value that indicates your preferred outcome for the refund request. https://developer.apple.com/documentation/appstoreserverapi/refundpreference """ rawRefundPreference: Optional[str] = RefundPreference.create_raw_attr('refundPreference') """ See refundPreference """ ================================================ FILE: appstoreserverlibrary/models/ConsumptionRequestReason.py ================================================ # Copyright (c) 2024 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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" FULFILLMENT_ISSUE = "FULFILLMENT_ISSUE" UNSATISFIED_WITH_PURCHASE = "UNSATISFIED_WITH_PURCHASE" LEGAL = "LEGAL" OTHER = "OTHER" ================================================ FILE: appstoreserverlibrary/models/ConsumptionRequestV1.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .AccountTenure import AccountTenure from .ConsumptionStatus import ConsumptionStatus from .DeliveryStatusV1 import DeliveryStatusV1 from .LibraryUtility import AttrsRawValueAware from .LifetimeDollarsPurchased import LifetimeDollarsPurchased from .LifetimeDollarsRefunded import LifetimeDollarsRefunded from .Platform import Platform from .PlayTime import PlayTime from .RefundPreferenceV1 import RefundPreferenceV1 from .UserStatus import UserStatus @define class ConsumptionRequestV1(AttrsRawValueAware): """ The request body containing consumption information. .. deprecated:: Use :class:`ConsumptionRequest` instead. https://developer.apple.com/documentation/appstoreserverapi/consumptionrequestv1 """ customerConsented: Optional[bool] = attr.ib(default=None) """ A Boolean value that indicates whether the customer consented to provide consumption data to the App Store. https://developer.apple.com/documentation/appstoreserverapi/customerconsented """ consumptionStatus: Optional[ConsumptionStatus] = ConsumptionStatus.create_main_attr('rawConsumptionStatus') """ A value that indicates the extent to which the customer consumed the in-app purchase. https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus """ rawConsumptionStatus: Optional[int] = ConsumptionStatus.create_raw_attr('consumptionStatus') """ See consumptionStatus """ platform: Optional[Platform] = Platform.create_main_attr('rawPlatform') """ A value that indicates the platform on which the customer consumed the in-app purchase. https://developer.apple.com/documentation/appstoreserverapi/platform """ rawPlatform: Optional[int] = Platform.create_raw_attr('platform') """ See platform """ sampleContentProvided: Optional[bool] = attr.ib(default=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. https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided """ deliveryStatus: Optional[DeliveryStatusV1] = DeliveryStatusV1.create_main_attr('rawDeliveryStatus') """ A value that indicates whether the app successfully delivered an in-app purchase that works properly. https://developer.apple.com/documentation/appstoreserverapi/deliverystatus """ rawDeliveryStatus: Optional[int] = DeliveryStatusV1.create_raw_attr('deliveryStatus') """ See deliveryStatus """ appAccountToken: Optional[str] = attr.ib(default=None) """ 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 """ accountTenure: Optional[AccountTenure] = AccountTenure.create_main_attr('rawAccountTenure') """ The age of the customer's account. https://developer.apple.com/documentation/appstoreserverapi/accounttenure """ rawAccountTenure: Optional[int] = AccountTenure.create_raw_attr('accountTenure') """ See accountTenure """ playTime: Optional[PlayTime] = PlayTime.create_main_attr('rawPlayTime') """ A value that indicates the amount of time that the customer used the app. https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest """ rawPlayTime: Optional[int] = PlayTime.create_raw_attr('playTime') """ See playTime """ lifetimeDollarsRefunded: Optional[LifetimeDollarsRefunded] = LifetimeDollarsRefunded.create_main_attr('rawLifetimeDollarsRefunded') """ A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded """ rawLifetimeDollarsRefunded: Optional[int] = LifetimeDollarsRefunded.create_raw_attr('lifetimeDollarsRefunded') """ See lifetimeDollarsRefunded """ lifetimeDollarsPurchased: Optional[LifetimeDollarsPurchased] = LifetimeDollarsPurchased.create_main_attr('rawLifetimeDollarsPurchased') """ 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/lifetimedollarspurchased """ rawLifetimeDollarsPurchased: Optional[int] = LifetimeDollarsPurchased.create_raw_attr('lifetimeDollarsPurchased') """ See lifetimeDollarsPurchased """ userStatus: Optional[UserStatus] = UserStatus.create_main_attr('rawUserStatus') """ The status of the customer's account. https://developer.apple.com/documentation/appstoreserverapi/userstatus """ rawUserStatus: Optional[int] = UserStatus.create_raw_attr('userStatus') """ See userStatus """ refundPreference: Optional[RefundPreferenceV1] = RefundPreferenceV1.create_main_attr('rawRefundPreference') """ 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 """ rawRefundPreference: Optional[int] = RefundPreferenceV1.create_raw_attr('refundPreference') """ See refundPreference """ ================================================ FILE: appstoreserverlibrary/models/ConsumptionStatus.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class ConsumptionStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ A value that indicates the extent to which the customer consumed the in-app purchase. https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus """ UNDECLARED = 0 NOT_CONSUMED = 1 PARTIALLY_CONSUMED = 2 FULLY_CONSUMED = 3 ================================================ FILE: appstoreserverlibrary/models/Data.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .ConsumptionRequestReason import ConsumptionRequestReason from .Environment import Environment from .Status import Status from .LibraryUtility import AttrsRawValueAware @define class Data(AttrsRawValueAware): """ The app metadata and the signed renewal and transaction information. https://developer.apple.com/documentation/appstoreservernotifications/data """ environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') """ 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) """ The unique identifier of an app in the App Store. https://developer.apple.com/documentation/appstoreservernotifications/appappleid """ bundleId: Optional[str] = attr.ib(default=None) """ The bundle identifier of an app. https://developer.apple.com/documentation/appstoreserverapi/bundleid """ bundleVersion: Optional[str] = attr.ib(default=None) """ The version of the build that identifies an iteration of the bundle. https://developer.apple.com/documentation/appstoreservernotifications/bundleversion """ signedTransactionInfo: Optional[str] = attr.ib(default=None) """ Transaction information signed by the App Store, in JSON Web Signature (JWS) format. https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ signedRenewalInfo: Optional[str] = attr.ib(default=None) """ Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo """ status: Optional[Status] = Status.create_main_attr('rawStatus') """ The status of an auto-renewable subscription as of the signedDate in the responseBodyV2DecodedPayload. https://developer.apple.com/documentation/appstoreservernotifications/status """ rawStatus: Optional[int] = Status.create_raw_attr('status') """ See status """ consumptionRequestReason: Optional[ConsumptionRequestReason] = ConsumptionRequestReason.create_main_attr('rawConsumptionRequestReason') """ The reason the customer requested the refund. https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason """ rawConsumptionRequestReason: Optional[str] = ConsumptionRequestReason.create_raw_attr('consumptionRequestReason') """ See consumptionRequestReason """ ================================================ FILE: appstoreserverlibrary/models/DecodedRealtimeRequestBody.py ================================================ # 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 @define class DecodedRealtimeRequestBody(AttrsRawValueAware): """ The decoded request body the App Store sends to your server to request a real-time retention message. https://developer.apple.com/documentation/retentionmessaging/decodedrealtimerequestbody """ originalTransactionId: str = attr.ib() """ The original transaction identifier of the customer's subscription. https://developer.apple.com/documentation/retentionmessaging/originaltransactionid """ appAppleId: int = attr.ib() """ The unique identifier of the app in the App Store. https://developer.apple.com/documentation/retentionmessaging/appappleid """ productId: str = attr.ib() """ The unique identifier of the auto-renewable subscription. https://developer.apple.com/documentation/retentionmessaging/productid """ userLocale: str = attr.ib() """ The device's locale. https://developer.apple.com/documentation/retentionmessaging/locale """ requestIdentifier: UUID = attr.ib() """ 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) """ The server environment, either sandbox or production. https://developer.apple.com/documentation/retentionmessaging/environment """ rawEnvironment: str = Environment.create_raw_attr('environment', required=True) """ See environment """ ================================================ FILE: appstoreserverlibrary/models/DefaultConfigurationRequest.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from uuid import UUID from attr import define import attr @define class DefaultConfigurationRequest: """ The request body that contains the default configuration information. https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationrequest """ messageIdentifier: Optional[UUID] = attr.ib(default=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 """ ================================================ FILE: appstoreserverlibrary/models/DefaultConfigurationResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from uuid import UUID from attr import define import attr @define class DefaultConfigurationResponse: """ The response body that contains the default configuration information. https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationresponse """ messageIdentifier: UUID = attr.ib() """ The message identifier of the retention message you configured as a default. https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ ================================================ FILE: appstoreserverlibrary/models/DeliveryStatus.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class DeliveryStatus(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ 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" UNDELIVERED_QUALITY_ISSUE = "UNDELIVERED_QUALITY_ISSUE" UNDELIVERED_WRONG_ITEM = "UNDELIVERED_WRONG_ITEM" UNDELIVERED_SERVER_OUTAGE = "UNDELIVERED_SERVER_OUTAGE" UNDELIVERED_OTHER = "UNDELIVERED_OTHER" ================================================ FILE: appstoreserverlibrary/models/DeliveryStatusV1.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class DeliveryStatusV1(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ A value that indicates whether the app successfully delivered an in-app purchase that works properly. .. deprecated:: Use :class:`DeliveryStatus` instead. https://developer.apple.com/documentation/appstoreserverapi/deliverystatusv1 """ DELIVERED_AND_WORKING_PROPERLY = 0 DID_NOT_DELIVER_DUE_TO_QUALITY_ISSUE = 1 DELIVERED_WRONG_ITEM = 2 DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE = 3 DID_NOT_DELIVER_DUE_TO_IN_GAME_CURRENCY_CHANGE = 4 DID_NOT_DELIVER_FOR_OTHER_REASON = 5 ================================================ FILE: appstoreserverlibrary/models/Environment.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class Environment(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The server environment, either sandbox or production. https://developer.apple.com/documentation/appstoreserverapi/environment """ SANDBOX = "Sandbox" PRODUCTION = "Production" XCODE = "Xcode" LOCAL_TESTING = "LocalTesting" # Used for unit testing ================================================ FILE: appstoreserverlibrary/models/ExpirationIntent.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class ExpirationIntent(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The reason an auto-renewable subscription expired. https://developer.apple.com/documentation/appstoreserverapi/expirationintent """ CUSTOMER_CANCELLED = 1 BILLING_ERROR = 2 CUSTOMER_DID_NOT_CONSENT_TO_PRICE_INCREASE = 3 PRODUCT_NOT_AVAILABLE = 4 OTHER = 5 ================================================ FILE: appstoreserverlibrary/models/ExtendReasonCode.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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 CUSTOMER_SATISFACTION = 1 OTHER = 2 SERVICE_ISSUE_OR_OUTAGE = 3 ================================================ FILE: appstoreserverlibrary/models/ExtendRenewalDateRequest.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .ExtendReasonCode import ExtendReasonCode @define class ExtendRenewalDateRequest: """ 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) """ 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) """ The reason code for the subscription date extension https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode """ requestIdentifier: Optional[str] = attr.ib(default=None) """ A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. https://developer.apple.com/documentation/appstoreserverapi/requestidentifier """ ================================================ FILE: appstoreserverlibrary/models/ExtendRenewalDateResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define 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) """ The original transaction identifier of a purchase. https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid """ webOrderLineItemId: Optional[str] = attr.ib(default=None) """ The unique identifier of subscription-purchase events across devices, including renewals. https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid """ success: Optional[bool] = attr.ib(default=None) """ 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) """ The new subscription expiration date for a subscription-renewal extension. https://developer.apple.com/documentation/appstoreserverapi/effectivedate """ ================================================ FILE: appstoreserverlibrary/models/ExternalPurchaseToken.py ================================================ # Copyright (c) 2024 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .LibraryUtility import AttrsRawValueAware @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) """ The field of an external purchase token that uniquely identifies the token. https://developer.apple.com/documentation/appstoreservernotifications/externalpurchaseid """ tokenCreationDate: Optional[int] = attr.ib(default=None) """ The field of an external purchase token that contains the UNIX date, in milliseconds, when the system created the token. https://developer.apple.com/documentation/appstoreservernotifications/tokencreationdate """ appAppleId: Optional[int] = attr.ib(default=None) """ The unique identifier of an app in the App Store. https://developer.apple.com/documentation/appstoreservernotifications/appappleid """ bundleId: Optional[str] = attr.ib(default=None) """ The bundle identifier of an app. https://developer.apple.com/documentation/appstoreservernotifications/bundleid """ ================================================ FILE: appstoreserverlibrary/models/FirstSendAttemptResult.py ================================================ # 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" ================================================ FILE: appstoreserverlibrary/models/GetImageListResponse.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional, List from attr import define import attr from .GetImageListResponseItem import GetImageListResponseItem @define class GetImageListResponse: """ A response that contains status information for all images. https://developer.apple.com/documentation/retentionmessaging/getimagelistresponse """ imageIdentifiers: Optional[List[GetImageListResponseItem]] = attr.ib(default=None) """ An array of all image identifiers and their image state. https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem """ ================================================ FILE: appstoreserverlibrary/models/GetImageListResponseItem.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from uuid import UUID from attr import define import attr from .ImageSize import ImageSize from .ImageState import ImageState from .LibraryUtility import AttrsRawValueAware @define class GetImageListResponseItem(AttrsRawValueAware): """ An image identifier and state information for an image. https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem """ imageIdentifier: Optional[UUID] = attr.ib(default=None) """ The identifier of the image. https://developer.apple.com/documentation/retentionmessaging/imageidentifier """ imageState: Optional[ImageState] = ImageState.create_main_attr('rawImageState') """ The current state of the image. https://developer.apple.com/documentation/retentionmessaging/imagestate """ rawImageState: Optional[str] = ImageState.create_raw_attr('imageState') """ See imageState """ imageSize: Optional[ImageSize] = ImageSize.create_main_attr('rawImageSize') """ The size of the image. https://developer.apple.com/documentation/retentionmessaging/imagesize """ rawImageSize: Optional[str] = ImageSize.create_raw_attr('imageSize') """ See imageSize """ ================================================ FILE: appstoreserverlibrary/models/GetMessageListResponse.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional, List from attr import define import attr from .GetMessageListResponseItem import GetMessageListResponseItem @define class GetMessageListResponse: """ A response that contains status information for all messages. https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponse """ messageIdentifiers: Optional[List[GetMessageListResponseItem]] = attr.ib(default=None) """ An array of all message identifiers and their message state. https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem """ ================================================ FILE: appstoreserverlibrary/models/GetMessageListResponseItem.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from uuid import UUID from attr import define import attr from .MessageState import MessageState from .LibraryUtility import AttrsRawValueAware @define class GetMessageListResponseItem(AttrsRawValueAware): """ A message identifier and status information for a message. https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem """ messageIdentifier: Optional[UUID] = attr.ib(default=None) """ The identifier of the message. https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ messageState: Optional[MessageState] = MessageState.create_main_attr('rawMessageState') """ The current state of the message. https://developer.apple.com/documentation/retentionmessaging/messageState """ rawMessageState: Optional[str] = MessageState.create_raw_attr('messageState') """ See messageState """ ================================================ FILE: appstoreserverlibrary/models/HeaderPosition.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class HeaderPosition(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The position where the header text appears in a message. https://developer.apple.com/documentation/retentionmessaging/headerposition """ ABOVE_BODY = "ABOVE_BODY" ABOVE_IMAGE = "ABOVE_IMAGE" ================================================ FILE: appstoreserverlibrary/models/HistoryResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from attr import define from typing import List, Optional import attr from .Environment import Environment from .LibraryUtility import AttrsRawValueAware @define class HistoryResponse(AttrsRawValueAware): """ 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 """ 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 """ 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) """ 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') """ The server environment in which you're making the request, whether sandbox or production. https://developer.apple.com/documentation/appstoreserverapi/environment """ rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') """ See environment """ signedTransactions: Optional[List[str]] = attr.ib(default=None) """ An array of in-app purchase transactions for the customer, signed by Apple, in JSON Web Signature format. https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ ================================================ FILE: appstoreserverlibrary/models/ImageSize.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class ImageSize(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The size of an image. https://developer.apple.com/documentation/retentionmessaging/imagesize """ FULL_SIZE = "FULL_SIZE" BULLET_POINT = "BULLET_POINT" ================================================ FILE: appstoreserverlibrary/models/ImageState.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class ImageState(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The approval state of an image. https://developer.apple.com/documentation/retentionmessaging/imagestate """ PENDING = "PENDING" APPROVED = "APPROVED" REJECTED = "REJECTED" ================================================ FILE: appstoreserverlibrary/models/InAppOwnershipType.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class InAppOwnershipType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The relationship of the user with the family-shared purchase to which they have access. https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype """ FAMILY_SHARED = "FAMILY_SHARED" PURCHASED = "PURCHASED" ================================================ FILE: appstoreserverlibrary/models/JWSRenewalInfoDecodedPayload.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import List, Optional from attr import define import attr from .AutoRenewStatus import AutoRenewStatus from .Environment import Environment from .ExpirationIntent import ExpirationIntent from .LibraryUtility import AttrsRawValueAware from .OfferType import OfferType from .PriceIncreaseStatus import PriceIncreaseStatus from .OfferDiscountType import OfferDiscountType @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 """ rawExpirationIntent: Optional[int] = ExpirationIntent.create_raw_attr('expirationIntent') """ See expirationIntent """ originalTransactionId: Optional[str] = attr.ib(default=None) """ The original transaction identifier of a purchase. https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid """ 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 """ productId: Optional[str] = attr.ib(default=None) """ The unique identifier for the product, that you create in App Store Connect. https://developer.apple.com/documentation/appstoreserverapi/productid """ autoRenewStatus: Optional[AutoRenewStatus] = AutoRenewStatus.create_main_attr('rawAutoRenewStatus') """ The renewal status of the auto-renewable subscription. https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus """ rawAutoRenewStatus: Optional[int] = AutoRenewStatus.create_raw_attr('autoRenewStatus') """ See autoRenewStatus """ isInBillingRetryPeriod: Optional[bool] = attr.ib(default=None) """ A Boolean value that indicates whether the App Store is attempting to automatically renew an expired subscription. https://developer.apple.com/documentation/appstoreserverapi/isinbillingretryperiod """ priceIncreaseStatus: Optional[PriceIncreaseStatus] = PriceIncreaseStatus.create_main_attr('rawPriceIncreaseStatus') """ The status that indicates whether the auto-renewable subscription is subject to a price increase. https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus """ rawPriceIncreaseStatus: Optional[int] = PriceIncreaseStatus.create_raw_attr('priceIncreaseStatus') """ See priceIncreaseStatus """ gracePeriodExpiresDate: Optional[int] = attr.ib(default=None) """ The time when the billing grace period for subscription renewals expires. https://developer.apple.com/documentation/appstoreserverapi/graceperiodexpiresdate """ offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType') """ The type of subscription offer. https://developer.apple.com/documentation/appstoreserverapi/offertype """ rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType') """ See offerType """ offerIdentifier: Optional[str] = attr.ib(default=None) """ The offer code or the promotional offer identifier. https://developer.apple.com/documentation/appstoreserverapi/offeridentifier """ signedDate: Optional[int] = attr.ib(default=None) """ The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. https://developer.apple.com/documentation/appstoreserverapi/signeddate """ environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') """ The server environment, either sandbox or production. https://developer.apple.com/documentation/appstoreserverapi/environment """ rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') """ See environment """ recentSubscriptionStartDate: Optional[int] = attr.ib(default=None) """ 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 """ renewalDate: Optional[int] = attr.ib(default=None) """ The UNIX time, in milliseconds, that the most recent auto-renewable subscription purchase expires. https://developer.apple.com/documentation/appstoreserverapi/renewaldate """ currency: Optional[str] = attr.ib(default=None) """ The currency code for the renewalPrice of the subscription. https://developer.apple.com/documentation/appstoreserverapi/currency """ renewalPrice: Optional[int] = attr.ib(default=None) """ The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period. https://developer.apple.com/documentation/appstoreserverapi/renewalprice """ offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType') """ The payment mode you configure for the offer. https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype """ rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType') """ See offerDiscountType """ eligibleWinBackOfferIds: Optional[List[str]] = attr.ib(default=None) """ 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 """ appAccountToken: Optional[str] = attr.ib(default=None) """ 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 """ appTransactionId: Optional[str] = attr.ib(default=None) """ The unique identifier of the app download transaction. https://developer.apple.com/documentation/appstoreserverapi/appTransactionId """ offerPeriod: Optional[str] = attr.ib(default=None) """ The duration of the offer. https://developer.apple.com/documentation/appstoreserverapi/offerPeriod """ ================================================ FILE: appstoreserverlibrary/models/JWSTransactionDecodedPayload.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .OfferDiscountType import OfferDiscountType from .Environment import Environment from .InAppOwnershipType import InAppOwnershipType from .LibraryUtility import AttrsRawValueAware from .OfferType import OfferType from .RevocationReason import RevocationReason from .RevocationType import RevocationType from .TransactionReason import TransactionReason from .Type import Type @define class JWSTransactionDecodedPayload(AttrsRawValueAware): """ A decoded payload containing transaction information. https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload """ originalTransactionId: Optional[str] = attr.ib(default=None) """ The original transaction identifier of a purchase. https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid """ transactionId: Optional[str] = attr.ib(default=None) """ 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 """ webOrderLineItemId: Optional[str] = attr.ib(default=None) """ The unique identifier of subscription-purchase events across devices, including renewals. https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid """ bundleId: Optional[str] = attr.ib(default=None) """ The bundle identifier of an app. https://developer.apple.com/documentation/appstoreserverapi/bundleid """ productId: Optional[str] = attr.ib(default=None) """ The unique identifier for the product, that you create in App Store Connect. https://developer.apple.com/documentation/appstoreserverapi/productid """ subscriptionGroupIdentifier: Optional[str] = attr.ib(default=None) """ The identifier of the subscription group that the subscription belongs to. https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier """ purchaseDate: Optional[int] = attr.ib(default=None) """ 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 """ originalPurchaseDate: Optional[int] = attr.ib(default=None) """ The purchase date of the transaction associated with the original transaction identifier. https://developer.apple.com/documentation/appstoreserverapi/originalpurchasedate """ expiresDate: Optional[int] = attr.ib(default=None) """ The UNIX time, in milliseconds, an auto-renewable subscription expires or renews. https://developer.apple.com/documentation/appstoreserverapi/expiresdate """ quantity: Optional[int] = attr.ib(default=None) """ The number of consumable products purchased. https://developer.apple.com/documentation/appstoreserverapi/quantity """ type: Optional[Type] = Type.create_main_attr('rawType') """ The type of the in-app purchase. https://developer.apple.com/documentation/appstoreserverapi/type """ rawType: Optional[str] = Type.create_raw_attr('type') """ See type """ appAccountToken: Optional[str] = attr.ib(default=None) """ 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 """ inAppOwnershipType: Optional[InAppOwnershipType] = InAppOwnershipType.create_main_attr('rawInAppOwnershipType') """ 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 """ rawInAppOwnershipType: Optional[str] = InAppOwnershipType.create_raw_attr('inAppOwnershipType') """ See inAppOwnershipType """ signedDate: Optional[int] = attr.ib(default=None) """ The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. https://developer.apple.com/documentation/appstoreserverapi/signeddate """ revocationReason: Optional[RevocationReason] = RevocationReason.create_main_attr('rawRevocationReason') """ The reason that the App Store refunded the transaction or revoked it from Family Sharing. https://developer.apple.com/documentation/appstoreserverapi/revocationreason """ rawRevocationReason: Optional[int] = RevocationReason.create_raw_attr('revocationReason') """ See revocationReason """ revocationDate: Optional[int] = attr.ib(default=None) """ 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) """ The Boolean value that indicates whether the user upgraded to another subscription. https://developer.apple.com/documentation/appstoreserverapi/isupgraded """ offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType') """ A value that represents the promotional offer type. https://developer.apple.com/documentation/appstoreserverapi/offertype """ rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType') """ See offerType """ offerIdentifier: Optional[str] = attr.ib(default=None) """ The identifier that contains the offer code or the promotional offer identifier. https://developer.apple.com/documentation/appstoreserverapi/offeridentifier """ environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment') """ The server environment, either sandbox or production. https://developer.apple.com/documentation/appstoreserverapi/environment """ rawEnvironment: Optional[str] = Environment.create_raw_attr('environment') """ See environment """ storefront: Optional[str] = attr.ib(default=None) """ 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 """ storefrontId: Optional[str] = attr.ib(default=None) """ An Apple-defined value that uniquely identifies the App Store storefront associated with the purchase. https://developer.apple.com/documentation/appstoreserverapi/storefrontid """ transactionReason: Optional[TransactionReason] = TransactionReason.create_main_attr('rawTransactionReason') """ 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 """ rawTransactionReason: Optional[str] = TransactionReason.create_raw_attr('transactionReason') """ See transactionReason """ currency: Optional[str] = attr.ib(default=None) """ The three-letter ISO 4217 currency code for the price of the product. https://developer.apple.com/documentation/appstoreserverapi/currency """ price: Optional[int] = attr.ib(default=None) """ 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 """ offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType') """ The payment mode you configure for the offer. https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype """ rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType') """ See offerDiscountType """ appTransactionId: Optional[str] = attr.ib(default=None) """ The unique identifier of the app download transaction. https://developer.apple.com/documentation/appstoreserverapi/appTransactionId """ offerPeriod: Optional[str] = attr.ib(default=None) """ The duration of the offer. https://developer.apple.com/documentation/appstoreserverapi/offerPeriod """ revocationType: Optional[RevocationType] = RevocationType.create_main_attr('rawRevocationType') """ The type of the refund or revocation that applies to the transaction. https://developer.apple.com/documentation/appstoreservernotifications/revocationtype """ rawRevocationType: Optional[str] = RevocationType.create_raw_attr('revocationType') """ See revocationType """ revocationPercentage: Optional[int] = attr.ib(default=None) """ The percentage, in milliunits, of the transaction that the App Store has refunded or revoked. https://developer.apple.com/documentation/appstoreservernotifications/revocationpercentage """ ================================================ FILE: appstoreserverlibrary/models/LastTransactionsItem.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .LibraryUtility import AttrsRawValueAware from .Status import Status @define 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') """ The status of the auto-renewable subscription. https://developer.apple.com/documentation/appstoreserverapi/status """ rawStatus: Optional[int] = Status.create_raw_attr('status') """ See status """ originalTransactionId: Optional[str] = attr.ib(default=None) """ The original transaction identifier of a purchase. https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid """ signedTransactionInfo: Optional[str] = attr.ib(default=None) """ Transaction information signed by the App Store, in JSON Web Signature (JWS) format. https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ signedRenewalInfo: Optional[str] = attr.ib(default=None) """ Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format. https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo """ ================================================ FILE: appstoreserverlibrary/models/LibraryUtility.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import EnumMeta from functools import lru_cache from typing import Any, List, Type, TypeVar 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 T = TypeVar('T') metadata_key = 'correspondingFieldName' metadata_type_key = 'typeOfField' metadata_required_key = 'requiredField' class AppStoreServerLibraryEnumMeta(EnumMeta): def __contains__(c, val): try: c(val) except ValueError: 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): 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") 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'}) def create_raw_attr(c, field_name: str, required: bool = False) -> Any: def value_set(self, _: Attribute, value: str): if required and value is None: raise ValueError(f"raw{field_name[0].upper() + field_name[1:]} cannot be None") newValue = c(value) if value in c else None if newValue != getattr(self, field_name): 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): main_value = getattr(instance, field_name) if main_value is not None: return main_value.value raise ValueError(f"Either {field_name} or raw{field_name[0].upper() + field_name[1:]} must be provided") return ib(default=Factory(factory, takes_self=True), kw_only=True, on_setattr=value_set, validator=validate_not_none, metadata={metadata_key: field_name, metadata_type_key: 'raw', metadata_required_key: True}) else: return ib(default=None, kw_only=True, on_setattr=value_set, metadata={metadata_key: field_name, metadata_type_key: 'raw', metadata_required_key: False}) class AttrsRawValueAware: def __attrs_post_init__(self): attr_fields: List[Attribute] = fields(type(self)) for attribute in attr_fields: if metadata_type_key not in attribute.metadata or attribute.metadata[metadata_type_key] != 'raw': continue field: str = attribute.metadata.get(metadata_key) rawField = 'raw' + field[0].upper() + field[1:] rawValue = getattr(self, rawField) value = getattr(self, field) if rawValue is not None: setattr(self, rawField, rawValue) elif value is not None: setattr(self, field, value) @lru_cache(maxsize=None) def _get_cattrs_converter(destination_class: Type[T]) -> cattrs.Converter: c = 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) # Need a function here because it must be a lambda based on cl, which is not always destination_class def make_overrides(cl): attributes: List[Attribute] = fields(cl) cattrs_overrides = {} # Use omit_if_default to prevent null fields from being serialized to JSON for attribute in attributes: if metadata_type_key in attribute.metadata: matching_name: str = attribute.metadata[metadata_key] if attribute.metadata[metadata_type_key] == 'raw': cattrs_overrides[matching_name] = override(omit=True) raw_field = 'raw' + matching_name[0].upper() + matching_name[1:] if attribute.metadata.get(metadata_required_key, False): cattrs_overrides[raw_field] = override(rename=matching_name) else: cattrs_overrides[raw_field] = override(rename=matching_name, omit_if_default=True) elif attribute.default is None and attribute.name not in cattrs_overrides: cattrs_overrides[attribute.name] = override(omit_if_default=True) return cattrs_overrides c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, **make_overrides(cl))) c.register_unstructure_hook_factory(has, lambda cl: make_dict_unstructure_fn(cl, c, **make_overrides(cl))) return c ================================================ FILE: appstoreserverlibrary/models/LifetimeDollarsPurchased.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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. https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased """ UNDECLARED = 0 ZERO_DOLLARS = 1 ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2 FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3 ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4 FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5 ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6 TWO_THOUSAND_DOLLARS_OR_GREATER = 7 ================================================ FILE: appstoreserverlibrary/models/LifetimeDollarsRefunded.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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 ZERO_DOLLARS = 1 ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2 FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3 ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4 FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5 ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6 TWO_THOUSAND_DOLLARS_OR_GREATER = 7 ================================================ FILE: appstoreserverlibrary/models/MassExtendRenewalDateRequest.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from attr import define from typing import List, Optional import attr from .ExtendReasonCode import ExtendReasonCode @define class MassExtendRenewalDateRequest: """ 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) """ 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) """ The reason code for the subscription-renewal-date extension. https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode """ requestIdentifier: Optional[str] = attr.ib(default=None) """ 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) """ 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 """ productId: Optional[str] = attr.ib(default=None) """ The unique identifier for the product, that you create in App Store Connect. https://developer.apple.com/documentation/appstoreserverapi/productid """ ================================================ FILE: appstoreserverlibrary/models/MassExtendRenewalDateResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define 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) """ A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. https://developer.apple.com/documentation/appstoreserverapi/requestidentifier """ ================================================ FILE: appstoreserverlibrary/models/MassExtendRenewalDateStatusResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define 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) """ A string that contains a unique identifier you provide to track each subscription-renewal-date extension request. https://developer.apple.com/documentation/appstoreserverapi/requestidentifier """ complete: Optional[bool] = attr.ib(default=None) """ A Boolean value that indicates whether the App Store completed the request to extend a subscription renewal date to active subscribers. https://developer.apple.com/documentation/appstoreserverapi/complete """ completeDate: Optional[int] = attr.ib(default=None) """ The UNIX time, in milliseconds, that the App Store completes a request to extend a subscription renewal date for eligible subscribers. https://developer.apple.com/documentation/appstoreserverapi/completedate """ succeededCount: Optional[int] = attr.ib(default=None) """ The count of subscriptions that successfully receive a subscription-renewal-date extension. https://developer.apple.com/documentation/appstoreserverapi/succeededcount """ failedCount: Optional[int] = attr.ib(default=None) """ The count of subscriptions that fail to receive a subscription-renewal-date extension. https://developer.apple.com/documentation/appstoreserverapi/failedcount """ ================================================ FILE: appstoreserverlibrary/models/Message.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from uuid import UUID from attr import define import attr @define class Message: """ A message identifier you provide in a real-time response to your Get Retention Message endpoint. https://developer.apple.com/documentation/retentionmessaging/message """ messageIdentifier: Optional[UUID] = attr.ib(default=None) """ The identifier of the message to display to the customer. https://developer.apple.com/documentation/retentionmessaging/messageidentifier """ ================================================ FILE: appstoreserverlibrary/models/MessageState.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class MessageState(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The approval state of the message. https://developer.apple.com/documentation/retentionmessaging/messagestate """ PENDING = "PENDING" APPROVED = "APPROVED" REJECTED = "REJECTED" ================================================ FILE: appstoreserverlibrary/models/NotificationHistoryRequest.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .NotificationTypeV2 import NotificationTypeV2 from .Subtype import Subtype @define class NotificationHistoryRequest: """ The request body for notification history. https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryrequest """ startDate: Optional[int] = attr.ib(default=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. https://developer.apple.com/documentation/appstoreserverapi/startdate """ endDate: Optional[int] = attr.ib(default=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. https://developer.apple.com/documentation/appstoreserverapi/enddate """ notificationType: Optional[NotificationTypeV2] = attr.ib(default=None) """ 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. https://developer.apple.com/documentation/appstoreserverapi/notificationtype """ notificationSubtype: Optional[Subtype] = attr.ib(default=None) """ 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. https://developer.apple.com/documentation/appstoreserverapi/notificationsubtype """ transactionId: Optional[str] = attr.ib(default=None) """ 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 """ onlyFailures: Optional[bool] = attr.ib(default=None) """ 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. https://developer.apple.com/documentation/appstoreserverapi/onlyfailures """ ================================================ FILE: appstoreserverlibrary/models/NotificationHistoryResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional, List from attr import define import attr from .NotificationHistoryResponseItem import NotificationHistoryResponseItem @define 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) """ 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 """ 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 """ notificationHistory: Optional[List[NotificationHistoryResponseItem]] = attr.ib(default=None) """ An array of App Store server notification history records. https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem """ ================================================ FILE: appstoreserverlibrary/models/NotificationHistoryResponseItem.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional, List from attr import define import attr from .SendAttemptItem import SendAttemptItem @define class NotificationHistoryResponseItem: """ 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) """ 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 """ sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None) """ 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 """ ================================================ FILE: appstoreserverlibrary/models/NotificationTypeV2.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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. https://developer.apple.com/documentation/appstoreservernotifications/notificationtype """ SUBSCRIBED = "SUBSCRIBED" DID_CHANGE_RENEWAL_PREF = "DID_CHANGE_RENEWAL_PREF" DID_CHANGE_RENEWAL_STATUS = "DID_CHANGE_RENEWAL_STATUS" OFFER_REDEEMED = "OFFER_REDEEMED" DID_RENEW = "DID_RENEW" EXPIRED = "EXPIRED" DID_FAIL_TO_RENEW = "DID_FAIL_TO_RENEW" GRACE_PERIOD_EXPIRED = "GRACE_PERIOD_EXPIRED" PRICE_INCREASE = "PRICE_INCREASE" REFUND = "REFUND" REFUND_DECLINED = "REFUND_DECLINED" CONSUMPTION_REQUEST = "CONSUMPTION_REQUEST" RENEWAL_EXTENDED = "RENEWAL_EXTENDED" REVOKE = "REVOKE" TEST = "TEST" RENEWAL_EXTENSION = "RENEWAL_EXTENSION" REFUND_REVERSED = "REFUND_REVERSED" EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN" ONE_TIME_CHARGE = "ONE_TIME_CHARGE" RESCIND_CONSENT = "RESCIND_CONSENT" ================================================ FILE: appstoreserverlibrary/models/OfferDiscountType.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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" PAY_AS_YOU_GO = "PAY_AS_YOU_GO" PAY_UP_FRONT = "PAY_UP_FRONT" ONE_TIME = "ONE_TIME" ================================================ FILE: appstoreserverlibrary/models/OfferType.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class OfferType(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The type of offer. https://developer.apple.com/documentation/appstoreserverapi/offertype """ INTRODUCTORY_OFFER = 1 PROMOTIONAL_OFFER = 2 OFFER_CODE = 3 WIN_BACK_OFFER = 4 ================================================ FILE: appstoreserverlibrary/models/OrderLookupResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from attr import define from typing import List, Optional import attr from .LibraryUtility import AttrsRawValueAware from .OrderLookupStatus import OrderLookupStatus @define 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') """ The status that indicates whether the order ID is valid. https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus """ rawStatus: Optional[int] = OrderLookupStatus.create_raw_attr('status') """ See status """ signedTransactions: Optional[List[str]] = attr.ib(default=None) """ An array of in-app purchase transactions that are part of order, signed by Apple, in JSON Web Signature format. https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ ================================================ FILE: appstoreserverlibrary/models/OrderLookupStatus.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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 INVALID = 1 ================================================ FILE: appstoreserverlibrary/models/PerformanceTestConfig.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define class PerformanceTestConfig: """ An object that enumerates the test configuration parameters. https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ maxConcurrentRequests: Optional[int] = attr.ib(default=None) """ The maximum number of concurrent requests the API allows. https://developer.apple.com/documentation/retentionmessaging/maxconcurrentrequests """ totalRequests: Optional[int] = attr.ib(default=None) """ The total number of requests to make during the test. https://developer.apple.com/documentation/retentionmessaging/totalrequests """ totalDuration: Optional[int] = attr.ib(default=None) """ The total duration of the test in milliseconds. https://developer.apple.com/documentation/retentionmessaging/totalduration """ responseTimeThreshold: Optional[int] = attr.ib(default=None) """ 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/responsetimethreshold """ successRateThreshold: Optional[int] = attr.ib(default=None) """ The success rate threshold percentage. https://developer.apple.com/documentation/retentionmessaging/successratethreshold """ ================================================ FILE: appstoreserverlibrary/models/PerformanceTestRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define import attr @define class PerformanceTestRequest: """ The request object you provide for a performance test that contains an original transaction identifier. https://developer.apple.com/documentation/retentionmessaging/performancetestrequest """ originalTransactionId: str = attr.ib() """ The original transaction identifier of an In-App Purchase you initiate in the sandbox environment, to use as the purchase for this test. https://developer.apple.com/documentation/retentionmessaging/originaltransactionid """ ================================================ FILE: appstoreserverlibrary/models/PerformanceTestResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .PerformanceTestConfig import PerformanceTestConfig @define class PerformanceTestResponse: """ The performance test response object. https://developer.apple.com/documentation/retentionmessaging/performancetestresponse """ config: Optional[PerformanceTestConfig] = attr.ib(default=None) """ The performance test configuration object. https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ requestId: Optional[str] = attr.ib(default=None) """ The performance test request identifier. https://developer.apple.com/documentation/retentionmessaging/requestid """ ================================================ FILE: appstoreserverlibrary/models/PerformanceTestResponseTimes.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define class PerformanceTestResponseTimes: """ An object that describes test response times. https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ average: Optional[int] = attr.ib(default=None) """ Average response time in milliseconds. https://developer.apple.com/documentation/retentionmessaging/average """ p50: Optional[int] = attr.ib(default=None) """ The 50th percentile response time in milliseconds. https://developer.apple.com/documentation/retentionmessaging/p50 """ p90: Optional[int] = attr.ib(default=None) """ The 90th percentile response time in milliseconds. https://developer.apple.com/documentation/retentionmessaging/p90 """ p95: Optional[int] = attr.ib(default=None) """ The 95th percentile response time in milliseconds. https://developer.apple.com/documentation/retentionmessaging/p95 """ p99: Optional[int] = attr.ib(default=None) """ The 99th percentile response time in milliseconds. https://developer.apple.com/documentation/retentionmessaging/p99 """ ================================================ FILE: appstoreserverlibrary/models/PerformanceTestResultResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Dict, Optional from attr import define, Attribute import attr from .LibraryUtility import AttrsRawValueAware, metadata_key, metadata_type_key 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 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]]): 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 if new_typed != getattr(self, 'failures'): object.__setattr__(self, 'failures', new_typed) return value @define class PerformanceTestResultResponse(AttrsRawValueAware): """ An object the API returns that describes the performance test results. https://developer.apple.com/documentation/retentionmessaging/performancetestresultresponse """ config: Optional[PerformanceTestConfig] = attr.ib(default=None) """ A PerformanceTestConfig object that enumerates the test parameters. https://developer.apple.com/documentation/retentionmessaging/performancetestconfig """ target: Optional[str] = attr.ib(default=None) """ The target URL for the performance test. https://developer.apple.com/documentation/retentionmessaging/target """ result: Optional[PerformanceTestStatus] = PerformanceTestStatus.create_main_attr('rawResult') """ A PerformanceTestStatus object that describes the overall result of the test. https://developer.apple.com/documentation/retentionmessaging/performanceteststatus """ rawResult: Optional[str] = PerformanceTestStatus.create_raw_attr('result') """ See result """ successRate: Optional[int] = attr.ib(default=None) """ An integer that describes he success rate percentage of the performance test. https://developer.apple.com/documentation/retentionmessaging/successrate """ numPending: Optional[int] = attr.ib(default=None) """ An integer that describes the number of pending requests in the performance test. https://developer.apple.com/documentation/retentionmessaging/numpending """ responseTimes: Optional[PerformanceTestResponseTimes] = attr.ib(default=None) """ A PerformanceTestResponseTimes object that enumerates the response times measured during the test. https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes """ failures: Optional[Dict[SendAttemptResult, int]] = attr.ib(default=None, on_setattr=_failures_value_set, metadata={metadata_key: 'rawFailures', metadata_type_key: 'main'}) """ 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/failures """ 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'}) """ See failures """ ================================================ FILE: appstoreserverlibrary/models/PerformanceTestStatus.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class PerformanceTestStatus(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ The status of the performance test. https://developer.apple.com/documentation/retentionmessaging/performanceteststatus """ PENDING = "PENDING" PASS = "PASS" FAIL = "FAIL" ================================================ FILE: appstoreserverlibrary/models/Platform.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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 APPLE = 1 NON_APPLE = 2 ================================================ FILE: appstoreserverlibrary/models/PlayTime.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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 ZERO_TO_FIVE_MINUTES = 1 FIVE_TO_SIXTY_MINUTES = 2 ONE_TO_SIX_HOURS = 3 SIX_HOURS_TO_TWENTY_FOUR_HOURS = 4 ONE_DAY_TO_FOUR_DAYS = 5 FOUR_DAYS_TO_SIXTEEN_DAYS = 6 OVER_SIXTEEN_DAYS = 7 ================================================ FILE: appstoreserverlibrary/models/PriceIncreaseStatus.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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 CUSTOMER_CONSENTED_OR_WAS_NOTIFIED_WITHOUT_NEEDING_CONSENT = 1 ================================================ FILE: appstoreserverlibrary/models/PromotionalOffer.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from uuid import UUID from attr import define import attr from .PromotionalOfferSignatureV1 import PromotionalOfferSignatureV1 @define class PromotionalOffer: """ A promotional offer and message you provide in a real-time response to your Get Retention Message endpoint. https://developer.apple.com/documentation/retentionmessaging/promotionaloffer """ messageIdentifier: Optional[UUID] = attr.ib(default=None) """ 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) """ The promotional offer signature in V2 format. https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev2 """ promotionalOfferSignatureV1: Optional[PromotionalOfferSignatureV1] = attr.ib(default=None) """ The promotional offer signature in V1 format. https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ ================================================ FILE: appstoreserverlibrary/models/PromotionalOfferSignatureV1.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from uuid import UUID from attr import define import attr @define class PromotionalOfferSignatureV1: """ The promotional offer signature you generate using an earlier signature version. https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1 """ encodedSignature: str = attr.ib() """ The Base64-encoded cryptographic signature you generate using the offer parameters. """ productId: str = attr.ib() """ The subscription's product identifier. https://developer.apple.com/documentation/retentionmessaging/productid """ nonce: UUID = attr.ib() """ A one-time-use UUID antireplay value you generate. """ timestamp: int = attr.ib() """ The UNIX time, in milliseconds, when you generate the signature. """ keyId: str = attr.ib() """ A string that identifies the private key you use to generate the signature. """ offerIdentifier: str = attr.ib() """ The subscription offer identifier that you set up in App Store Connect. """ appAccountToken: Optional[UUID] = attr.ib(default=None) """ A UUID that you provide to associate with the transaction if the customer accepts the promotional offer. """ ================================================ FILE: appstoreserverlibrary/models/PurchasePlatform.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta class PurchasePlatform(str, Enum, metaclass=AppStoreServerLibraryEnumMeta): """ Values that represent Apple platforms. https://developer.apple.com/documentation/storekit/appstore/platform """ IOS = "iOS" MAC_OS = "macOS" TV_OS = "tvOS" VISION_OS = "visionOS" ================================================ FILE: appstoreserverlibrary/models/RealtimeRequestBody.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional, Dict from attr import define import attr @define class RealtimeRequestBody: """ The request body the App Store server sends to your Get Retention Message endpoint. https://developer.apple.com/documentation/retentionmessaging/realtimerequestbody """ signedPayload: Optional[str] = attr.ib(default=None) """ The payload in JSON Web Signature (JWS) format, signed by the App Store. https://developer.apple.com/documentation/retentionmessaging/signedpayload """ ================================================ FILE: appstoreserverlibrary/models/RealtimeResponseBody.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .Message import Message from .AlternateProduct import AlternateProduct from .PromotionalOffer import PromotionalOffer @define class RealtimeResponseBody: """ A response you provide to choose, in real time, a retention message the system displays to the customer. https://developer.apple.com/documentation/retentionmessaging/realtimeresponsebody """ message: Optional[Message] = attr.ib(default=None) """ 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) """ A retention message with a switch-plan option. https://developer.apple.com/documentation/retentionmessaging/alternateproduct """ promotionalOffer: Optional[PromotionalOffer] = attr.ib(default=None) """ A retention message that includes a promotional offer. https://developer.apple.com/documentation/retentionmessaging/promotionaloffer """ ================================================ FILE: appstoreserverlibrary/models/RealtimeUrlRequest.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from attr import define import attr @define class RealtimeUrlRequest: """ The request body for configuring the URL of your Get Retention Message endpoint. https://developer.apple.com/documentation/retentionmessaging/realtimeurlrequest """ realtimeURL: str = attr.ib(validator=attr.validators.max_len(256)) """ A string that contains the URL of your Get Retention Message endpoint for configuration. https://developer.apple.com/documentation/retentionmessaging/realtimeurl """ ================================================ FILE: appstoreserverlibrary/models/RealtimeUrlResponse.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define class RealtimeUrlResponse: """ The response body that contains the URL for your Get Retention Message endpoint. https://developer.apple.com/documentation/retentionmessaging/realtimeurlresponse """ realtimeURL: Optional[str] = attr.ib() """ A string that contains the URL you provided for your Get Retention Message endpoint. https://developer.apple.com/documentation/retentionmessaging/realtimeurl """ ================================================ FILE: appstoreserverlibrary/models/RefundHistoryResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from attr import define from typing import List, Optional import attr @define 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) """ 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 """ 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 """ 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 """ ================================================ FILE: appstoreserverlibrary/models/RefundPreference.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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. https://developer.apple.com/documentation/appstoreserverapi/refundpreference """ DECLINE = "DECLINE" GRANT_FULL = "GRANT_FULL" GRANT_PRORATED = "GRANT_PRORATED" ================================================ FILE: appstoreserverlibrary/models/RefundPreferenceV1.py ================================================ # Copyright (c) 2024 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class RefundPreferenceV1(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ A value that indicates your preferred outcome for the refund request. .. deprecated:: Use :class:`RefundPreference` instead. https://developer.apple.com/documentation/appstoreserverapi/refundpreferencev1 """ UNDECLARED = 0 PREFER_GRANT = 1 PREFER_DECLINE = 2 NO_PREFERENCE = 3 ================================================ FILE: appstoreserverlibrary/models/ResponseBodyV2.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define class ResponseBodyV2: """ The response body the App Store sends in a version 2 server notification. https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2 """ signedPayload: Optional[str] = attr.ib(default=None) """ 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 """ ================================================ FILE: appstoreserverlibrary/models/ResponseBodyV2DecodedPayload.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .AppData import AppData from .Data import Data from .ExternalPurchaseToken import ExternalPurchaseToken from .LibraryUtility import AttrsRawValueAware from .NotificationTypeV2 import NotificationTypeV2 from .Subtype import Subtype from .Summary import Summary @define class ResponseBodyV2DecodedPayload(AttrsRawValueAware): """ A decoded payload containing the version 2 notification data. https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload """ notificationType: Optional[NotificationTypeV2] = NotificationTypeV2.create_main_attr('rawNotificationType') """ The in-app purchase event for which the App Store sends this version 2 notification. https://developer.apple.com/documentation/appstoreservernotifications/notificationtype """ rawNotificationType: Optional[str] = NotificationTypeV2.create_raw_attr('notificationType') """ See notificationType """ subtype: Optional[Subtype] = Subtype.create_main_attr('rawSubtype') """ Additional information that identifies the notification event. The subtype field is present only for specific version 2 notifications. https://developer.apple.com/documentation/appstoreservernotifications/subtype """ rawSubtype: Optional[str] = Subtype.create_raw_attr('subtype') """ See subtype """ notificationUUID: Optional[str] = attr.ib(default=None) """ A unique identifier for the notification. https://developer.apple.com/documentation/appstoreservernotifications/notificationuuid """ data: Optional[Data] = attr.ib(default=None) """ 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 """ version: Optional[str] = attr.ib(default=None) """ A string that indicates the notification's App Store Server Notifications version number. https://developer.apple.com/documentation/appstoreservernotifications/version """ signedDate: Optional[int] = attr.ib(default=None) """ The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data. https://developer.apple.com/documentation/appstoreserverapi/signeddate """ summary: Optional[Summary] = attr.ib(default=None) """ 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 """ externalPurchaseToken: Optional[ExternalPurchaseToken] = attr.ib(default=None) """ 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 """ appData: Optional[AppData] = attr.ib(default=None) """ The object that contains the app metadata and signed app transaction information. This field appears when the notificationType is RESCIND_CONSENT. https://developer.apple.com/documentation/appstoreservernotifications/appdata """ ================================================ FILE: appstoreserverlibrary/models/RevocationReason.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class RevocationReason(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The reason for a refunded transaction. https://developer.apple.com/documentation/appstoreserverapi/revocationreason """ REFUNDED_DUE_TO_ISSUE = 1 REFUNDED_FOR_OTHER_REASON = 0 ================================================ FILE: appstoreserverlibrary/models/RevocationType.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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 """ REFUND_FULL = "REFUND_FULL" REFUND_PRORATED = "REFUND_PRORATED" FAMILY_REVOKE = "FAMILY_REVOKE" ================================================ FILE: appstoreserverlibrary/models/SendAttemptItem.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr from .LibraryUtility import AttrsRawValueAware from .SendAttemptResult import SendAttemptResult @define 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) """ The date the App Store server attempts to send a notification. https://developer.apple.com/documentation/appstoreserverapi/attemptdate """ sendAttemptResult: Optional[SendAttemptResult] = SendAttemptResult.create_main_attr('rawSendAttemptResult') """ 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 """ ================================================ FILE: appstoreserverlibrary/models/SendAttemptResult.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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" 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" ================================================ FILE: appstoreserverlibrary/models/SendTestNotificationResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define class SendTestNotificationResponse: """ A response that contains the test notification token. https://developer.apple.com/documentation/appstoreserverapi/sendtestnotificationresponse """ testNotificationToken: Optional[str] = attr.ib(default=None) """ A unique identifier for a notification test that the App Store server sends to your server. https://developer.apple.com/documentation/appstoreserverapi/testnotificationtoken """ ================================================ FILE: appstoreserverlibrary/models/Status.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class Status(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The status of an auto-renewable subscription. https://developer.apple.com/documentation/appstoreserverapi/status """ ACTIVE = 1 EXPIRED = 2 BILLING_RETRY = 3 BILLING_GRACE_PERIOD = 4 REVOKED = 5 ================================================ FILE: appstoreserverlibrary/models/StatusResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional, List from attr import define import attr from .Environment import Environment from .LibraryUtility import AttrsRawValueAware from .SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem @define class StatusResponse(AttrsRawValueAware): """ 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') """ 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) """ The unique identifier of an app in the App Store. https://developer.apple.com/documentation/appstoreservernotifications/appappleid """ data: Optional[List[SubscriptionGroupIdentifierItem]] = attr.ib(default=None) """ 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 """ ================================================ FILE: appstoreserverlibrary/models/SubscriptionGroupIdentifierItem.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional, List from attr import define import attr from .LastTransactionsItem import LastTransactionsItem @define 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) """ The identifier of the subscription group that the subscription belongs to. https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier """ lastTransactions: Optional[List[LastTransactionsItem]] = attr.ib(default=None) """ 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 """ ================================================ FILE: appstoreserverlibrary/models/Subtype.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum from .LibraryUtility import AppStoreServerLibraryEnumMeta 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" RESUBSCRIBE = "RESUBSCRIBE" DOWNGRADE = "DOWNGRADE" UPGRADE = "UPGRADE" AUTO_RENEW_ENABLED = "AUTO_RENEW_ENABLED" AUTO_RENEW_DISABLED = "AUTO_RENEW_DISABLED" VOLUNTARY = "VOLUNTARY" BILLING_RETRY = "BILLING_RETRY" PRICE_INCREASE = "PRICE_INCREASE" GRACE_PERIOD = "GRACE_PERIOD" PENDING = "PENDING" ACCEPTED = "ACCEPTED" BILLING_RECOVERY = "BILLING_RECOVERY" PRODUCT_NOT_FOR_SALE = "PRODUCT_NOT_FOR_SALE" SUMMARY = "SUMMARY" FAILURE = "FAILURE" UNREPORTED = "UNREPORTED" ================================================ FILE: appstoreserverlibrary/models/Summary.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from attr import define from typing import List, Optional import attr from .Environment import Environment from .LibraryUtility import AttrsRawValueAware @define 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') """ 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) """ The unique identifier of an app in the App Store. https://developer.apple.com/documentation/appstoreservernotifications/appappleid """ bundleId: Optional[str] = attr.ib(default=None) """ The bundle identifier of an app. https://developer.apple.com/documentation/appstoreserverapi/bundleid """ productId: Optional[str] = attr.ib(default=None) """ The unique identifier for the product, that you create in App Store Connect. https://developer.apple.com/documentation/appstoreserverapi/productid """ requestIdentifier: Optional[str] = attr.ib(default=None) """ 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) """ 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 """ succeededCount: Optional[int] = attr.ib(default=None) """ The count of subscriptions that successfully receive a subscription-renewal-date extension. https://developer.apple.com/documentation/appstoreserverapi/succeededcount """ failedCount: Optional[int] = attr.ib(default=None) """ The count of subscriptions that fail to receive a subscription-renewal-date extension. https://developer.apple.com/documentation/appstoreserverapi/failedcount """ ================================================ FILE: appstoreserverlibrary/models/TransactionHistoryRequest.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import Enum from typing import List, Optional import attr from .InAppOwnershipType import InAppOwnershipType class ProductType(str, Enum): AUTO_RENEWABLE = "AUTO_RENEWABLE" NON_RENEWABLE = "NON_RENEWABLE" CONSUMABLE = "CONSUMABLE" NON_CONSUMABLE = "NON_CONSUMABLE" class Order(str, Enum): ASCENDING = "ASCENDING" DESCENDING = "DESCENDING" @attr.define class TransactionHistoryRequest: startDate: Optional[int] = attr.ib(default=None) """ 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) """ 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) """ 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) """ 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) """ 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) """ 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) """ 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) """ 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. """ ================================================ FILE: appstoreserverlibrary/models/TransactionInfoResponse.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional from attr import define import attr @define 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) """ A customer’s in-app purchase transaction, signed by Apple, in JSON Web Signature (JWS) format. https://developer.apple.com/documentation/appstoreserverapi/jwstransaction """ ================================================ FILE: appstoreserverlibrary/models/TransactionReason.py ================================================ # 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" RENEWAL = "RENEWAL" ================================================ FILE: appstoreserverlibrary/models/Type.py ================================================ # 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. https://developer.apple.com/documentation/appstoreserverapi/type """ AUTO_RENEWABLE_SUBSCRIPTION = "Auto-Renewable Subscription" NON_CONSUMABLE = "Non-Consumable" CONSUMABLE = "Consumable" NON_RENEWING_SUBSCRIPTION ="Non-Renewing Subscription" ================================================ FILE: appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from attr import define import attr @define class UpdateAppAccountTokenRequest: """ The request body that contains an app account token value. https://developer.apple.com/documentation/appstoreserverapi/updateappaccounttokenrequest """ appAccountToken: str = attr.ib() """ The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction. https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken """ ================================================ FILE: appstoreserverlibrary/models/UploadMessageImage.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from uuid import UUID from attr import define import attr @define class UploadMessageImage: """ The definition of an image with its alternative text. https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage """ imageIdentifier: UUID = attr.ib() """ The unique identifier of an image. https://developer.apple.com/documentation/retentionmessaging/imageidentifier """ altText: str = attr.ib(validator=attr.validators.max_len(150)) """ The alternative text you provide for the corresponding image. https://developer.apple.com/documentation/retentionmessaging/alttext """ ================================================ FILE: appstoreserverlibrary/models/UploadMessageRequestBody.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from typing import List, Optional from attr import define import attr from .BulletPoint import BulletPoint from .HeaderPosition import HeaderPosition from .LibraryUtility import AttrsRawValueAware 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. https://developer.apple.com/documentation/retentionmessaging/uploadmessagerequestbody """ header: str = attr.ib(validator=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)) """ 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) """ 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') """ The position of header text, which defaults to placing header text above the body. https://developer.apple.com/documentation/retentionmessaging/headerposition """ rawHeaderPosition: Optional[str] = HeaderPosition.create_raw_attr('headerPosition') """ 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 """ ================================================ FILE: appstoreserverlibrary/models/UserStatus.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from enum import IntEnum from .LibraryUtility import AppStoreServerLibraryEnumMeta class UserStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta): """ The status of a customer's account within your app. https://developer.apple.com/documentation/appstoreserverapi/userstatus """ UNDECLARED = 0 ACTIVE = 1 SUSPENDED = 2 TERMINATED = 3 LIMITED_ACCESS = 4 ================================================ FILE: appstoreserverlibrary/models/__init__.py ================================================ ================================================ FILE: appstoreserverlibrary/promotional_offer.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurvePrivateKey import uuid import base64 class PromotionalOfferSignatureCreator: _signing_key: EllipticCurvePrivateKey _key_id: str _bundle_id: str 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): """ 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 :param product_identifier: The subscription product identifier :param subscription_offer_id: The subscription discount identifier :param application_username: An optional string value that you define; may be an empty string :param nonce: A one-time UUID value that your server generates. Generate a new nonce for every signature. :param timestamp: A timestamp your server generates in UNIX time format, in milliseconds. The timestamp keeps the offer active for 24 hours. :return: The Base64 encoded signature """ payload = self._bundle_id + '\u2063' + \ self._key_id + '\u2063' + \ product_identifier + '\u2063' + \ subscription_offer_id + '\u2063' + \ application_username.lower() + '\u2063'+ \ str(nonce).lower() + '\u2063' + \ str(timestamp) return base64.b64encode(self._signing_key.sign( payload.encode('utf-8'), ECDSA(SHA256()) )).decode('utf-8') ================================================ FILE: appstoreserverlibrary/py.typed ================================================ ================================================ FILE: appstoreserverlibrary/receipt_utility.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from base64 import b64decode from typing import Optional import asn1 import base64 import re PKCS7_OID = "1.2.840.113549.1.7.2" IN_APP_ARRAY = 17 TRANSACTION_IDENTIFIER = 1703 ORIGINAL_TRANSACTION_IDENTIFIER = 1705 class ReceiptUtility: def _decode_octet_string(self, octet_string: bytes): decoder = asn1.Decoder() decoder.start(octet_string) _, value = decoder.read() return value def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[str]: """ 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 :return: A transaction id from the array of in-app purchases, null if the receipt contains no in-app purchases """ try: val = self._decode_octet_string(b64decode(app_receipt, validate=True)) found_oid = val[0] if found_oid != PKCS7_OID: raise ValueError() inner_value = val[1][0][2][1][0] # Xcode uses nested OctetStrings, we extract the inner string in this case value = self._decode_octet_string(inner_value) # We are in the top-level sequence, work our way to the array of in-apps for inner_value in value: if inner_value[0] == IN_APP_ARRAY: array_values = self._decode_octet_string(inner_value[2]) # In-app array for array_value in array_values: if array_value[0] == TRANSACTION_IDENTIFIER or array_value[0] == ORIGINAL_TRANSACTION_IDENTIFIER: return self._decode_octet_string(array_value[2]) return None except Exception as e: raise ValueError(e) 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. *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 """ decoded_top_level = base64.b64decode(transaction_receipt).decode('utf-8') 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) return None ================================================ FILE: appstoreserverlibrary/signed_data_verifier.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import List, Optional, Dict from base64 import b64decode from enum import IntEnum import time import datetime import asn1 import jwt import requests from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.ec import ECDSA from cryptography.hazmat.primitives.hashes import SHA1, SHA256 from cryptography.x509 import ocsp, oid from OpenSSL import crypto from appstoreserverlibrary.models.AppTransaction import AppTransaction from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter from .models.DecodedRealtimeRequestBody import DecodedRealtimeRequestBody from .models.Environment import Environment from .models.ResponseBodyV2DecodedPayload import ResponseBodyV2DecodedPayload from .models.JWSTransactionDecodedPayload import JWSTransactionDecodedPayload from .models.JWSRenewalInfoDecodedPayload import JWSRenewalInfoDecodedPayload class SignedDataVerifier: """ A class providing utility methods for verifying and decoding App Store signed data. """ def __init__( self, root_certificates: List[bytes], enable_online_checks: bool, environment: Environment, bundle_id: str, app_apple_id: Optional[int] = None, ): self._chain_verifier = _ChainVerifier(root_certificates) self._environment = environment self._bundle_id = bundle_id self._app_apple_id = app_apple_id self._enable_online_checks = enable_online_checks if environment == Environment.PRODUCTION and app_apple_id is None: raise ValueError("appAppleId is required when the environment is Production") def verify_and_decode_renewal_info(self, signed_renewal_info: str) -> JWSRenewalInfoDecodedPayload: """ Verifies and decodes a signedRenewalInfo obtained from the App Store Server API, an App Store Server Notification, or from a device See https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo :param signed_renewal_info: The signedRenewalInfo field :return: The decoded renewal info after verification :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) 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: """ 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 :param signed_transaction: The signedTransaction field :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) if decoded_transaction_info.bundleId != self._bundle_id: raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) if decoded_transaction_info.environment != self._environment: raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT) return decoded_transaction_info def verify_and_decode_notification(self, signed_payload: str) -> ResponseBodyV2DecodedPayload: """ 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 :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) bundle_id = None app_apple_id = None environment = None if decoded_signed_notification.data: bundle_id = decoded_signed_notification.data.bundleId app_apple_id = decoded_signed_notification.data.appAppleId environment = decoded_signed_notification.data.environment elif decoded_signed_notification.summary: bundle_id = decoded_signed_notification.summary.bundleId app_apple_id = decoded_signed_notification.summary.appAppleId environment = decoded_signed_notification.summary.environment elif decoded_signed_notification.externalPurchaseToken: bundle_id = decoded_signed_notification.externalPurchaseToken.bundleId app_apple_id = decoded_signed_notification.externalPurchaseToken.appAppleId if decoded_signed_notification.externalPurchaseToken.externalPurchaseId and decoded_signed_notification.externalPurchaseToken.externalPurchaseId.startswith("SANDBOX"): environment = Environment.SANDBOX else: environment = Environment.PRODUCTION elif decoded_signed_notification.appData: bundle_id = decoded_signed_notification.appData.bundleId app_apple_id = decoded_signed_notification.appData.appAppleId environment = decoded_signed_notification.appData.environment self._verify_notification(bundle_id, app_apple_id, environment) return decoded_signed_notification def _verify_notification(self, bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]): if bundle_id != self._bundle_id or (self._environment == Environment.PRODUCTION and app_apple_id != self._app_apple_id): raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER) if environment != self._environment: raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT) def verify_and_decode_app_transaction(self, signed_app_transaction: str) -> AppTransaction: """ Verifies and decodes a signed AppTransaction See https://developer.apple.com/documentation/storekit/apptransaction :param signed_app_transaction: The signed AppTransaction :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) 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) if environment != self._environment: raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT) return decoded_app_transaction def verify_and_decode_realtime_request(self, signed_payload: str) -> DecodedRealtimeRequestBody: """ Verifies and decodes a Retention Messaging API signedPayload See https://developer.apple.com/documentation/retentionmessaging/signedpayload :param signedPayload: 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) 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_signed_object(self, signed_obj: str) -> dict: try: decoded_jwt = jwt.decode(signed_obj, options={"verify_signature": False}) if self._environment == Environment.XCODE or self._environment == Environment.LOCAL_TESTING: # Data is not signed by the App Store, and verification should be skipped # 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") if x5c_header is None or len(x5c_header) == 0: raise Exception("x5c claim was empty") algorithm_header: 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') effective_date = time.time() if self._enable_online_checks or signed_date is None else int(signed_date) // 1000 signing_key = self._chain_verifier.verify_chain(x5c_header, self._enable_online_checks, effective_date) return jwt.decode(signed_obj, signing_key, algorithms=["ES256"]) except VerificationException as e: raise e except Exception as e: raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e class _ChainVerifier: MAXIMUM_CACHE_SIZE = 32 # There are unlikely to be more than a couple keys at once CACHE_TIME_LIMIT = 15 * 60 # 15 minutes 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)] = {} def verify_chain(self, certificates: List[str], perform_online_checks: bool, effective_date: int) -> 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: return cached_public_key verified_public_key = self._verify_chain_without_caching(certificates=certificates, perform_online_checks=perform_online_checks, effective_date=effective_date) if perform_online_checks: 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: if len(self.root_certificates) == 0: raise VerificationException(VerificationStatus.INVALID_CERTIFICATE) if len(certificates) != 3: raise VerificationException(VerificationStatus.INVALID_CHAIN_LENGTH) trusted_store = crypto.X509Store() try: for trusted_cert_bytes in self.root_certificates: trusted_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, trusted_cert_bytes) trusted_store.add_cert(trusted_cert) if self.enable_strict_checks: trusted_store.set_flags(crypto.X509StoreFlags.X509_STRICT) leaf_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, b64decode(certificates[0], validate=True)) intermediate_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, b64decode(certificates[1], validate=True)) verification_context = crypto.X509StoreContext(trusted_store, leaf_cert, [intermediate_cert]) except Exception as e: raise VerificationException(VerificationStatus.INVALID_CERTIFICATE) from e trusted_store.set_time(datetime.datetime.fromtimestamp(effective_date, tz=datetime.timezone.utc)) try: verification_context.verify_certificate() trusted_chain = verification_context.get_verified_chain() except Exception as e: raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e self.check_oid(trusted_chain[0].to_cryptography(), "1.2.840.113635.100.6.11.1") self.check_oid(trusted_chain[1].to_cryptography(), "1.2.840.113635.100.6.2.1") if perform_online_checks: self.check_ocsp_status(trusted_chain[1], trusted_chain[2], trusted_chain[2]) self.check_ocsp_status(trusted_chain[0], trusted_chain[1], trusted_chain[2]) return ( leaf_cert.to_cryptography() .public_key() .public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo) .decode() ) def check_oid(self, cert: x509.Certificate, oid: str): try: cert.extensions.get_extension_for_oid(x509.ObjectIdentifier(oid)) except Exception as e: raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e def check_ocsp_status(self, cert: crypto.X509, issuer: crypto.X509, root: crypto.X509): builder = ocsp.OCSPRequestBuilder() builder = builder.add_certificate(cert.to_cryptography(), issuer.to_cryptography(), SHA256()) req = builder.build() authority_values = ( cert.to_cryptography() .extensions.get_extension_for_oid(x509.oid.ExtensionOID.AUTHORITY_INFORMATION_ACCESS) .value ) ocsps = [val for val in authority_values if val.access_method == x509.oid.AuthorityInformationAccessOID.OCSP] for o in ocsps: try: r = requests.post( o.access_location.value, headers={"Content-Type": "application/ocsp-request"}, data=req.public_bytes(serialization.Encoding.DER), timeout=30, ) except (requests.exceptions.RequestException, OSError) as e: raise VerificationException(VerificationStatus.RETRYABLE_VERIFICATION_FAILURE) from e if r.status_code != 200: raise VerificationException(VerificationStatus.RETRYABLE_VERIFICATION_FAILURE) else: ocsp_resp = ocsp.load_der_ocsp_response(r.content) if ocsp_resp.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL: certs = [issuer] for ocsp_cert in ocsp_resp.certificates: certs.append(crypto.X509.from_cryptography(ocsp_cert)) # Find signing cert signing_cert = None for potential_signing_cert in certs: if ocsp_resp.responder_key_hash: subject_public_key_info = ( potential_signing_cert.get_pubkey() .to_cryptography_key() .public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) ) decoder = asn1.Decoder() decoder.start(subject_public_key_info) decoder.enter() decoder.read() _, value = decoder.read() digest = hashes.Hash(SHA1()) digest.update(value) if digest.finalize() == ocsp_resp.responder_key_hash: signing_cert = potential_signing_cert break elif ocsp_resp.responder_name: if ocsp_resp.responder_name == potential_signing_cert.subject.rfc4514_string(): signing_cert = potential_signing_cert break if signing_cert is None: raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) if signing_cert.to_cryptography().public_bytes( encoding=serialization.Encoding.DER ) == issuer.to_cryptography().public_bytes(encoding=serialization.Encoding.DER): # We trust this because it is the issuer pass else: trusted_store = crypto.X509Store() trusted_store.add_cert(issuer) trusted_store.add_cert(root) # Apparently a full chain is always needed verification_context = crypto.X509StoreContext(trusted_store, signing_cert, []) verification_context.verify_certificate() if ( oid.ExtendedKeyUsageOID.OCSP_SIGNING not in signing_cert.to_cryptography() .extensions.get_extension_for_class(x509.ExtendedKeyUsage) .value._usages ): raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) # Confirm response is signed by signing_certificate signing_cert.to_cryptography().public_key().verify( ocsp_resp.signature, ocsp_resp.tbs_response_bytes, ECDSA(ocsp_resp.signature_hash_algorithm) ) # Get the CertId for single_response in ocsp_resp.responses: # Get the cert ID with the provided hashing algorithm (using the request builder wrapper) builder = ocsp.OCSPRequestBuilder() builder = builder.add_certificate( cert.to_cryptography(), issuer.to_cryptography(), single_response.hash_algorithm ) req = builder.build() if ( single_response.certificate_status == ocsp.OCSPCertStatus.GOOD and single_response.serial_number == req.serial_number and single_response.issuer_key_hash == req.issuer_key_hash and single_response.issuer_name_hash == req.issuer_name_hash ): # Success return raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) def get_cached_public_key(self, certificates: List[str]) -> Optional[str]: verified_public_key = self.verified_certificates_cache.get(tuple(certificates)) if verified_public_key is None: return None if verified_public_key[1] <= time.time(): return None return verified_public_key[0] def put_verified_public_key(self, certificates: List[str], verified_public_key: str): cache_expiration = time.time() + _ChainVerifier.CACHE_TIME_LIMIT self.verified_certificates_cache[tuple(certificates)] = (verified_public_key, cache_expiration) if len(self.verified_certificates_cache) > _ChainVerifier.MAXIMUM_CACHE_SIZE: for k, v in list(self.verified_certificates_cache.items()): if v[1] <= time.time(): del self.verified_certificates_cache[k] class VerificationStatus(IntEnum): OK = 0 VERIFICATION_FAILURE = 1 INVALID_APP_IDENTIFIER = 2 INVALID_CERTIFICATE = 3 INVALID_CHAIN_LENGTH = 4 INVALID_CHAIN = 5 INVALID_ENVIRONMENT = 6 RETRYABLE_VERIFICATION_FAILURE = 7 class VerificationException(Exception): def __init__(self, status: VerificationStatus): super().__init__("Verification failed with status " + status.name) self.status = status ================================================ FILE: docs/requirements.txt ================================================ sphinx == 9.1.0 ================================================ FILE: pyproject.toml ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "app-store-server-library" version = "3.0.0" description = "The App Store Server Library" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "MIT"} requires-python = ">=3.7, <4" classifiers = [ "License :: OSI Approved :: MIT License" ] dependencies = [ "attrs>=21.3.0", "PyJWT>=2.6.0,<3", "requests>=2.28.0,<3", "cryptography>=40.0.0", "pyOpenSSL>=23.1.1", "asn1==3.2.0", "cattrs>=23.1.2", ] [project.optional-dependencies] async = ["httpx"] [tool.setuptools] packages = {find = {exclude = ["tests"]}} [tool.setuptools.package-data] appstoreserverlibrary = ["py.typed"] ================================================ FILE: requirements.txt ================================================ attrs >= 21.3.0 PyJWT >= 2.6.0, < 3 requests >= 2.28.0, < 3 cryptography >= 40.0.0 pyOpenSSL >= 23.1.1 asn1==3.2.0 cattrs >= 23.1.2 httpx==0.28.1 ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/resources/certs/testSigningKey.p8 ================================================ -----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgSpP55ELdXswj9JRZ APRwtTfS4CNRqpKIs+28rNHiPAqhRANCAASs8nLES7b+goKslppNVOurf0MonZdw 3pb6TxS8Z/5j+UNY1sWK1ChxpuwNS9I3R50cfdQo/lA9PPhw6XIg8ytd -----END PRIVATE KEY----- ================================================ FILE: tests/resources/mock_signed_data/legacyTransaction ================================================ ewoicHVyY2hhc2UtaW5mbyIgPSAiZXdvaWRISmhibk5oWTNScGIyNHRhV1FpSUQwZ0lqTXpPVGt6TXprNUlqc0tmUW89IjsKfQo= ================================================ FILE: tests/resources/mock_signed_data/missingX5CHeaderClaim ================================================ eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1Y3dyb25nIjpbIk1JSUJvRENDQVVhZ0F3SUJBZ0lCRERBS0JnZ3Foa2pPUFFRREF6QkZNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1CNFhEVEl6TURFd05USXhNekV6TkZvWERUTXpNREV3TVRJeE16RXpORm93UFRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eERUQUxCZ05WQkFvTUJFeGxZV1l3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVRpdFlIRWFZVnVjOGc5QWpUT3dFck12R3lQeWtQYStwdXZUSThoSlRIWlpETEdhczJxWDErRXJ4Z1FUSmdWWHY3Nm5tTGhoUkpIK2oyNUFpQUk4aUdzb3k4d0xUQUpCZ05WSFJNRUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVFCZ29xaGtpRzkyTmtCZ3NCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05JQURCRkFpQlg0YytUMEZwNW5KNVFSQ2xSZnU1UFNCeVJ2TlB0dWFUc2swdlBCM1dBSUFJaEFOZ2FhdUFqL1lQOXMwQWtFaHlKaHhRTy82UTJ6b3VaK0gxQ0lPZWhuTXpRIiwiTUlJQm56Q0NBVVdnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQXpBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1CNFhEVEl6TURFd05USXhNekV3TlZvWERUTXpNREV3TVRJeE16RXdOVm93UlRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eEZUQVRCZ05WQkFvTURFbHVkR1Z5YldWa2FXRjBaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCQlVONVY5cktqZlJpTUFJb2pFQTBBdjVNcDBvRitPMGNMNGd6clRGMTc4aW5VSHVnajdFdDQ2TnJrUTdoS2dNVm5qb2dxNDVRMXJNcytjTUhWTklMV3FqTlRBek1BOEdBMVVkRXdRSU1BWUJBZjhDQVFBd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJBR0NpcUdTSWIzWTJRR0FnRUVBZ1VBTUFvR0NDcUdTTTQ5QkFNREEwZ0FNRVVDSVFDbXNJS1lzNDF1bGxzc0hYNHJWdmVVVDBaN0lzNS9oTEsxbEZQVHR1bjNoQUlnYzIrMlJHNStnTmNGVmNzK1hKZUVsNEdaK29qbDNST09tbGwreWU3ZHluUT0iLCJNSUlCZ2pDQ0FTbWdBd0lCQWdJSkFMVWM1QUxpSDVwYk1Bb0dDQ3FHU000OUJBTURNRFl4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh3SGhjTk1qTXdNVEExTWpFek1ESXlXaGNOTXpNd01UQXlNakV6TURJeVdqQTJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVjKy9CbCtnb3NwbzZ0ZjlaN2lvNXRkS2RybE4xWWRWbnFFaEVEWERTaHpkQUpQUWlqYW1YSU1IZjh4V1dUYTF6Z29ZVHhPS3BidUp0RHBsejFYcmlUYU1nTUI0d0RBWURWUjBUQkFVd0F3RUIvekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdDZ1lJS29aSXpqMEVBd01EUndBd1JBSWdlbVdRWG5NQWRUYWQySkRKV25nOVU0dUJCTDVtQTdXSTA1SDdvSDdjNmlRQ0lIaVJxTWpOZnpVQXlpdTloNnJPVS9LK2lUUjBJLzNZL05TV3NYSFgrYWNjIl19.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.1TFhjDR4WwQJNgizVGYXz3WE3ajxTdH1wKLQQ71MtrkadSxxOo3yPo_6L9Z03unIU7YK-NRNzSIb5bh5WqTprQ ================================================ FILE: tests/resources/mock_signed_data/renewalInfo ================================================ eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJzaWduZWREYXRlIjoxNjcyOTU2MTU0MDAwfQ.FbK2OL-t6l4892W7fzWyus_g9mIl2CzWLbVt7Kgcnt6zzVulF8bzovgpe0v_y490blROGixy8KDoe2dSU53-Xw ================================================ FILE: tests/resources/mock_signed_data/testNotification ================================================ eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQsImVudmlyb25tZW50IjoiU2FuZGJveCIsImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsInNpZ25lZERhdGUiOjE2ODEzMTQzMjQwMDAsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.VVXYwuNm2Y3XsOUva-BozqatRCsDuykA7xIe_CCRw6aIAAxJ1nb2sw871jfZ6dcgNhUuhoZ93hfbc1v_5zB7Og ================================================ FILE: tests/resources/mock_signed_data/transactionInfo ================================================ eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlIiwic2lnbmVkRGF0ZSI6MTY3Mjk1NjE1NDAwMH0.PnHWpeIJZ8f2Q218NSGLo_aR0IBEJvC6PxmxKXh-qfYTrZccx2suGl223OSNAX78e4Ylf2yJCG2N-FfU-NIhZQ ================================================ FILE: tests/resources/mock_signed_data/wrongBundleId ================================================ eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUud3JvbmcifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.WWE31hTB_mcv2O_lf-xI-MNY3d8txc0MzpqFx4QnYDfFIxB95Lo2Fm3r46YSjLLdL7xCWdEJrJP5bHgRCejAGg ================================================ FILE: tests/resources/models/advancedCommerceDescriptors.json ================================================ { "description": "description", "displayName": "display name" } ================================================ FILE: tests/resources/models/advancedCommerceOffer.json ================================================ { "period": "P1W", "periodCount": 3, "price": 5000, "reason": "WIN_BACK" } ================================================ FILE: tests/resources/models/advancedCommerceOneTimeChargeCreateRequest.json ================================================ { "currency": "USD", "item": { "description": "description", "displayName": "display name", "SKU": "sku", "price": 10000 }, "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440000" }, "taxCode": "taxCode", "storefront": "USA", "operation": "CREATE", "version": "1.0" } ================================================ FILE: tests/resources/models/advancedCommerceOneTimeChargeItem.json ================================================ { "description": "description", "displayName": "display name", "SKU": "sku", "price": 15000 } ================================================ FILE: tests/resources/models/advancedCommerceRequestInfo.json ================================================ { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440010", "appAccountToken": "660e8400-e29b-41d4-a716-446655440011", "consistencyToken": "consistency_token_value" } ================================================ FILE: tests/resources/models/advancedCommerceRequestRefundItem.json ================================================ { "SKU": "sku", "refundReason": "LEGAL", "refundType": "FULL", "revoke": true, "refundAmount": 5000 } ================================================ FILE: tests/resources/models/advancedCommerceRequestRefundRequest.json ================================================ { "items": [ { "SKU": "sku", "refundReason": "LEGAL", "refundType": "FULL", "revoke": true }, { "SKU": "sku", "refundReason": "OTHER", "refundType": "PRORATED", "revoke": false } ], "refundRiskingPreference": true, "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440002" }, "currency": "USD", "storefront": "USA" } ================================================ FILE: tests/resources/models/advancedCommerceRequestRefundResponse.json ================================================ { "signedTransactionInfo": "signed_transaction_info_value" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionCancelRequest.json ================================================ { "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440003" }, "storefront": "USA" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionCancelResponse.json ================================================ { "signedRenewalInfo": "signed_renewal_info", "signedTransactionInfo": "signed_transaction_info" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionChangeMetadataDescriptors.json ================================================ { "effective": "IMMEDIATELY", "description": "description", "displayName": "displayName" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionChangeMetadataItem.json ================================================ { "currentSKU": "currentSku", "effective": "NEXT_BILL_CYCLE", "description": "description", "displayName": "displayName", "SKU": "sku" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionChangeMetadataRequest.json ================================================ { "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440009" }, "items": [ { "currentSKU": "currentSKU", "effective": "IMMEDIATELY", "description": "description", "displayName": "display name", "SKU": "sku" } ] } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionChangeMetadataResponse.json ================================================ { "signedRenewalInfo": "signed_renewal_info", "signedTransactionInfo": "signed_transaction_info" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionCreateItem.json ================================================ { "description": "description", "displayName": "display name", "SKU": "sku", "price": 20000 } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionCreateRequest.json ================================================ { "currency": "USD", "descriptors": { "description": "description", "displayName": "display name" }, "items": [ { "SKU": "sku", "description": "description", "displayName": "display name", "price": 20000 }, { "SKU": "sku", "description": "description", "displayName": "display name", "price": 30000 } ], "period": "P1M", "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440001" }, "taxCode": "taxCode", "storefront": "USA", "previousTransactionId": "transactionId" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionMigrateDescriptors.json ================================================ { "description": "description", "displayName": "displayName" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionMigrateItem.json ================================================ { "SKU": "sku", "description": "description", "displayName": "displayName" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionMigrateRenewalItem.json ================================================ { "SKU": "sku", "description": "description", "displayName": "displayName" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionMigrateRequest.json ================================================ { "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440006" }, "descriptors": { "description": "description", "displayName": "display name" }, "items": [ { "SKU": "sku", "description": "description", "displayName": "display name" } ], "targetProductId": "targetProductId", "taxCode": "taxCode" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionMigrateResponse.json ================================================ { "signedRenewalInfo": "signed_renewal_info_value", "signedTransactionInfo": "signed_transaction_info_value" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionModifyAddItem.json ================================================ { "SKU": "sku", "description": "description", "displayName": "displayName", "price": 12000 } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionModifyChangeItem.json ================================================ { "currentSKU": "currentSku", "description": "description", "displayName": "displayName", "effective": "IMMEDIATELY", "price": 13000, "reason": "UPGRADE", "SKU": "sku" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionModifyDescriptors.json ================================================ { "effective": "IMMEDIATELY", "description": "description", "displayName": "displayName" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionModifyInAppRequest.json ================================================ { "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440007" }, "transactionId": "transactionId", "retainBillingCycle": true, "descriptors": { "effective": "IMMEDIATELY", "description": "description", "displayName": "display name" }, "taxCode": "taxCode", "currency": "USD" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionModifyPeriodChange.json ================================================ { "effective": "IMMEDIATELY", "period": "P3M" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionModifyRemoveItem.json ================================================ { "SKU": "sku" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionPriceChangeItem.json ================================================ { "SKU": "sku", "price": 16000, "dependentSKUs": ["dependentSKU"] } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionPriceChangeRequest.json ================================================ { "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440005" }, "items": [ { "SKU": "sku123", "price": 15000 } ], "currency": "USD" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionPriceChangeResponse.json ================================================ { "signedRenewalInfo": "signed_renewal_info", "signedTransactionInfo": "signed_transaction_info" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionReactivateInAppRequest.json ================================================ { "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440008" }, "transactionId": "transactionId", "items": [ { "SKU": "sku" } ] } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionReactivateItem.json ================================================ { "SKU": "sku" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionRevokeRequest.json ================================================ { "requestInfo": { "requestReferenceId": "550e8400-e29b-41d4-a716-446655440004" }, "refundRiskingPreference": true, "refundReason": "LEGAL", "refundType": "FULL", "storefront": "USA" } ================================================ FILE: tests/resources/models/advancedCommerceSubscriptionRevokeResponse.json ================================================ { "signedRenewalInfo": "signed_renewal_info", "signedTransactionInfo": "signed_transaction_info" } ================================================ FILE: tests/resources/models/apiException.json ================================================ { "errorCode": 5000000, "errorMessage": "An unknown error occurred." } ================================================ FILE: tests/resources/models/apiTooManyRequestsException.json ================================================ { "errorCode": 4290000, "errorMessage": "Rate limit exceeded." } ================================================ FILE: tests/resources/models/apiUnknownError.json ================================================ { "errorCode": 9990000, "errorMessage": "Testing error." } ================================================ FILE: tests/resources/models/appData.json ================================================ { "appAppleId": 987654321, "bundleId": "com.example", "environment": "Sandbox", "signedAppTransactionInfo": "signed-app-transaction-info" } ================================================ FILE: tests/resources/models/appTransaction.json ================================================ { "receiptType": "LocalTesting", "appAppleId": 531412, "bundleId": "com.example", "applicationVersion": "1.2.3", "versionExternalIdentifier": 512, "receiptCreationDate": 1698148900000, "originalPurchaseDate": 1698148800000, "originalApplicationVersion": "1.1.2", "deviceVerification": "device_verification_value", "deviceVerificationNonce": "48ccfa42-7431-4f22-9908-7e88983e105a", "preorderDate": 1698148700000, "appTransactionId": "71134", "originalPlatform": "iOS" } ================================================ FILE: tests/resources/models/appTransactionDoesNotExistError.json ================================================ { "errorCode": 4040019, "errorMessage": "No AppTransaction exists for the customer." } ================================================ FILE: tests/resources/models/appTransactionInfoResponse.json ================================================ { "signedAppTransactionInfo": "signed_app_transaction_info_value" } ================================================ FILE: tests/resources/models/decodedRealtimeRequest.json ================================================ { "originalTransactionId": "99371282", "appAppleId": 531412, "productId": "com.example.product", "userLocale": "en-US", "requestIdentifier": "3db5c98d-8acf-4e29-831e-8e1f82f9f6e9", "environment": "LocalTesting", "signedDate": 1698148900000 } ================================================ FILE: tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json ================================================ { "requestIdentifier": "758883e8-151b-47b7-abd0-60c4d804c2f5" } ================================================ FILE: tests/resources/models/extendSubscriptionRenewalDateResponse.json ================================================ { "originalTransactionId": "2312412", "webOrderLineItemId": "9993", "success": true, "effectiveDate": 1698148900000 } ================================================ FILE: tests/resources/models/familyTransactionNotSupportedError.json ================================================ { "errorCode": 4000185, "errorMessage": "Invalid request. Family Sharing transactions aren't supported by this endpoint." } ================================================ FILE: tests/resources/models/getAllSubscriptionStatusesResponse.json ================================================ { "environment": "LocalTesting", "bundleId": "com.example", "appAppleId": 5454545, "data": [ { "subscriptionGroupIdentifier": "sub_group_one", "lastTransactions": [ { "status": 1, "originalTransactionId": "3749183", "signedTransactionInfo": "signed_transaction_one", "signedRenewalInfo": "signed_renewal_one" }, { "status": 5, "originalTransactionId": "5314314134", "signedTransactionInfo": "signed_transaction_two", "signedRenewalInfo": "signed_renewal_two" } ] }, { "subscriptionGroupIdentifier": "sub_group_two", "lastTransactions": [ { "status": 2, "originalTransactionId": "3413453", "signedTransactionInfo": "signed_transaction_three", "signedRenewalInfo": "signed_renewal_three" } ] } ] } ================================================ FILE: tests/resources/models/getDefaultMessageResponse.json ================================================ { "messageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890" } ================================================ FILE: tests/resources/models/getImageListResponse.json ================================================ { "imageIdentifiers": [ { "imageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890", "imageState": "APPROVED", "imageSize": "FULL_SIZE" } ] } ================================================ FILE: tests/resources/models/getMessageListResponse.json ================================================ { "messageIdentifiers": [ { "messageIdentifier": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890", "messageState": "APPROVED" } ] } ================================================ FILE: tests/resources/models/getNotificationHistoryResponse.json ================================================ { "paginationToken": "57715481-805a-4283-8499-1c19b5d6b20a", "hasMore": true, "notificationHistory": [ { "sendAttempts": [ { "attemptDate": 1698148900000, "sendAttemptResult": "NO_RESPONSE" }, { "attemptDate": 1698148950000, "sendAttemptResult": "SUCCESS" } ], "signedPayload": "signed_payload_one" }, { "sendAttempts": [ { "attemptDate": 1698148800000, "sendAttemptResult": "CIRCULAR_REDIRECT" } ], "signedPayload": "signed_payload_two" } ] } ================================================ FILE: tests/resources/models/getRealtimeUrlResponse.json ================================================ { "realtimeURL": "https://example.com/realtime" } ================================================ FILE: tests/resources/models/getRefundHistoryResponse.json ================================================ { "signedTransactions": [ "signed_transaction_one", "signed_transaction_two" ], "revision": "revision_output", "hasMore": true } ================================================ FILE: tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json ================================================ { "requestIdentifier": "20fba8a0-2b80-4a7d-a17f-85c1854727f8", "complete": true, "completeDate": 1698148900000, "succeededCount": 30, "failedCount": 2 } ================================================ FILE: tests/resources/models/getTestNotificationStatusResponse.json ================================================ { "signedPayload": "signed_payload", "sendAttempts": [ { "attemptDate": 1698148900000, "sendAttemptResult": "NO_RESPONSE" }, { "attemptDate": 1698148950000, "sendAttemptResult": "SUCCESS" } ] } ================================================ FILE: tests/resources/models/invalidAppAccountTokenUUIDError.json ================================================ { "errorCode": 4000183, "errorMessage": "Invalid request. The app account token field must be a valid UUID." } ================================================ FILE: tests/resources/models/invalidTransactionIdError.json ================================================ { "errorCode": 4000006, "errorMessage": "Invalid transaction id." } ================================================ FILE: tests/resources/models/lookupOrderIdResponse.json ================================================ { "status": 1, "signedTransactions": [ "signed_transaction_one", "signed_transaction_two" ] } ================================================ FILE: tests/resources/models/performanceTestResponse.json ================================================ { "config": { "maxConcurrentRequests": 10, "totalRequests": 100, "totalDuration": 60000, "responseTimeThreshold": 500, "successRateThreshold": 95 }, "requestId": "c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d" } ================================================ FILE: tests/resources/models/performanceTestResultResponse.json ================================================ { "config": { "maxConcurrentRequests": 10, "totalRequests": 100, "totalDuration": 60000, "responseTimeThreshold": 500, "successRateThreshold": 95 }, "target": "https://example.com/retention", "result": "PASS", "successRate": 98, "numPending": 0, "responseTimes": { "average": 120, "p50": 100, "p90": 200, "p95": 250, "p99": 400 }, "failures": { "TIMED_OUT": 1, "NO_RESPONSE": 1 } } ================================================ FILE: tests/resources/models/requestTestNotificationResponse.json ================================================ { "testNotificationToken": "ce3af791-365e-4c60-841b-1674b43c1609" } ================================================ FILE: tests/resources/models/signedConsumptionRequestNotification.json ================================================ { "notificationType": "CONSUMPTION_REQUEST", "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", "data": { "environment": "LocalTesting", "appAppleId": 41234, "bundleId": "com.example", "bundleVersion": "1.2.3", "signedTransactionInfo": "signed_transaction_info_value", "signedRenewalInfo": "signed_renewal_info_value", "status": 1, "consumptionRequestReason": "UNINTENDED_PURCHASE" }, "version": "2.0", "signedDate": 1698148900000 } ================================================ FILE: tests/resources/models/signedExternalPurchaseTokenNotification.json ================================================ { "notificationType": "EXTERNAL_PURCHASE_TOKEN", "subtype": "UNREPORTED", "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", "version": "2.0", "signedDate": 1698148900000, "externalPurchaseToken": { "externalPurchaseId": "b2158121-7af9-49d4-9561-1f588205523e", "tokenCreationDate": 1698148950000, "appAppleId": 55555, "bundleId": "com.example" } } ================================================ FILE: tests/resources/models/signedExternalPurchaseTokenSandboxNotification.json ================================================ { "notificationType": "EXTERNAL_PURCHASE_TOKEN", "subtype": "UNREPORTED", "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", "version": "2.0", "signedDate": 1698148900000, "externalPurchaseToken": { "externalPurchaseId": "SANDBOX_b2158121-7af9-49d4-9561-1f588205523e", "tokenCreationDate": 1698148950000, "appAppleId": 55555, "bundleId": "com.example" } } ================================================ FILE: tests/resources/models/signedNotification.json ================================================ { "notificationType": "SUBSCRIBED", "subtype": "INITIAL_BUY", "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", "data": { "environment": "LocalTesting", "appAppleId": 41234, "bundleId": "com.example", "bundleVersion": "1.2.3", "signedTransactionInfo": "signed_transaction_info_value", "signedRenewalInfo": "signed_renewal_info_value", "status": 1 }, "version": "2.0", "signedDate": 1698148900000 } ================================================ FILE: tests/resources/models/signedRenewalInfo.json ================================================ { "expirationIntent": 1, "originalTransactionId": "12345", "autoRenewProductId": "com.example.product.2", "productId": "com.example.product", "autoRenewStatus": 1, "isInBillingRetryPeriod": true, "priceIncreaseStatus": 0, "gracePeriodExpiresDate": 1698148900000, "offerType": 2, "offerIdentifier": "abc.123", "signedDate": 1698148800000, "environment": "LocalTesting", "recentSubscriptionStartDate": 1698148800000, "renewalDate": 1698148850000, "renewalPrice": 9990, "currency": "USD", "offerDiscountType": "PAY_AS_YOU_GO", "eligibleWinBackOfferIds": [ "eligible1", "eligible2" ], "appTransactionId": "71134", "offerPeriod": "P1Y", "appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138" } ================================================ FILE: tests/resources/models/signedRescindConsentNotification.json ================================================ { "notificationType": "RESCIND_CONSENT", "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", "appData": { "appAppleId": 41234, "bundleId": "com.example", "environment": "LocalTesting", "signedAppTransactionInfo": "signed_app_transaction_info_value" }, "version": "2.0", "signedDate": 1698148900000 } ================================================ FILE: tests/resources/models/signedSummaryNotification.json ================================================ { "notificationType": "RENEWAL_EXTENSION", "subtype": "SUMMARY", "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", "version": "2.0", "signedDate": 1698148900000, "summary": { "environment": "LocalTesting", "appAppleId": 41234, "bundleId": "com.example", "productId": "com.example.product", "requestIdentifier": "efb27071-45a4-4aca-9854-2a1e9146f265", "storefrontCountryCodes": [ "CAN", "USA", "MEX" ], "succeededCount": 5, "failedCount": 2 } } ================================================ FILE: tests/resources/models/signedTransaction.json ================================================ { "transactionId":"23456", "originalTransactionId":"12345", "webOrderLineItemId":"34343", "bundleId":"com.example", "productId":"com.example.product", "subscriptionGroupIdentifier":"55555", "purchaseDate":1698148900000, "originalPurchaseDate":1698148800000, "expiresDate":1698149000000, "quantity":1, "type":"Auto-Renewable Subscription", "appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138", "inAppOwnershipType":"PURCHASED", "signedDate":1698148900000, "revocationReason": 1, "revocationDate": 1698148950000, "isUpgraded": true, "offerType":1, "offerIdentifier": "abc.123", "environment":"LocalTesting", "transactionReason":"PURCHASE", "storefront":"USA", "storefrontId":"143441", "price": 10990, "currency": "USD", "offerDiscountType": "PAY_AS_YOU_GO", "appTransactionId": "71134", "offerPeriod": "P1Y" } ================================================ FILE: tests/resources/models/signedTransactionWithRevocation.json ================================================ { "originalTransactionId": "12345", "transactionId": "23456", "webOrderLineItemId": "34343", "bundleId": "com.example", "productId": "com.example.product", "subscriptionGroupIdentifier": "55555", "purchaseDate": 1698148900000, "originalPurchaseDate": 1698148800000, "expiresDate": 1698149000000, "quantity": 1, "type": "Auto-Renewable Subscription", "appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138", "inAppOwnershipType": "PURCHASED", "signedDate": 1698148900000, "revocationReason": 1, "revocationDate": 1698148950000, "isUpgraded": true, "offerType": 1, "offerIdentifier": "abc.123", "environment": "LocalTesting", "storefront": "USA", "storefrontId": "143441", "transactionReason": "PURCHASE", "price": 10990, "currency": "USD", "offerDiscountType": "PAY_AS_YOU_GO", "appTransactionId": "71134", "offerPeriod": "P1Y", "revocationType": "REFUND_PRORATED", "revocationPercentage": 50000 } ================================================ FILE: tests/resources/models/transactionHistoryResponse.json ================================================ { "revision": "revision_output", "hasMore": true, "bundleId": "com.example", "appAppleId": 323232, "environment": "LocalTesting", "signedTransactions": [ "signed_transaction_value", "signed_transaction_value2" ] } ================================================ FILE: tests/resources/models/transactionHistoryResponseWithMalformedAppAppleId.json ================================================ { "revision": "revision_output", "hasMore": 1, "bundleId": "com.example", "appAppleId": "hi", "environment": "LocalTesting", "signedTransactions": [ "signed_transaction_value", "signed_transaction_value2" ] } ================================================ FILE: tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json ================================================ { "revision": "revision_output", "hasMore": true, "bundleId": "com.example", "appAppleId": 323232, "environment": "LocalTestingxxx", "signedTransactions": [ "signed_transaction_value", "signed_transaction_value2" ] } ================================================ FILE: tests/resources/models/transactionIdNotFoundError.json ================================================ { "errorCode": 4040010, "errorMessage": "Transaction id not found." } ================================================ FILE: tests/resources/models/transactionIdNotOriginalTransactionId.json ================================================ { "errorCode": 4000187, "errorMessage": "Invalid request. The transaction ID provided is not an original transaction ID." } ================================================ FILE: tests/resources/models/transactionInfoResponse.json ================================================ { "signedTransactionInfo": "signed_transaction_info_value" } ================================================ FILE: tests/resources/xcode/xcode-app-receipt-empty ================================================ MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIHhMYHeMA8CAQACAQEEBwwFWGNvZGUwCwIBAQIBAQQDAgEAMDUCAQICAQEELQwrY29tLmV4YW1wbGUubmF0dXJlbGFiLmJhY2t5YXJkYmlyZHMuZXhhbXBsZTALAgEDAgEBBAMMATEwEAIBBAIBAQQI0bz+zwQAAAAwHAIBBQIBAQQU4nEwK24WxZhKi0PSGTYgWoXOIqMwCgIBCAIBAQQCFgAwHgIBDAIBAQQWFhQyMDIzLTEwLTE5VDAxOjE4OjU0WjAeAgEVAgEBBBYWFDQwMDEtMDEtMDFUMDA6MDA6MDBaAAAAAAAAoIIDeDCCA3QwggJcoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MB4XDTIwMDQwMTE3NTIzNVoXDTQwMDMyNzE3NTIzNVowXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23+QPCxzD9uXJkuTuwr4oSE+yGHZJMheH3U+2pPbMRqRgLm/5QzLPLsORGIm+gQptknnb+Ab5g1ozSVuw3YI9UoLrnp0PMSpC7PPYg/7tLz324ReKOtHDfHti6z1n7AJOKNue8smUAoa4YnRcnYLOUzLT27As1+3lbq5qF1KdKvvb0GlfgmNuj09zXBX2O3v1dp3yJMEHO8JiHhlzoHyjXLnBxpuJhL3MrENuziQawbE/A3llVDNkci6JfRYyYzhcdtKRfMtGZYDVoGmRO51d1tTz3isXbo+X1ArXCmM3cLXKhffIrTX5Hior6htp8HaaC1mzM8pC1As48L75l8SwQIDAQABozswOTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIChDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAsgDgPPHo6WK9wNYdQJ5XuTiQd3ZS0qhLcG64Z5n7s4pVn+8dKLhfKtFznzVHN7tG03YQ8vBp7M1imXH5YIqESDjEvYtnJbmrbDNlrdjCmnhID+nMwScNxs9kPG2AWTOMyjYGKhEbjUnOCP9mwEcoS+tawSsJViylqgkDezIx3OiFeEjOwMUSEWoPDK4vBcpvemR/ICx15kyxEtP94x9eDX24WNegfOR/Y6uXmivDKtjQsuHVWg05G29nKKkSg9aHeG2ZvV6zCuCYzvbqw45taeu3QIE9hz1wUdHEXY2l3H9qWBreYHY3Uuz/rBldDBUvig/1icjXKx0e7CuRBac9TzGCAY8wggGLAgEBMGQwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0AgEBMA0GCWCGSAFlAwQCAQUAMA0GCSqGSIb3DQEBCwUABIIBAIjP3bmY+TrOM0e8n7PeH3OEies1+spNT1n8om4424n/NyIJ9XRyj1QGxshxh6p2BQuUQV8mkWKpHYQJqPobVEcl72ndbHSfzkH2vM57jy/2bCopLt+zWQl0QMA9iKEB3G075wgyD6lcSveZnER/4J6E9+tO6O3R2YFVziwL2UmNR1XgfOhKyNwCfSV1CyVVoSUkkZI7fJ1S6Pce2nLKM1pf+oCWr5vAySd9E4givt/YagGJF+3RHZMEcrqHnnP8kQKi99xnXcIfYyK6VMD9uBb2+4N7MCRDhoY/8+vX9I75paW0UicS6MwacJPueNxLaAboOP4nFSlYhEhZuLiZrdIAAAAAAAA= ================================================ FILE: tests/resources/xcode/xcode-app-receipt-with-transaction ================================================ MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIIBdjGCAXIwDwIBAAIBAQQHDAVYY29kZTALAgEBAgEBBAMCAQAwNQIBAgIBAQQtDCtjb20uZXhhbXBsZS5uYXR1cmVsYWIuYmFja3lhcmRiaXJkcy5leGFtcGxlMAsCAQMCAQEEAwwBMTAQAgEEAgEBBAjyv/X7DwAAADAcAgEFAgEBBBQWU6vLoHZxeVVlaOg/UEG2OOKahTAKAgEIAgEBBAIWADAeAgEMAgEBBBYWFDIwMjMtMTAtMTlUMDE6NDU6NDBaMIGRAgERAgEBBIGIMYGFMAwCAgalAgEBBAMCAQEwFwICBqYCAQEEDgwMcGFzcy5wcmVtaXVtMAwCAganAgEBBAMMATAwHwICBqgCAQEEFhYUMjAyMy0xMC0xOVQwMTo0NTozNlowHwICBqwCAQEEFhYUMjAyMy0xMS0xOVQwMTo0NTozNlowDAICBrcCAQEEAwIBATAeAgEVAgEBBBYWFDQwMDEtMDEtMDFUMDA6MDA6MDBaAAAAAAAAoIIDeDCCA3QwggJcoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MB4XDTIwMDQwMTE3NTIzNVoXDTQwMDMyNzE3NTIzNVowXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23+QPCxzD9uXJkuTuwr4oSE+yGHZJMheH3U+2pPbMRqRgLm/5QzLPLsORGIm+gQptknnb+Ab5g1ozSVuw3YI9UoLrnp0PMSpC7PPYg/7tLz324ReKOtHDfHti6z1n7AJOKNue8smUAoa4YnRcnYLOUzLT27As1+3lbq5qF1KdKvvb0GlfgmNuj09zXBX2O3v1dp3yJMEHO8JiHhlzoHyjXLnBxpuJhL3MrENuziQawbE/A3llVDNkci6JfRYyYzhcdtKRfMtGZYDVoGmRO51d1tTz3isXbo+X1ArXCmM3cLXKhffIrTX5Hior6htp8HaaC1mzM8pC1As48L75l8SwQIDAQABozswOTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIChDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAsgDgPPHo6WK9wNYdQJ5XuTiQd3ZS0qhLcG64Z5n7s4pVn+8dKLhfKtFznzVHN7tG03YQ8vBp7M1imXH5YIqESDjEvYtnJbmrbDNlrdjCmnhID+nMwScNxs9kPG2AWTOMyjYGKhEbjUnOCP9mwEcoS+tawSsJViylqgkDezIx3OiFeEjOwMUSEWoPDK4vBcpvemR/ICx15kyxEtP94x9eDX24WNegfOR/Y6uXmivDKtjQsuHVWg05G29nKKkSg9aHeG2ZvV6zCuCYzvbqw45taeu3QIE9hz1wUdHEXY2l3H9qWBreYHY3Uuz/rBldDBUvig/1icjXKx0e7CuRBac9TzGCAY8wggGLAgEBMGQwXzERMA8GA1UEAwwIU3RvcmVLaXQxETAPBgNVBAoMCFN0b3JlS2l0MREwDwYDVQQLDAhTdG9yZUtpdDELMAkGA1UEBhMCVVMxFzAVBgkqhkiG9w0BCQEWCFN0b3JlS2l0AgEBMA0GCWCGSAFlAwQCAQUAMA0GCSqGSIb3DQEBCwUABIIBAMNY9TpOCg59NnKdDA6Xc4D74lEaa+YwQqD/z8ajAGxpw3efoQRvx8Q1qR6IVs9BcRYGyJmsFrau19QeSIRjjqaxhV8ZbRFenWp0Yps6OCPVHw94Ej3AstAL/8WIArBM1OS6OZJESJdQz5xpwavWLGm1rU2730glMdHzHfm2h0wNp/0BKV0ugV9SRQN4RsyAMNS+rCO1mtSDI6nx8E+dEVMIa4mUg+yhXRlg6KzdzKWnr9vDtRVmhdq0ANfP+jfvncsyC+d/c3cAsXOK066hKFwYWTKaRZ7M2eXus5TcU83/aaovHyKVyKKCRnKuP7VPt9d5eWLSg/7v2ctHJtjmhqsAAAAAAAA= ================================================ FILE: tests/resources/xcode/xcode-signed-app-transaction ================================================ eyJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFwcGxlX1hjb2RlX0tleSJ9.eyJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJhcHBsaWNhdGlvblZlcnNpb24iOiIxIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiI0OGM4YjkyZC1jZTBkLTQyMjktYmVkZi1lNjFiNGY5Y2ZjOTIiLCJyZWNlaXB0VHlwZSI6Ilhjb2RlIiwicmVjZWlwdENyZWF0aW9uRGF0ZSI6MTY5NzY4MDEyMjI1Ny40NDcsImRldmljZVZlcmlmaWNhdGlvbiI6ImNZVXNYYzUzRWJZYzBwT2VYRzVkNlwvMzFMR0hlVkdmODRzcVNOME9ySmk1dVwvajJIODlXV0tnUzhOMGhNc01sZiIsInJlcXVlc3REYXRlIjoxNjk3NjgwMTIyMjU3LjQ0Nywib3JpZ2luYWxBcHBsaWNhdGlvblZlcnNpb24iOiIxIiwib3JpZ2luYWxQdXJjaGFzZURhdGUiOi02MjEzNTc2OTYwMDAwMH0.Dpdk_VsO2MUCevwyS407alJpPc1Nq_UIP9EiDHaQBxlyi35NFnsKUVNuFNcGWrGRCCImnb4QGBKHfQC2i4sPCg ================================================ FILE: tests/resources/xcode/xcode-signed-renewal-info ================================================ eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiJ9.eyJkZXZpY2VWZXJpZmljYXRpb24iOiJ1K1cxb1FUcXZGSE9RK1pCZTRRMHhQTUMyOGtxRUZ2YmJzRVBwTEtEVlJGdjFHSkdlZ21yTkhWb09ZTU9QdmIyIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiIzNDM5OTE5ZS04N2M5LTQ3YjYtYWVlZS0yODIzZjdhOWQzYzMiLCJyZW5ld2FsRGF0ZSI6MTcwMDM1ODMzNjA0OS43Mjk3LCJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiIwIiwicmVjZW50U3Vic2NyaXB0aW9uU3RhcnREYXRlIjoxNjk3Njc5OTM2MDQ5LjcyOTcsImF1dG9SZW5ld1N0YXR1cyI6MSwic2lnbmVkRGF0ZSI6MTY5NzY3OTkzNjcxMS4wNzQ3LCJlbnZpcm9ubWVudCI6Ilhjb2RlIiwiYXV0b1JlbmV3UHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIn0.WnT3aB9Lwjbr0ICUGn_5CdglzedVd7eOkrqirhcWFvwJZzN1FajuMV6gFEbgD82aL0Ix6HGZcwkNDlVNLvYOEQ ================================================ FILE: tests/resources/xcode/xcode-signed-transaction ================================================ eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlCekRDQ0FYR2dBd0lCQWdJQkFUQUtCZ2dxaGtqT1BRUURBakJJTVNJd0lBWURWUVFERXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTVNJd0lBWURWUVFLRXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTUI0WERUSXpNVEF4T1RBeE5EVXpObG9YRFRJME1UQXhPREF4TkRVek5sb3dTREVpTUNBR0ExVUVBeE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEVpTUNBR0ExVUVDaE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCS1hFUWdZakNvdVB0VHN0R3JLcFk4STUzbkg3cmJESG5jSUxHbm9nU0F1bElJM3NcL3VmTTBnOURjM0JjcjQ5N0FVZ3pHVHZXcGl3SnhwZUIzNzFOZ1YralREQktNQklHQTFVZEV3RUJcL3dRSU1BWUJBZjhDQVFBd0pBWURWUjBSQkIwd0c0RVpVM1J2Y21WTGFYUWdWR1Z6ZEdsdVp5QnBiaUJZWTI5a1pUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0NnWUlLb1pJemowRUF3SURTUUF3UmdJaEFNWnZWWUo2NENGK2gyZm1zbXd2ekFjZVBySUQxM3JwSUpHQkVXK1dncHB0QWlFQXhXaXk0K1QxenQzN1ZzdTB2YjZZdW0wK05MdERxSGxLNnJwTWN2NkpmbkE9Il19.eyJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJwdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5Nywic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiNkYzQTkzQUIiLCJzaWduZWREYXRlIjoxNjk3Njc5OTM2MDU2LjQ4NSwib3JpZ2luYWxQdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5NywiaXNVcGdyYWRlZCI6ZmFsc2UsImRldmljZVZlcmlmaWNhdGlvbiI6InNHRG5wZytvemI4dXdEU3VDRFoyb1ZabzFDS3JiQjh1alI4VnhDeGh5a1J3eUJJSzZ4NlhDeUVSbTh5V3J6RTgiLCJvZmZlclR5cGUiOjEsInF1YW50aXR5IjoxLCJ0cmFuc2FjdGlvbklkIjoiMCIsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJ0cmFuc2FjdGlvblJlYXNvbiI6IlBVUkNIQVNFIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZXhwaXJlc0RhdGUiOjE3MDAzNTgzMzYwNDkuNzI5NywiZW52aXJvbm1lbnQiOiJYY29kZSIsInN0b3JlZnJvbnRJZCI6IjE0MzQ0MSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjAiLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJkZXZpY2VWZXJpZmljYXRpb25Ob25jZSI6IjdlZGVhODdkLTk4ZjAtNDJkMC05NjgyLTQ5Y2E4MTAyMmY3MyIsIndlYk9yZGVyTGluZUl0ZW1JZCI6IjAiLCJzdG9yZWZyb250IjoiVVNBIn0.rkJYnvujStteRkMHhoIR2ThmNFnyKcx5XxIakXYdh-1oKtEVEU5zQAiONaLDpBDO5JhLLrTbfp7LS5tMiqmgHw ================================================ FILE: tests/test_advanced_commerce_models.py ================================================ # Copyright (c) 2026 Apple Inc. Licensed under MIT License. import json import unittest from appstoreserverlibrary.models.AdvancedCommerceDescriptors import AdvancedCommerceDescriptors from appstoreserverlibrary.models.AdvancedCommerceEffective import AdvancedCommerceEffective from appstoreserverlibrary.models.AdvancedCommerceOffer import AdvancedCommerceOffer from appstoreserverlibrary.models.AdvancedCommerceOfferPeriod import AdvancedCommerceOfferPeriod from appstoreserverlibrary.models.AdvancedCommerceOfferReason import AdvancedCommerceOfferReason from appstoreserverlibrary.models.AdvancedCommerceOneTimeChargeCreateRequest import AdvancedCommerceOneTimeChargeCreateRequest from appstoreserverlibrary.models.AdvancedCommerceOneTimeChargeItem import AdvancedCommerceOneTimeChargeItem from appstoreserverlibrary.models.AdvancedCommercePeriod import AdvancedCommercePeriod from appstoreserverlibrary.models.AdvancedCommerceReason import AdvancedCommerceReason from appstoreserverlibrary.models.AdvancedCommerceRefundReason import AdvancedCommerceRefundReason from appstoreserverlibrary.models.AdvancedCommerceRefundType import AdvancedCommerceRefundType from appstoreserverlibrary.models.AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo from appstoreserverlibrary.models.AdvancedCommerceRequestRefundItem import AdvancedCommerceRequestRefundItem from appstoreserverlibrary.models.AdvancedCommerceRequestRefundRequest import AdvancedCommerceRequestRefundRequest from appstoreserverlibrary.models.AdvancedCommerceRequestRefundResponse import AdvancedCommerceRequestRefundResponse from appstoreserverlibrary.models.AdvancedCommerceSubscriptionCancelRequest import AdvancedCommerceSubscriptionCancelRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionCancelResponse import AdvancedCommerceSubscriptionCancelResponse from appstoreserverlibrary.models.AdvancedCommerceSubscriptionChangeMetadataDescriptors import AdvancedCommerceSubscriptionChangeMetadataDescriptors from appstoreserverlibrary.models.AdvancedCommerceSubscriptionChangeMetadataItem import AdvancedCommerceSubscriptionChangeMetadataItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionChangeMetadataRequest import AdvancedCommerceSubscriptionChangeMetadataRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionChangeMetadataResponse import AdvancedCommerceSubscriptionChangeMetadataResponse from appstoreserverlibrary.models.AdvancedCommerceSubscriptionCreateItem import AdvancedCommerceSubscriptionCreateItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionCreateRequest import AdvancedCommerceSubscriptionCreateRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateDescriptors import AdvancedCommerceSubscriptionMigrateDescriptors from appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateItem import AdvancedCommerceSubscriptionMigrateItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateRenewalItem import AdvancedCommerceSubscriptionMigrateRenewalItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateRequest import AdvancedCommerceSubscriptionMigrateRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateResponse import AdvancedCommerceSubscriptionMigrateResponse from appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyAddItem import AdvancedCommerceSubscriptionModifyAddItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyChangeItem import AdvancedCommerceSubscriptionModifyChangeItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyDescriptors import AdvancedCommerceSubscriptionModifyDescriptors from appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyInAppRequest import AdvancedCommerceSubscriptionModifyInAppRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyPeriodChange import AdvancedCommerceSubscriptionModifyPeriodChange from appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyRemoveItem import AdvancedCommerceSubscriptionModifyRemoveItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionPriceChangeItem import AdvancedCommerceSubscriptionPriceChangeItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionPriceChangeRequest import AdvancedCommerceSubscriptionPriceChangeRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionPriceChangeResponse import AdvancedCommerceSubscriptionPriceChangeResponse from appstoreserverlibrary.models.AdvancedCommerceSubscriptionReactivateInAppRequest import AdvancedCommerceSubscriptionReactivateInAppRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionReactivateItem import AdvancedCommerceSubscriptionReactivateItem from appstoreserverlibrary.models.AdvancedCommerceSubscriptionRevokeRequest import AdvancedCommerceSubscriptionRevokeRequest from appstoreserverlibrary.models.AdvancedCommerceSubscriptionRevokeResponse import AdvancedCommerceSubscriptionRevokeResponse from appstoreserverlibrary.models.AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter from tests.util import read_data_from_file class AdvancedCommerceModelsTest(unittest.TestCase): def test_advanced_commerce_period(self): self.assertEqual("P1W", AdvancedCommercePeriod.P1W.value) self.assertEqual("P1M", AdvancedCommercePeriod.P1M.value) self.assertEqual("P2M", AdvancedCommercePeriod.P2M.value) self.assertEqual("P3M", AdvancedCommercePeriod.P3M.value) self.assertEqual("P6M", AdvancedCommercePeriod.P6M.value) self.assertEqual("P1Y", AdvancedCommercePeriod.P1Y.value) self.assertEqual(AdvancedCommercePeriod.P1W, AdvancedCommercePeriod("P1W")) self.assertEqual(AdvancedCommercePeriod.P1M, AdvancedCommercePeriod("P1M")) self.assertEqual(AdvancedCommercePeriod.P1Y, AdvancedCommercePeriod("P1Y")) self.assertFalse("INVALID" in AdvancedCommercePeriod) self.assertEqual("P1W", AdvancedCommercePeriod.P1W.value) def test_advanced_commerce_reason(self): self.assertEqual("UPGRADE", AdvancedCommerceReason.UPGRADE.value) self.assertEqual("DOWNGRADE", AdvancedCommerceReason.DOWNGRADE.value) self.assertEqual("APPLY_OFFER", AdvancedCommerceReason.APPLY_OFFER.value) self.assertEqual(AdvancedCommerceReason.UPGRADE, AdvancedCommerceReason("UPGRADE")) self.assertEqual(AdvancedCommerceReason.DOWNGRADE, AdvancedCommerceReason("DOWNGRADE")) self.assertEqual(AdvancedCommerceReason.APPLY_OFFER, AdvancedCommerceReason("APPLY_OFFER")) self.assertFalse("INVALID" in AdvancedCommerceReason) self.assertEqual("UPGRADE", AdvancedCommerceReason.UPGRADE.value) def test_advanced_commerce_refund_reason(self): self.assertEqual("UNINTENDED_PURCHASE", AdvancedCommerceRefundReason.UNINTENDED_PURCHASE.value) self.assertEqual("FULFILLMENT_ISSUE", AdvancedCommerceRefundReason.FULFILLMENT_ISSUE.value) self.assertEqual("UNSATISFIED_WITH_PURCHASE", AdvancedCommerceRefundReason.UNSATISFIED_WITH_PURCHASE.value) self.assertEqual("LEGAL", AdvancedCommerceRefundReason.LEGAL.value) self.assertEqual("OTHER", AdvancedCommerceRefundReason.OTHER.value) self.assertEqual("MODIFY_ITEMS_REFUND", AdvancedCommerceRefundReason.MODIFY_ITEMS_REFUND.value) self.assertEqual("SIMULATE_REFUND_DECLINE", AdvancedCommerceRefundReason.SIMULATE_REFUND_DECLINE.value) self.assertEqual(AdvancedCommerceRefundReason.LEGAL, AdvancedCommerceRefundReason("LEGAL")) self.assertEqual(AdvancedCommerceRefundReason.OTHER, AdvancedCommerceRefundReason("OTHER")) self.assertFalse("INVALID" in AdvancedCommerceRefundReason) self.assertEqual("LEGAL", AdvancedCommerceRefundReason.LEGAL.value) def test_advanced_commerce_refund_type(self): self.assertEqual("FULL", AdvancedCommerceRefundType.FULL.value) self.assertEqual("PRORATED", AdvancedCommerceRefundType.PRORATED.value) self.assertEqual("CUSTOM", AdvancedCommerceRefundType.CUSTOM.value) self.assertEqual(AdvancedCommerceRefundType.FULL, AdvancedCommerceRefundType("FULL")) self.assertEqual(AdvancedCommerceRefundType.PRORATED, AdvancedCommerceRefundType("PRORATED")) self.assertEqual(AdvancedCommerceRefundType.CUSTOM, AdvancedCommerceRefundType("CUSTOM")) self.assertFalse("INVALID" in AdvancedCommerceRefundType) self.assertEqual("FULL", AdvancedCommerceRefundType.FULL.value) def test_advanced_commerce_offer_period(self): self.assertEqual("P3D", AdvancedCommerceOfferPeriod.P3D.value) self.assertEqual("P1W", AdvancedCommerceOfferPeriod.P1W.value) self.assertEqual("P2W", AdvancedCommerceOfferPeriod.P2W.value) self.assertEqual("P1M", AdvancedCommerceOfferPeriod.P1M.value) self.assertEqual("P2M", AdvancedCommerceOfferPeriod.P2M.value) self.assertEqual("P3M", AdvancedCommerceOfferPeriod.P3M.value) self.assertEqual(AdvancedCommerceOfferPeriod.P1W, AdvancedCommerceOfferPeriod("P1W")) self.assertEqual(AdvancedCommerceOfferPeriod.P1M, AdvancedCommerceOfferPeriod("P1M")) self.assertEqual(AdvancedCommerceOfferPeriod.P3D, AdvancedCommerceOfferPeriod("P3D")) self.assertFalse("INVALID" in AdvancedCommerceOfferPeriod) self.assertEqual("P1W", AdvancedCommerceOfferPeriod.P1W.value) def test_advanced_commerce_offer_reason(self): self.assertEqual("ACQUISITION", AdvancedCommerceOfferReason.ACQUISITION.value) self.assertEqual("WIN_BACK", AdvancedCommerceOfferReason.WIN_BACK.value) self.assertEqual("RETENTION", AdvancedCommerceOfferReason.RETENTION.value) self.assertEqual(AdvancedCommerceOfferReason.ACQUISITION, AdvancedCommerceOfferReason("ACQUISITION")) self.assertEqual(AdvancedCommerceOfferReason.WIN_BACK, AdvancedCommerceOfferReason("WIN_BACK")) self.assertEqual(AdvancedCommerceOfferReason.RETENTION, AdvancedCommerceOfferReason("RETENTION")) self.assertFalse("INVALID" in AdvancedCommerceOfferReason) self.assertEqual("WIN_BACK", AdvancedCommerceOfferReason.WIN_BACK.value) def test_advanced_commerce_effective(self): self.assertEqual("IMMEDIATELY", AdvancedCommerceEffective.IMMEDIATELY.value) self.assertEqual("NEXT_BILL_CYCLE", AdvancedCommerceEffective.NEXT_BILL_CYCLE.value) self.assertEqual(AdvancedCommerceEffective.IMMEDIATELY, AdvancedCommerceEffective("IMMEDIATELY")) self.assertEqual(AdvancedCommerceEffective.NEXT_BILL_CYCLE, AdvancedCommerceEffective("NEXT_BILL_CYCLE")) self.assertFalse("INVALID" in AdvancedCommerceEffective) self.assertEqual("IMMEDIATELY", AdvancedCommerceEffective.IMMEDIATELY.value) def test_validation_utils_description(self): valid_description = "Valid description" AdvancedCommerceValidationUtils.description_validator(None, None, valid_description) max_length_description = "A" * 45 AdvancedCommerceValidationUtils.description_validator(None, None, max_length_description) too_long_description = "A" * 46 with self.assertRaises(ValueError): AdvancedCommerceValidationUtils.description_validator(None, None, too_long_description) def test_validation_utils_display_name(self): valid_display_name = "Valid Name" AdvancedCommerceValidationUtils.display_name_validator(None, None, valid_display_name) max_length_display_name = "A" * 30 AdvancedCommerceValidationUtils.display_name_validator(None, None, max_length_display_name) too_long_display_name = "A" * 31 with self.assertRaises(ValueError): AdvancedCommerceValidationUtils.display_name_validator(None, None, too_long_display_name) def test_validation_utils_sku(self): valid_sku = "valid.sku.123" AdvancedCommerceValidationUtils.sku_validator(None, None, valid_sku) max_length_sku = "A" * 128 AdvancedCommerceValidationUtils.sku_validator(None, None, max_length_sku) too_long_sku = "A" * 129 with self.assertRaises(ValueError): AdvancedCommerceValidationUtils.sku_validator(None, None, too_long_sku) def test_validation_utils_period_count(self): AdvancedCommerceValidationUtils.period_count_validator(None, None, 1) AdvancedCommerceValidationUtils.period_count_validator(None, None, 6) AdvancedCommerceValidationUtils.period_count_validator(None, None, 12) with self.assertRaises(ValueError): AdvancedCommerceValidationUtils.period_count_validator(None, None, 0) with self.assertRaises(ValueError): AdvancedCommerceValidationUtils.period_count_validator(None, None, 13) def test_validation_utils_items(self): valid_list = [ AdvancedCommerceOneTimeChargeItem( description="desc", displayName="name", SKU="sku1", price=1000 ) ] AdvancedCommerceValidationUtils.items_validator(None, None, valid_list) with self.assertRaises(ValueError): AdvancedCommerceValidationUtils.items_validator(None, None, None) with self.assertRaises(ValueError): AdvancedCommerceValidationUtils.items_validator(None, None, []) list_with_none = [None] with self.assertRaises(ValueError): AdvancedCommerceValidationUtils.items_validator(None, None, list_with_none) def test_advanced_commerce_descriptors_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceDescriptors.json') descriptors_dict = json.loads(json_data) descriptors = _get_cattrs_converter(AdvancedCommerceDescriptors).structure(descriptors_dict, AdvancedCommerceDescriptors) self.assertEqual("description", descriptors.description) self.assertEqual("display name", descriptors.displayName) def test_advanced_commerce_one_time_charge_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceOneTimeChargeItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceOneTimeChargeItem).structure(item_dict, AdvancedCommerceOneTimeChargeItem) self.assertEqual("description", item.description) self.assertEqual("display name", item.displayName) self.assertEqual("sku", item.SKU) self.assertEqual(15000, item.price) def test_advanced_commerce_subscription_create_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCreateItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionCreateItem).structure(item_dict, AdvancedCommerceSubscriptionCreateItem) self.assertEqual("description", item.description) self.assertEqual("display name", item.displayName) self.assertEqual("sku", item.SKU) self.assertEqual(20000, item.price) def test_advanced_commerce_request_refund_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestRefundItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceRequestRefundItem).structure(item_dict, AdvancedCommerceRequestRefundItem) self.assertEqual("sku", item.SKU) self.assertEqual(AdvancedCommerceRefundReason.LEGAL, item.refundReason) self.assertEqual("LEGAL", item.rawRefundReason) self.assertEqual(AdvancedCommerceRefundType.FULL, item.refundType) self.assertEqual("FULL", item.rawRefundType) self.assertTrue(item.revoke) self.assertEqual(5000, item.refundAmount) def test_advanced_commerce_offer_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceOffer.json') offer_dict = json.loads(json_data) offer = _get_cattrs_converter(AdvancedCommerceOffer).structure(offer_dict, AdvancedCommerceOffer) self.assertEqual(AdvancedCommerceOfferPeriod.P1W, offer.period) self.assertEqual("P1W", offer.rawPeriod) self.assertEqual(3, offer.periodCount) self.assertEqual(5000, offer.price) self.assertEqual(AdvancedCommerceOfferReason.WIN_BACK, offer.reason) self.assertEqual("WIN_BACK", offer.rawReason) def test_advanced_commerce_one_time_charge_create_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceOneTimeChargeCreateRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceOneTimeChargeCreateRequest).structure(request_dict, AdvancedCommerceOneTimeChargeCreateRequest) self.assertEqual("USD", request.currency) self.assertIsNotNone(request.item) self.assertEqual("description", request.item.description) self.assertEqual("display name", request.item.displayName) self.assertEqual("sku", request.item.SKU) self.assertEqual(10000, request.item.price) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440000", str(request.requestInfo.requestReferenceId)) self.assertEqual("taxCode", request.taxCode) self.assertEqual("USA", request.storefront) self.assertEqual("CREATE_ONE_TIME_CHARGE", request.operation) self.assertEqual("1", request.version) def test_advanced_commerce_subscription_create_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCreateRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceSubscriptionCreateRequest).structure(request_dict, AdvancedCommerceSubscriptionCreateRequest) self.assertEqual("USD", request.currency) self.assertIsNotNone(request.descriptors) self.assertEqual("description", request.descriptors.description) self.assertEqual("display name", request.descriptors.displayName) self.assertEqual(2, len(request.items)) self.assertEqual("sku", request.items[0].SKU) self.assertEqual(20000, request.items[0].price) self.assertEqual("sku", request.items[1].SKU) self.assertEqual(30000, request.items[1].price) self.assertEqual("P1M", request.period) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440001", str(request.requestInfo.requestReferenceId)) self.assertEqual("taxCode", request.taxCode) self.assertEqual("USA", request.storefront) self.assertEqual("transactionId", request.previousTransactionId) def test_advanced_commerce_request_refund_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestRefundRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceRequestRefundRequest).structure(request_dict, AdvancedCommerceRequestRefundRequest) self.assertEqual(2, len(request.items)) self.assertEqual("sku", request.items[0].SKU) self.assertEqual(AdvancedCommerceRefundReason.LEGAL, request.items[0].refundReason) self.assertEqual(AdvancedCommerceRefundType.FULL, request.items[0].refundType) self.assertTrue(request.items[0].revoke) self.assertEqual("sku", request.items[1].SKU) self.assertEqual(AdvancedCommerceRefundReason.OTHER, request.items[1].refundReason) self.assertEqual(AdvancedCommerceRefundType.PRORATED, request.items[1].refundType) self.assertFalse(request.items[1].revoke) self.assertTrue(request.refundRiskingPreference) self.assertEqual("550e8400-e29b-41d4-a716-446655440002", str(request.requestInfo.requestReferenceId)) self.assertEqual("USD", request.currency) self.assertEqual("USA", request.storefront) def test_advanced_commerce_subscription_cancel_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCancelRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceSubscriptionCancelRequest).structure(request_dict, AdvancedCommerceSubscriptionCancelRequest) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440003", str(request.requestInfo.requestReferenceId)) self.assertEqual("USA", request.storefront) def test_advanced_commerce_subscription_revoke_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionRevokeRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceSubscriptionRevokeRequest).structure(request_dict, AdvancedCommerceSubscriptionRevokeRequest) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440004", str(request.requestInfo.requestReferenceId)) self.assertTrue(request.refundRiskingPreference) self.assertEqual("USA", request.storefront) def test_advanced_commerce_subscription_price_change_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionPriceChangeRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceSubscriptionPriceChangeRequest).structure(request_dict, AdvancedCommerceSubscriptionPriceChangeRequest) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440005", str(request.requestInfo.requestReferenceId)) self.assertEqual(1, len(request.items)) self.assertEqual("sku123", request.items[0].SKU) self.assertEqual(15000, request.items[0].price) self.assertEqual("USD", request.currency) def test_advanced_commerce_request_refund_response_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestRefundResponse.json') response_dict = json.loads(json_data) response = _get_cattrs_converter(AdvancedCommerceRequestRefundResponse).structure(response_dict, AdvancedCommerceRequestRefundResponse) self.assertEqual("signed_transaction_info_value", response.signedTransactionInfo) def test_advanced_commerce_subscription_cancel_response_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCancelResponse.json') response_dict = json.loads(json_data) response = _get_cattrs_converter(AdvancedCommerceSubscriptionCancelResponse).structure(response_dict, AdvancedCommerceSubscriptionCancelResponse) self.assertEqual("signed_renewal_info", response.signedRenewalInfo) self.assertEqual("signed_transaction_info", response.signedTransactionInfo) def test_advanced_commerce_subscription_revoke_response_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionRevokeResponse.json') response_dict = json.loads(json_data) response = _get_cattrs_converter(AdvancedCommerceSubscriptionRevokeResponse).structure(response_dict, AdvancedCommerceSubscriptionRevokeResponse) self.assertEqual("signed_renewal_info", response.signedRenewalInfo) self.assertEqual("signed_transaction_info", response.signedTransactionInfo) def test_advanced_commerce_subscription_price_change_response_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionPriceChangeResponse.json') response_dict = json.loads(json_data) response = _get_cattrs_converter(AdvancedCommerceSubscriptionPriceChangeResponse).structure(response_dict, AdvancedCommerceSubscriptionPriceChangeResponse) self.assertEqual("signed_renewal_info", response.signedRenewalInfo) self.assertEqual("signed_transaction_info", response.signedTransactionInfo) def test_advanced_commerce_subscription_change_metadata_response_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataResponse.json') response_dict = json.loads(json_data) response = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataResponse).structure(response_dict, AdvancedCommerceSubscriptionChangeMetadataResponse) self.assertEqual("signed_renewal_info", response.signedRenewalInfo) self.assertEqual("signed_transaction_info", response.signedTransactionInfo) def test_advanced_commerce_subscription_migrate_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateRequest).structure(request_dict, AdvancedCommerceSubscriptionMigrateRequest) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440006", str(request.requestInfo.requestReferenceId)) self.assertIsNotNone(request.descriptors) self.assertEqual("description", request.descriptors.description) self.assertEqual("display name", request.descriptors.displayName) self.assertEqual(1, len(request.items)) self.assertEqual("sku", request.items[0].SKU) self.assertEqual("targetProductId", request.targetProductId) self.assertEqual("taxCode", request.taxCode) def test_advanced_commerce_subscription_modify_in_app_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyInAppRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyInAppRequest).structure(request_dict, AdvancedCommerceSubscriptionModifyInAppRequest) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440007", str(request.requestInfo.requestReferenceId)) self.assertEqual("transactionId", request.transactionId) self.assertTrue(request.retainBillingCycle) self.assertIsNotNone(request.descriptors) self.assertEqual("description", request.descriptors.description) self.assertEqual("display name", request.descriptors.displayName) self.assertEqual("taxCode", request.taxCode) self.assertEqual("USD", request.currency) def test_advanced_commerce_subscription_reactivate_in_app_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionReactivateInAppRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceSubscriptionReactivateInAppRequest).structure(request_dict, AdvancedCommerceSubscriptionReactivateInAppRequest) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440008", str(request.requestInfo.requestReferenceId)) self.assertEqual("transactionId", request.transactionId) self.assertEqual(1, len(request.items)) self.assertEqual("sku", request.items[0].SKU) def test_advanced_commerce_subscription_change_metadata_request_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataRequest.json') request_dict = json.loads(json_data) request = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataRequest).structure(request_dict, AdvancedCommerceSubscriptionChangeMetadataRequest) self.assertIsNotNone(request.requestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440009", str(request.requestInfo.requestReferenceId)) self.assertEqual(1, len(request.items)) self.assertEqual("currentSKU", request.items[0].currentSKU) self.assertEqual("sku", request.items[0].SKU) def test_advanced_commerce_subscription_migrate_descriptors_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateDescriptors.json') descriptors_dict = json.loads(json_data) descriptors = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateDescriptors).structure(descriptors_dict, AdvancedCommerceSubscriptionMigrateDescriptors) self.assertEqual("description", descriptors.description) self.assertEqual("displayName", descriptors.displayName) def test_advanced_commerce_subscription_modify_descriptors_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyDescriptors.json') descriptors_dict = json.loads(json_data) descriptors = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyDescriptors).structure(descriptors_dict, AdvancedCommerceSubscriptionModifyDescriptors) self.assertEqual("description", descriptors.description) self.assertEqual("displayName", descriptors.displayName) def test_advanced_commerce_subscription_change_metadata_descriptors_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataDescriptors.json') descriptors_dict = json.loads(json_data) descriptors = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataDescriptors).structure(descriptors_dict, AdvancedCommerceSubscriptionChangeMetadataDescriptors) self.assertEqual("description", descriptors.description) self.assertEqual("displayName", descriptors.displayName) def test_advanced_commerce_subscription_change_metadata_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataItem).structure(item_dict, AdvancedCommerceSubscriptionChangeMetadataItem) self.assertEqual("currentSku", item.currentSKU) self.assertEqual("sku", item.SKU) self.assertEqual("description", item.description) self.assertEqual("displayName", item.displayName) def test_advanced_commerce_subscription_migrate_renewal_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateRenewalItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateRenewalItem).structure(item_dict, AdvancedCommerceSubscriptionMigrateRenewalItem) self.assertEqual("sku", item.SKU) self.assertEqual("description", item.description) self.assertEqual("displayName", item.displayName) def test_advanced_commerce_subscription_modify_add_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyAddItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyAddItem).structure(item_dict, AdvancedCommerceSubscriptionModifyAddItem) self.assertEqual("sku", item.SKU) self.assertEqual("description", item.description) self.assertEqual("displayName", item.displayName) self.assertEqual(12000, item.price) def test_advanced_commerce_subscription_modify_change_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyChangeItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyChangeItem).structure(item_dict, AdvancedCommerceSubscriptionModifyChangeItem) self.assertEqual("currentSku", item.currentSKU) self.assertEqual("sku", item.SKU) self.assertEqual("description", item.description) self.assertEqual("displayName", item.displayName) self.assertEqual(13000, item.price) def test_advanced_commerce_subscription_modify_remove_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyRemoveItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyRemoveItem).structure(item_dict, AdvancedCommerceSubscriptionModifyRemoveItem) self.assertEqual("sku", item.SKU) def test_advanced_commerce_subscription_modify_period_change_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyPeriodChange.json') change_dict = json.loads(json_data) change = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyPeriodChange).structure(change_dict, AdvancedCommerceSubscriptionModifyPeriodChange) self.assertEqual("P3M", change.period) def test_advanced_commerce_subscription_price_change_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionPriceChangeItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionPriceChangeItem).structure(item_dict, AdvancedCommerceSubscriptionPriceChangeItem) self.assertEqual("sku", item.SKU) self.assertEqual(16000, item.price) self.assertEqual("dependentSKU", item.dependentSKUs[0]) def test_advanced_commerce_subscription_price_change_item_dependent_sku_validation(self): valid_sku = "A" * 128 too_long_sku = "A" * 129 # Valid SKU in dependentSKUs is accepted item = AdvancedCommerceSubscriptionPriceChangeItem(SKU="sku", price=1000, dependentSKUs=[valid_sku]) self.assertEqual(valid_sku, item.dependentSKUs[0]) # Too-long SKU in dependentSKUs raises ValueError with self.assertRaises(ValueError): AdvancedCommerceSubscriptionPriceChangeItem(SKU="sku", price=1000, dependentSKUs=[too_long_sku]) # None list is allowed (field is optional) item_none = AdvancedCommerceSubscriptionPriceChangeItem(SKU="sku", price=1000, dependentSKUs=None) self.assertIsNone(item_none.dependentSKUs) def test_advanced_commerce_subscription_reactivate_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionReactivateItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionReactivateItem).structure(item_dict, AdvancedCommerceSubscriptionReactivateItem) self.assertEqual("sku", item.SKU) def test_advanced_commerce_request_info_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestInfo.json') info_dict = json.loads(json_data) info = _get_cattrs_converter(AdvancedCommerceRequestInfo).structure(info_dict, AdvancedCommerceRequestInfo) self.assertEqual("550e8400-e29b-41d4-a716-446655440010", str(info.requestReferenceId)) self.assertEqual("660e8400-e29b-41d4-a716-446655440011", str(info.appAccountToken)) self.assertEqual("consistency_token_value", info.consistencyToken) def test_advanced_commerce_subscription_migrate_item_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateItem.json') item_dict = json.loads(json_data) item = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateItem).structure(item_dict, AdvancedCommerceSubscriptionMigrateItem) self.assertEqual("sku", item.SKU) self.assertEqual("description", item.description) self.assertEqual("displayName", item.displayName) def test_advanced_commerce_subscription_migrate_response_deserialization(self): json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateResponse.json') response_dict = json.loads(json_data) response = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateResponse).structure(response_dict, AdvancedCommerceSubscriptionMigrateResponse) self.assertEqual("signed_renewal_info_value", response.signedRenewalInfo) self.assertEqual("signed_transaction_info_value", response.signedTransactionInfo) ================================================ FILE: tests/test_api_client.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Any, Dict, List, Union import unittest from requests import Response from appstoreserverlibrary.api_client import APIError, APIException, AppStoreServerAPIClient, GetTransactionHistoryVersion from appstoreserverlibrary.models.AccountTenure import AccountTenure from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus from appstoreserverlibrary.models.ConsumptionRequest import ConsumptionRequest 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.Environment import Environment from appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent from appstoreserverlibrary.models.ExtendReasonCode import ExtendReasonCode from appstoreserverlibrary.models.ExtendRenewalDateRequest import ExtendRenewalDateRequest from appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType from appstoreserverlibrary.models.LastTransactionsItem import LastTransactionsItem from appstoreserverlibrary.models.LifetimeDollarsPurchased import LifetimeDollarsPurchased from appstoreserverlibrary.models.LifetimeDollarsRefunded import LifetimeDollarsRefunded from appstoreserverlibrary.models.MassExtendRenewalDateRequest import MassExtendRenewalDateRequest from appstoreserverlibrary.models.NotificationHistoryRequest import NotificationHistoryRequest from appstoreserverlibrary.models.NotificationHistoryResponseItem import NotificationHistoryResponseItem from appstoreserverlibrary.models.NotificationTypeV2 import NotificationTypeV2 from appstoreserverlibrary.models.OfferType import OfferType from appstoreserverlibrary.models.OrderLookupStatus import OrderLookupStatus from appstoreserverlibrary.models.Platform import Platform from appstoreserverlibrary.models.PlayTime import PlayTime from appstoreserverlibrary.models.RefundPreference import RefundPreference from appstoreserverlibrary.models.RefundPreferenceV1 import RefundPreferenceV1 from appstoreserverlibrary.models.SendAttemptItem import SendAttemptItem from appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult from appstoreserverlibrary.models.Status import Status from appstoreserverlibrary.models.SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem from appstoreserverlibrary.models.Subtype import Subtype from appstoreserverlibrary.models.TransactionHistoryRequest import Order, ProductType, TransactionHistoryRequest from appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest from appstoreserverlibrary.models.UserStatus import UserStatus from appstoreserverlibrary.models.DefaultConfigurationRequest import DefaultConfigurationRequest from appstoreserverlibrary.models.HeaderPosition import HeaderPosition from appstoreserverlibrary.models.ImageSize import ImageSize from appstoreserverlibrary.models.ImageState import ImageState from appstoreserverlibrary.models.MessageState import MessageState from appstoreserverlibrary.models.BulletPoint import BulletPoint from appstoreserverlibrary.models.PerformanceTestRequest import PerformanceTestRequest from appstoreserverlibrary.models.PerformanceTestStatus import PerformanceTestStatus from appstoreserverlibrary.models.RealtimeUrlRequest import RealtimeUrlRequest from appstoreserverlibrary.models.UploadMessageImage import UploadMessageImage from appstoreserverlibrary.models.UploadMessageRequestBody import UploadMessageRequestBody from uuid import UUID from tests.util import decode_json_from_signed_date, read_data_from_binary_file, read_data_from_file from io import BytesIO class DecodedPayloads(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', 'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass', {}, {'extendByDays': 45, 'extendReasonCode': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac', 'storefrontCountryCodes': ['USA', 'MEX'], 'productId': 'com.example.productId'}) extend_renewal_date_request = MassExtendRenewalDateRequest( extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', storefrontCountryCodes=['USA', 'MEX'], productId='com.example.productId') mass_extend_renewal_date_response = client.extend_renewal_date_for_all_active_subscribers(extend_renewal_date_request) self.assertIsNotNone(mass_extend_renewal_date_response) self.assertEqual('758883e8-151b-47b7-abd0-60c4d804c2f5', mass_extend_renewal_date_response.requestIdentifier) def test_extend_subscription_renewal_date(self): 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': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac'}) extend_renewal_date_request = ExtendRenewalDateRequest( extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac' ) extend_renewal_date_response = client.extend_subscription_renewal_date('4124214', extend_renewal_date_request) self.assertIsNotNone(extend_renewal_date_response) self.assertEqual('2312412', extend_renewal_date_response.originalTransactionId) self.assertEqual('9993', extend_renewal_date_response.webOrderLineItemId) self.assertTrue(extend_renewal_date_response.success) self.assertEqual(1698148900000, extend_renewal_date_response.effectiveDate) def test_get_all_subscription_statuses(self): client = self.get_client_with_body_from_file('tests/resources/models/getAllSubscriptionStatusesResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/subscriptions/4321', {'status': [2, 1]}, None) status_response = client.get_all_subscription_statuses('4321', [Status.EXPIRED, Status.ACTIVE]) self.assertIsNotNone(status_response) self.assertEqual(Environment.LOCAL_TESTING, status_response.environment) self.assertEqual('LocalTesting', status_response.rawEnvironment) self.assertEqual('com.example', status_response.bundleId) self.assertEqual(5454545, status_response.appAppleId) expected_body = [ SubscriptionGroupIdentifierItem( subscriptionGroupIdentifier='sub_group_one', lastTransactions=[ LastTransactionsItem( status=Status.ACTIVE, originalTransactionId='3749183', signedTransactionInfo='signed_transaction_one', signedRenewalInfo='signed_renewal_one' ), LastTransactionsItem( status=Status.REVOKED, originalTransactionId='5314314134', signedTransactionInfo='signed_transaction_two', signedRenewalInfo='signed_renewal_two' ) ] ), SubscriptionGroupIdentifierItem( subscriptionGroupIdentifier='sub_group_two', lastTransactions=[ LastTransactionsItem( status=Status.EXPIRED, originalTransactionId='3413453', signedTransactionInfo='signed_transaction_three', signedRenewalInfo='signed_renewal_three' ) ] ) ] self.assertEqual(expected_body, status_response.data) def test_get_refund_history(self): client = self.get_client_with_body_from_file('tests/resources/models/getRefundHistoryResponse.json', 'GET', 'https://local-testing-base-url/inApps/v2/refund/lookup/555555', {'revision': ['revision_input']}, None) refund_history_response = client.get_refund_history('555555', 'revision_input') self.assertIsNotNone(refund_history_response) self.assertEqual(['signed_transaction_one', 'signed_transaction_two'], refund_history_response.signedTransactions) self.assertEqual('revision_output', refund_history_response.revision) self.assertTrue(refund_history_response.hasMore) def test_get_status_of_subscription_renewal_date_extensions(self): client = self.get_client_with_body_from_file('tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass/20fba8a0-2b80-4a7d-a17f-85c1854727f8/com.example.product', {}, None) mass_extend_renewal_date_status_response = client.get_status_of_subscription_renewal_date_extensions('com.example.product', '20fba8a0-2b80-4a7d-a17f-85c1854727f8') self.assertIsNotNone(mass_extend_renewal_date_status_response) self.assertEqual('20fba8a0-2b80-4a7d-a17f-85c1854727f8', mass_extend_renewal_date_status_response.requestIdentifier) self.assertTrue(mass_extend_renewal_date_status_response.complete) self.assertEqual(1698148900000, mass_extend_renewal_date_status_response.completeDate) self.assertEqual(30, mass_extend_renewal_date_status_response.succeededCount) self.assertEqual(2, mass_extend_renewal_date_status_response.failedCount) def test_get_test_notification_status(self): client = self.get_client_with_body_from_file('tests/resources/models/getTestNotificationStatusResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/notifications/test/8cd2974c-f905-492a-bf9a-b2f47c791d19', {}, None) check_test_notification_response = client.get_test_notification_status('8cd2974c-f905-492a-bf9a-b2f47c791d19') self.assertIsNotNone(check_test_notification_response) self.assertEqual('signed_payload', check_test_notification_response.signedPayload) sendAttemptItems = [ SendAttemptItem(attemptDate=1698148900000,sendAttemptResult=SendAttemptResult.NO_RESPONSE), SendAttemptItem(attemptDate=1698148950000,sendAttemptResult=SendAttemptResult.SUCCESS) ] self.assertEqual(sendAttemptItems, check_test_notification_response.sendAttempts) def test_get_notification_history(self): client = self.get_client_with_body_from_file('tests/resources/models/getNotificationHistoryResponse.json', 'POST', 'https://local-testing-base-url/inApps/v1/notifications/history', {'paginationToken': ['a036bc0e-52b8-4bee-82fc-8c24cb6715d6']}, {'startDate': 1698148900000, 'endDate': 1698148950000, 'notificationType': 'SUBSCRIBED', 'notificationSubtype': 'INITIAL_BUY', 'transactionId': '999733843', 'onlyFailures': True}) notification_history_request = NotificationHistoryRequest( startDate=1698148900000, endDate=1698148950000, notificationType=NotificationTypeV2.SUBSCRIBED, notificationSubtype=Subtype.INITIAL_BUY, transactionId='999733843', onlyFailures=True ) notification_history_response = client.get_notification_history('a036bc0e-52b8-4bee-82fc-8c24cb6715d6', notification_history_request) self.assertIsNotNone(notification_history_response) self.assertEqual('57715481-805a-4283-8499-1c19b5d6b20a', notification_history_response.paginationToken) self.assertTrue(notification_history_response.hasMore) expected_notification_history = [ NotificationHistoryResponseItem(sendAttempts=[ SendAttemptItem( attemptDate=1698148900000, sendAttemptResult=SendAttemptResult.NO_RESPONSE ), SendAttemptItem( attemptDate=1698148950000, rawSendAttemptResult='SUCCESS' ) ], signedPayload='signed_payload_one'), NotificationHistoryResponseItem(sendAttempts=[ SendAttemptItem( attemptDate=1698148800000, sendAttemptResult=SendAttemptResult.CIRCULAR_REDIRECT ) ], signedPayload='signed_payload_two') ] self.assertEqual(expected_notification_history, notification_history_response.notificationHistory) def test_get_transaction_history_v1(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/history/1234', {'revision': ['revision_input'], 'startDate': ['123455'], 'endDate': ['123456'], 'productId': ['com.example.1', 'com.example.2'], 'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'], 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], 'revoked': ['False']}, None) request = TransactionHistoryRequest( sort=Order.ASCENDING, productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], endDate=123456, startDate=123455, revoked=False, inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED, productIds=['com.example.1', 'com.example.2'], subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1) self.assertIsNotNone(history_response) self.assertEqual('revision_output', history_response.revision) self.assertTrue(history_response.hasMore) self.assertEqual('com.example', history_response.bundleId) self.assertEqual(323232, history_response.appAppleId) self.assertEqual(Environment.LOCAL_TESTING, history_response.environment) self.assertEqual('LocalTesting', history_response.rawEnvironment) self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions) def test_get_transaction_history_v2(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', 'GET', 'https://local-testing-base-url/inApps/v2/history/1234', {'revision': ['revision_input'], 'startDate': ['123455'], 'endDate': ['123456'], 'productId': ['com.example.1', 'com.example.2'], 'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'], 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], 'revoked': ['False']}, None) request = TransactionHistoryRequest( sort=Order.ASCENDING, productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], endDate=123456, startDate=123455, revoked=False, inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED, productIds=['com.example.1', 'com.example.2'], subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2) self.assertIsNotNone(history_response) self.assertEqual('revision_output', history_response.revision) self.assertTrue(history_response.hasMore) self.assertEqual('com.example', history_response.bundleId) self.assertEqual(323232, history_response.appAppleId) self.assertEqual(Environment.LOCAL_TESTING, history_response.environment) self.assertEqual('LocalTesting', history_response.rawEnvironment) self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions) def test_get_transaction_info(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) transaction_info_response = client.get_transaction_info('1234') self.assertIsNotNone(transaction_info_response) self.assertEqual('signed_transaction_info_value', transaction_info_response.signedTransactionInfo) def test_look_up_order_id(self): client = self.get_client_with_body_from_file('tests/resources/models/lookupOrderIdResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/lookup/W002182', {}, None) order_lookup_response = client.look_up_order_id('W002182') self.assertIsNotNone(order_lookup_response) self.assertEqual(OrderLookupStatus.INVALID, order_lookup_response.status) self.assertEqual(1, order_lookup_response.rawStatus) self.assertEqual(['signed_transaction_one', 'signed_transaction_two'], order_lookup_response.signedTransactions) def test_request_test_notification(self): client = self.get_client_with_body_from_file('tests/resources/models/requestTestNotificationResponse.json', 'POST', 'https://local-testing-base-url/inApps/v1/notifications/test', {}, None) send_test_notification_response = client.request_test_notification() self.assertIsNotNone(send_test_notification_response) self.assertEqual('ce3af791-365e-4c60-841b-1674b43c1609', send_test_notification_response.testNotificationToken) def test_send_consumption_data(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/consumption/49571273', {}, {'customerConsented': True, 'consumptionStatus': 1, 'platform': 2, 'sampleContentProvided': False, 'deliveryStatus': 3, 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813', 'accountTenure': 4, 'playTime': 5, 'lifetimeDollarsRefunded': 6, 'lifetimeDollarsPurchased': 7, 'userStatus': 4, 'refundPreference': 3}) consumptionRequest = ConsumptionRequestV1( customerConsented=True, consumptionStatus=ConsumptionStatus.NOT_CONSUMED, platform=Platform.NON_APPLE, sampleContentProvided=False, deliveryStatus=DeliveryStatusV1.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE, appAccountToken='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, lifetimeDollarsPurchased=LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER, userStatus=UserStatus.LIMITED_ACCESS, refundPreference=RefundPreferenceV1.NO_PREFERENCE ) client.send_consumption_data('49571273', consumptionRequest) def test_send_consumption_information(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v2/transactions/consumption/49571273', {}, { 'customerConsented': True, 'sampleContentProvided': False, 'deliveryStatus': 'DELIVERED', 'consumptionPercentage': 50000, 'refundPreference': 'GRANT_FULL' }) consumptionRequest = ConsumptionRequest( customerConsented=True, sampleContentProvided=False, deliveryStatus=DeliveryStatus.DELIVERED, consumptionPercentage=50000, refundPreference=RefundPreference.GRANT_FULL ) client.send_consumption_information('49571273', consumptionRequest) def test_api_error(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) 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_xcode_not_supported_error(self): try: signing_key = self.get_signing_key() AppStoreServerAPIClient(signing_key, 'keyId', 'issuerId', 'com.example', Environment.XCODE) except ValueError as e: self.assertEqual("Xcode is not a supported environment for an AppStoreServerAPIClient", e.args[0]) return self.assertFalse(True) def test_api_too_many_requests(self): client = self.get_client_with_body_from_file('tests/resources/models/apiTooManyRequestsException.json', 'POST', 'https://local-testing-base-url/inApps/v1/notifications/test', {}, None, 429) try: client.request_test_notification() except APIException as e: self.assertEqual(429, e.http_status_code) self.assertEqual(4290000, e.raw_api_error) self.assertEqual(APIError.RATE_LIMIT_EXCEEDED, e.api_error) self.assertEqual("Rate limit exceeded.", 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', 'https://local-testing-base-url/inApps/v1/notifications/test', {}, None, 400) try: client.request_test_notification() except APIException as e: self.assertEqual(400, e.http_status_code) self.assertEqual(9990000, e.raw_api_error) self.assertIsNone(e.api_error) self.assertEqual("Testing error.", e.error_message) return self.assertFalse(True) def test_get_transaction_history_with_unknown_environment(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json', 'GET', 'https://local-testing-base-url/inApps/v2/history/1234', {'revision': ['revision_input'], 'startDate': ['123455'], 'endDate': ['123456'], 'productId': ['com.example.1', 'com.example.2'], 'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'], 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], 'revoked': ['False']}, None) request = TransactionHistoryRequest( sort=Order.ASCENDING, productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], endDate=123456, startDate=123455, revoked=False, inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED, productIds=['com.example.1', 'com.example.2'], subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2) self.assertIsNone(history_response.environment) self.assertEqual("LocalTestingxxx", history_response.rawEnvironment) def test_get_transaction_history_with_malformed_app_apple_id(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedAppAppleId.json', 'GET', 'https://local-testing-base-url/inApps/v1/history/1234', {'revision': ['revision_input'], 'startDate': ['123455'], 'endDate': ['123456'], 'productId': ['com.example.1', 'com.example.2'], 'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'], 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], 'revoked': ['False']}, None) request = TransactionHistoryRequest( sort=Order.ASCENDING, productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], endDate=123456, startDate=123455, revoked=False, inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED, productIds=['com.example.1', 'com.example.2'], subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) try: client.get_transaction_history('1234', 'revision_input', request) except Exception: return self.assertFalse(True) def test_set_app_account_token(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken', {}, { 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813' }) update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813') client.set_app_account_token('49571273', update_app_account_token_request) def test_invalid_app_account_token_error(self): client = self.get_client_with_body_from_file('tests/resources/models/invalidAppAccountTokenUUIDError.json', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken', {}, None, 400) try: client.set_app_account_token('49571273', None) except APIException as e: self.assertEqual(400, e.http_status_code) self.assertEqual(4000183, e.raw_api_error) self.assertEqual(APIError.INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR, e.api_error) self.assertEqual("Invalid request. The app account token field must be a valid UUID.", e.error_message) return self.assertFalse(True) def test_family_transaction_not_supported_error(self): client = self.get_client_with_body_from_file('tests/resources/models/familyTransactionNotSupportedError.json', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/1234' '/appAccountToken', {}, None, 400) try: client.set_app_account_token('1234', None) except APIException as e: self.assertEqual(400, e.http_status_code) self.assertEqual(4000185, e.raw_api_error) self.assertEqual(APIError.FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR, e.api_error) self.assertEqual("Invalid request. Family Sharing transactions aren't supported by this endpoint.", e.error_message) return self.assertFalse(True) def test_transaction_id_not_original_transaction_id_error(self): client = self.get_client_with_body_from_file( 'tests/resources/models/transactionIdNotOriginalTransactionId.json', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/1234' '/appAccountToken', {}, None, 400) try: client.set_app_account_token('1234', None) except APIException as e: self.assertEqual(400, e.http_status_code) self.assertEqual(4000187, e.raw_api_error) self.assertEqual(APIError.TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR, e.api_error) self.assertEqual("Invalid request. The transaction ID provided is not an original transaction ID.", e.error_message) return self.assertFalse(True) def test_upload_image(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, None, 200, bytes([1, 2, 3]), 'image/png') client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3])) def test_delete_image(self): client = self.get_client_with_body(b'', 'DELETE', 'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, None) client.delete_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890')) def test_get_image_list(self): client = self.get_client_with_body_from_file('tests/resources/models/getImageListResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/messaging/image/list', {}, None) response = client.get_image_list() self.assertIsNotNone(response) self.assertEqual(1, len(response.imageIdentifiers)) self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.imageIdentifiers[0].imageIdentifier) self.assertEqual(ImageState.APPROVED, response.imageIdentifiers[0].imageState) self.assertEqual(ImageSize.FULL_SIZE, response.imageIdentifiers[0].imageSize) def test_upload_message(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, {'header': 'Header text', 'body': 'Body text'}) upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text') client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body) def test_upload_message_with_image(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, {'header': 'Header text', 'body': 'Body text', 'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'}}) image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text') upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image) client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body) def test_delete_message(self): client = self.get_client_with_body(b'', 'DELETE', 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, None) client.delete_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890')) def test_get_message_list(self): client = self.get_client_with_body_from_file('tests/resources/models/getMessageListResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/messaging/message/list', {}, None) response = client.get_message_list() self.assertIsNotNone(response) self.assertEqual(1, len(response.messageIdentifiers)) self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifiers[0].messageIdentifier) self.assertEqual(MessageState.APPROVED, response.messageIdentifiers[0].messageState) def test_configure_default_message(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US', {}, {'messageIdentifier': 'a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'}) default_configuration_request = DefaultConfigurationRequest(messageIdentifier=UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890')) client.configure_default_message('com.example.product', 'en-US', default_configuration_request) def test_delete_default_message(self): client = self.get_client_with_body(b'', 'DELETE', 'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US', {}, None) client.delete_default_message('com.example.product', 'en-US') def test_get_default_message(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.product/en-US', {}, None) response = client.get_default_message('com.example.product', 'en-US') self.assertIsNotNone(response) self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifier) def test_upload_image_with_image_size(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {'imageSize': ['FULL_SIZE']}, None, 200, bytes([1, 2, 3]), 'image/png') client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3]), ImageSize.FULL_SIZE) def test_configure_realtime_url(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', {}, {'realtimeURL': 'https://example.com/realtime'}) realtime_url_request = RealtimeUrlRequest(realtimeURL='https://example.com/realtime') client.configure_realtime_url(realtime_url_request) def test_delete_realtime_url(self): client = self.get_client_with_body(b'', 'DELETE', 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', {}, None) client.delete_realtime_url() def test_get_realtime_url(self): client = self.get_client_with_body_from_file('tests/resources/models/getRealtimeUrlResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', {}, None) response = client.get_realtime_url() self.assertIsNotNone(response) self.assertEqual('https://example.com/realtime', response.realtimeURL) def test_upload_message_with_bullet_points(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, {'header': 'Header text', 'body': 'Body text', 'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'}, 'headerPosition': 'ABOVE_IMAGE', 'bulletPoints': [{'text': 'Bullet 1', 'imageIdentifier': 'c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012', 'altText': 'Bullet alt'}]}) image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text') bullet_point = BulletPoint(text='Bullet 1', imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt') upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image, headerPosition=HeaderPosition.ABOVE_IMAGE, bulletPoints=[bullet_point]) client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body) def test_initiate_performance_test(self): client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResponse.json', 'POST', 'https://local-testing-base-url/inApps/v1/messaging/performanceTest', {}, {'originalTransactionId': '70000500092808'}) 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.assertIsNotNone(response.config) self.assertEqual(10, response.config.maxConcurrentRequests) self.assertEqual(100, response.config.totalRequests) self.assertEqual(60000, response.config.totalDuration) self.assertEqual(500, response.config.responseTimeThreshold) self.assertEqual(95, response.config.successRateThreshold) def test_get_performance_test_results(self): client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResultResponse.json', 'GET', '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') self.assertIsNotNone(response) self.assertIsNotNone(response.config) self.assertEqual(10, response.config.maxConcurrentRequests) self.assertEqual(100, response.config.totalRequests) self.assertEqual(60000, response.config.totalDuration) self.assertEqual(500, response.config.responseTimeThreshold) self.assertEqual(95, response.config.successRateThreshold) self.assertEqual('https://example.com/retention', response.target) self.assertEqual(PerformanceTestStatus.PASS, response.result) self.assertEqual('PASS', response.rawResult) self.assertEqual(98, response.successRate) self.assertEqual(0, response.numPending) self.assertIsNotNone(response.responseTimes) self.assertEqual(120, response.responseTimes.average) self.assertEqual(100, response.responseTimes.p50) self.assertEqual(200, response.responseTimes.p90) self.assertEqual(250, response.responseTimes.p95) self.assertEqual(400, response.responseTimes.p99) self.assertEqual({SendAttemptResult.TIMED_OUT: 1, SendAttemptResult.NO_RESPONSE: 1}, response.failures) self.assertEqual({'TIMED_OUT': 1, 'NO_RESPONSE': 1}, response.rawFailures) def test_get_app_transaction_info_success(self): client = self.get_client_with_body_from_file('tests/resources/models/appTransactionInfoResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/transactions/appTransactions/1234', {}, None) app_transaction_info_response = client.get_app_transaction_info('1234') self.assertIsNotNone(app_transaction_info_response) self.assertEqual('signed_app_transaction_info_value', app_transaction_info_response.signedAppTransactionInfo) def test_get_app_transaction_info_invalid_transaction_id(self): client = self.get_client_with_body_from_file('tests/resources/models/invalidTransactionIdError.json', 'GET', 'https://local-testing-base-url/inApps/v1/transactions/appTransactions/invalid_id', {}, None, 400) try: client.get_app_transaction_info('invalid_id') 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.assertEqual("Invalid transaction id.", e.error_message) return self.assertFalse(True) def test_get_app_transaction_info_app_transaction_does_not_exist(self): client = self.get_client_with_body_from_file('tests/resources/models/appTransactionDoesNotExistError.json', 'GET', 'https://local-testing-base-url/inApps/v1/transactions/appTransactions/nonexistent_id', {}, None, 404) try: client.get_app_transaction_info('nonexistent_id') 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("No AppTransaction exists for the customer.", e.error_message) return self.assertFalse(True) def test_get_app_transaction_info_transaction_id_not_found(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionIdNotFoundError.json', 'GET', 'https://local-testing-base-url/inApps/v1/transactions/appTransactions/not_found_id', {}, None, 404) try: client.get_app_transaction_info('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 get_signing_key(self): return read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') def get_client_with_body(self, body: str, expected_method: str, expected_url: str, expected_params: Dict[str, Union[str, List[str]]], expected_json: Dict[str, Any], status_code: int = 200, expected_data: bytes = None, expected_content_type: str = None): signing_key = self.get_signing_key() client = AppStoreServerAPIClient(signing_key, 'keyId', 'issuerId', 'com.example', Environment.LOCAL_TESTING) def fake_execute_and_validate_inputs(method: bytes, url: str, params: Dict[str, Union[str, List[str]]], headers: Dict[str, str], json: Dict[str, Any], data: bytes): self.assertEqual(expected_method, method) self.assertEqual(expected_url, url) self.assertEqual(expected_params, params) self.assertTrue(headers['User-Agent'].startswith('app-store-server-library/python')) self.assertTrue(headers['Authorization'].startswith('Bearer ')) self.assertEqual('application/json', headers['Accept']) decoded_jwt = decode_json_from_signed_date(headers['Authorization'][7:]) self.assertEqual('appstoreconnect-v1', decoded_jwt['payload']['aud']) self.assertEqual('issuerId', decoded_jwt['payload']['iss']) self.assertEqual('keyId', decoded_jwt['header']['kid']) self.assertEqual('com.example', decoded_jwt['payload']['bid']) # Content-specific validation if expected_data is not None: self.assertEqual(['User-Agent', 'Authorization', 'Accept', 'Content-Type'], list(headers.keys())) self.assertEqual(expected_content_type, headers['Content-Type']) self.assertIsNone(json) self.assertEqual(expected_data, data) else: self.assertEqual(['User-Agent', 'Authorization', 'Accept'], list(headers.keys())) self.assertEqual(expected_json, json) response = Response() response.status_code = status_code response.raw = BytesIO(body) response.headers['Content-Type'] = 'application/json' return response client._execute_request = fake_execute_and_validate_inputs return client def get_client_with_body_from_file(self, path: str, expected_method: str, expected_url: str, expected_params: Dict[str, Union[str, List[str]]], expected_json: Dict[str, Any], status_code: int = 200): body = read_data_from_binary_file(path) return self.get_client_with_body(body, expected_method, expected_url, expected_params, expected_json, status_code) ================================================ FILE: tests/test_api_client_async.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Any, Dict, List, Union import unittest from httpx import Response from appstoreserverlibrary.api_client import APIError, APIException, AsyncAppStoreServerAPIClient, GetTransactionHistoryVersion from appstoreserverlibrary.models.AccountTenure import AccountTenure from appstoreserverlibrary.models.AppTransactionInfoResponse import AppTransactionInfoResponse from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus 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.Environment import Environment from appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent from appstoreserverlibrary.models.ExtendReasonCode import ExtendReasonCode from appstoreserverlibrary.models.ExtendRenewalDateRequest import ExtendRenewalDateRequest from appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType from appstoreserverlibrary.models.LastTransactionsItem import LastTransactionsItem from appstoreserverlibrary.models.LifetimeDollarsPurchased import LifetimeDollarsPurchased from appstoreserverlibrary.models.LifetimeDollarsRefunded import LifetimeDollarsRefunded from appstoreserverlibrary.models.MassExtendRenewalDateRequest import MassExtendRenewalDateRequest from appstoreserverlibrary.models.NotificationHistoryRequest import NotificationHistoryRequest from appstoreserverlibrary.models.NotificationHistoryResponseItem import NotificationHistoryResponseItem from appstoreserverlibrary.models.NotificationTypeV2 import NotificationTypeV2 from appstoreserverlibrary.models.OfferType import OfferType from appstoreserverlibrary.models.OrderLookupStatus import OrderLookupStatus from appstoreserverlibrary.models.Platform import Platform from appstoreserverlibrary.models.PlayTime import PlayTime from appstoreserverlibrary.models.PriceIncreaseStatus import PriceIncreaseStatus from appstoreserverlibrary.models.RefundPreference import RefundPreference from appstoreserverlibrary.models.RefundPreferenceV1 import RefundPreferenceV1 from appstoreserverlibrary.models.RevocationReason import RevocationReason from appstoreserverlibrary.models.SendAttemptItem import SendAttemptItem from appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult from appstoreserverlibrary.models.Status import Status from appstoreserverlibrary.models.SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem from appstoreserverlibrary.models.Subtype import Subtype from appstoreserverlibrary.models.TransactionHistoryRequest import Order, ProductType, TransactionHistoryRequest from appstoreserverlibrary.models.TransactionReason import TransactionReason from appstoreserverlibrary.models.Type import Type from appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest from appstoreserverlibrary.models.UserStatus import UserStatus from appstoreserverlibrary.models.DefaultConfigurationRequest import DefaultConfigurationRequest from appstoreserverlibrary.models.HeaderPosition import HeaderPosition from appstoreserverlibrary.models.ImageSize import ImageSize from appstoreserverlibrary.models.ImageState import ImageState from appstoreserverlibrary.models.MessageState import MessageState from appstoreserverlibrary.models.BulletPoint import BulletPoint from appstoreserverlibrary.models.PerformanceTestRequest import PerformanceTestRequest from appstoreserverlibrary.models.PerformanceTestStatus import PerformanceTestStatus from appstoreserverlibrary.models.RealtimeUrlRequest import RealtimeUrlRequest from appstoreserverlibrary.models.UploadMessageImage import UploadMessageImage from appstoreserverlibrary.models.UploadMessageRequestBody import UploadMessageRequestBody from uuid import UUID from tests.util import decode_json_from_signed_date, read_data_from_binary_file, read_data_from_file from io import BytesIO class DecodedPayloads(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', 'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass', {}, {'extendByDays': 45, 'extendReasonCode': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac', 'storefrontCountryCodes': ['USA', 'MEX'], 'productId': 'com.example.productId'}) extend_renewal_date_request = MassExtendRenewalDateRequest( extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac', storefrontCountryCodes=['USA', 'MEX'], productId='com.example.productId') mass_extend_renewal_date_response = await client.extend_renewal_date_for_all_active_subscribers(extend_renewal_date_request) self.assertIsNotNone(mass_extend_renewal_date_response) self.assertEqual('758883e8-151b-47b7-abd0-60c4d804c2f5', mass_extend_renewal_date_response.requestIdentifier) async def test_extend_subscription_renewal_date(self): 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': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac'}) extend_renewal_date_request = ExtendRenewalDateRequest( extendByDays=45, extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION, requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac' ) extend_renewal_date_response = await client.extend_subscription_renewal_date('4124214', extend_renewal_date_request) self.assertIsNotNone(extend_renewal_date_response) self.assertEqual('2312412', extend_renewal_date_response.originalTransactionId) self.assertEqual('9993', extend_renewal_date_response.webOrderLineItemId) self.assertTrue(extend_renewal_date_response.success) self.assertEqual(1698148900000, extend_renewal_date_response.effectiveDate) async def test_get_all_subscription_statuses(self): client = self.get_client_with_body_from_file('tests/resources/models/getAllSubscriptionStatusesResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/subscriptions/4321', {'status': [2, 1]}, None) status_response = await client.get_all_subscription_statuses('4321', [Status.EXPIRED, Status.ACTIVE]) self.assertIsNotNone(status_response) self.assertEqual(Environment.LOCAL_TESTING, status_response.environment) self.assertEqual('LocalTesting', status_response.rawEnvironment) self.assertEqual('com.example', status_response.bundleId) self.assertEqual(5454545, status_response.appAppleId) expected_body = [ SubscriptionGroupIdentifierItem( subscriptionGroupIdentifier='sub_group_one', lastTransactions=[ LastTransactionsItem( status=Status.ACTIVE, originalTransactionId='3749183', signedTransactionInfo='signed_transaction_one', signedRenewalInfo='signed_renewal_one' ), LastTransactionsItem( status=Status.REVOKED, originalTransactionId='5314314134', signedTransactionInfo='signed_transaction_two', signedRenewalInfo='signed_renewal_two' ) ] ), SubscriptionGroupIdentifierItem( subscriptionGroupIdentifier='sub_group_two', lastTransactions=[ LastTransactionsItem( status=Status.EXPIRED, originalTransactionId='3413453', signedTransactionInfo='signed_transaction_three', signedRenewalInfo='signed_renewal_three' ) ] ) ] self.assertEqual(expected_body, status_response.data) async def test_get_refund_history(self): client = self.get_client_with_body_from_file('tests/resources/models/getRefundHistoryResponse.json', 'GET', 'https://local-testing-base-url/inApps/v2/refund/lookup/555555', {'revision': ['revision_input']}, None) refund_history_response = await client.get_refund_history('555555', 'revision_input') self.assertIsNotNone(refund_history_response) self.assertEqual(['signed_transaction_one', 'signed_transaction_two'], refund_history_response.signedTransactions) self.assertEqual('revision_output', refund_history_response.revision) self.assertTrue(refund_history_response.hasMore) async def test_get_status_of_subscription_renewal_date_extensions(self): client = self.get_client_with_body_from_file('tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass/20fba8a0-2b80-4a7d-a17f-85c1854727f8/com.example.product', {}, None) mass_extend_renewal_date_status_response = await client.get_status_of_subscription_renewal_date_extensions('com.example.product', '20fba8a0-2b80-4a7d-a17f-85c1854727f8') self.assertIsNotNone(mass_extend_renewal_date_status_response) self.assertEqual('20fba8a0-2b80-4a7d-a17f-85c1854727f8', mass_extend_renewal_date_status_response.requestIdentifier) self.assertTrue(mass_extend_renewal_date_status_response.complete) self.assertEqual(1698148900000, mass_extend_renewal_date_status_response.completeDate) self.assertEqual(30, mass_extend_renewal_date_status_response.succeededCount) self.assertEqual(2, mass_extend_renewal_date_status_response.failedCount) async def test_get_test_notification_status(self): client = self.get_client_with_body_from_file('tests/resources/models/getTestNotificationStatusResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/notifications/test/8cd2974c-f905-492a-bf9a-b2f47c791d19', {}, None) check_test_notification_response = await client.get_test_notification_status('8cd2974c-f905-492a-bf9a-b2f47c791d19') self.assertIsNotNone(check_test_notification_response) self.assertEqual('signed_payload', check_test_notification_response.signedPayload) sendAttemptItems = [ SendAttemptItem(attemptDate=1698148900000,sendAttemptResult=SendAttemptResult.NO_RESPONSE), SendAttemptItem(attemptDate=1698148950000,sendAttemptResult=SendAttemptResult.SUCCESS) ] self.assertEqual(sendAttemptItems, check_test_notification_response.sendAttempts) async def test_get_notification_history(self): client = self.get_client_with_body_from_file('tests/resources/models/getNotificationHistoryResponse.json', 'POST', 'https://local-testing-base-url/inApps/v1/notifications/history', {'paginationToken': ['a036bc0e-52b8-4bee-82fc-8c24cb6715d6']}, {'startDate': 1698148900000, 'endDate': 1698148950000, 'notificationType': 'SUBSCRIBED', 'notificationSubtype': 'INITIAL_BUY', 'transactionId': '999733843', 'onlyFailures': True}) notification_history_request = NotificationHistoryRequest( startDate=1698148900000, endDate=1698148950000, notificationType=NotificationTypeV2.SUBSCRIBED, notificationSubtype=Subtype.INITIAL_BUY, transactionId='999733843', onlyFailures=True ) notification_history_response = await client.get_notification_history('a036bc0e-52b8-4bee-82fc-8c24cb6715d6', notification_history_request) self.assertIsNotNone(notification_history_response) self.assertEqual('57715481-805a-4283-8499-1c19b5d6b20a', notification_history_response.paginationToken) self.assertTrue(notification_history_response.hasMore) expected_notification_history = [ NotificationHistoryResponseItem(sendAttempts=[ SendAttemptItem( attemptDate=1698148900000, sendAttemptResult=SendAttemptResult.NO_RESPONSE ), SendAttemptItem( attemptDate=1698148950000, rawSendAttemptResult='SUCCESS' ) ], signedPayload='signed_payload_one'), NotificationHistoryResponseItem(sendAttempts=[ SendAttemptItem( attemptDate=1698148800000, sendAttemptResult=SendAttemptResult.CIRCULAR_REDIRECT ) ], signedPayload='signed_payload_two') ] self.assertEqual(expected_notification_history, notification_history_response.notificationHistory) async def test_get_transaction_history_v1(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/history/1234', {'revision': ['revision_input'], 'startDate': ['123455'], 'endDate': ['123456'], 'productId': ['com.example.1', 'com.example.2'], 'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'], 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], 'revoked': ['False']}, None) request = TransactionHistoryRequest( sort=Order.ASCENDING, productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], endDate=123456, startDate=123455, revoked=False, inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED, productIds=['com.example.1', 'com.example.2'], subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1) self.assertIsNotNone(history_response) self.assertEqual('revision_output', history_response.revision) self.assertTrue(history_response.hasMore) self.assertEqual('com.example', history_response.bundleId) self.assertEqual(323232, history_response.appAppleId) self.assertEqual(Environment.LOCAL_TESTING, history_response.environment) self.assertEqual('LocalTesting', history_response.rawEnvironment) self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions) async def test_get_transaction_history_v2(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json', 'GET', 'https://local-testing-base-url/inApps/v2/history/1234', {'revision': ['revision_input'], 'startDate': ['123455'], 'endDate': ['123456'], 'productId': ['com.example.1', 'com.example.2'], 'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'], 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], 'revoked': ['False']}, None) request = TransactionHistoryRequest( sort=Order.ASCENDING, productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], endDate=123456, startDate=123455, revoked=False, inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED, productIds=['com.example.1', 'com.example.2'], subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2) self.assertIsNotNone(history_response) self.assertEqual('revision_output', history_response.revision) self.assertTrue(history_response.hasMore) self.assertEqual('com.example', history_response.bundleId) self.assertEqual(323232, history_response.appAppleId) self.assertEqual(Environment.LOCAL_TESTING, history_response.environment) self.assertEqual('LocalTesting', history_response.rawEnvironment) self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions) async def test_get_transaction_info(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) transaction_info_response = await client.get_transaction_info('1234') self.assertIsNotNone(transaction_info_response) self.assertEqual('signed_transaction_info_value', transaction_info_response.signedTransactionInfo) async def test_look_up_order_id(self): client = self.get_client_with_body_from_file('tests/resources/models/lookupOrderIdResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/lookup/W002182', {}, None) order_lookup_response = await client.look_up_order_id('W002182') self.assertIsNotNone(order_lookup_response) self.assertEqual(OrderLookupStatus.INVALID, order_lookup_response.status) self.assertEqual(1, order_lookup_response.rawStatus) self.assertEqual(['signed_transaction_one', 'signed_transaction_two'], order_lookup_response.signedTransactions) async def test_request_test_notification(self): client = self.get_client_with_body_from_file('tests/resources/models/requestTestNotificationResponse.json', 'POST', 'https://local-testing-base-url/inApps/v1/notifications/test', {}, None) send_test_notification_response = await client.request_test_notification() self.assertIsNotNone(send_test_notification_response) self.assertEqual('ce3af791-365e-4c60-841b-1674b43c1609', send_test_notification_response.testNotificationToken) async def test_send_consumption_data(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/consumption/49571273', {}, {'customerConsented': True, 'consumptionStatus': 1, 'platform': 2, 'sampleContentProvided': False, 'deliveryStatus': 3, 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813', 'accountTenure': 4, 'playTime': 5, 'lifetimeDollarsRefunded': 6, 'lifetimeDollarsPurchased': 7, 'userStatus': 4, 'refundPreference': 3}) consumptionRequest = ConsumptionRequestV1( customerConsented=True, consumptionStatus=ConsumptionStatus.NOT_CONSUMED, platform=Platform.NON_APPLE, sampleContentProvided=False, deliveryStatus=DeliveryStatusV1.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE, appAccountToken='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, lifetimeDollarsPurchased=LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER, userStatus=UserStatus.LIMITED_ACCESS, refundPreference=RefundPreferenceV1.NO_PREFERENCE ) await client.send_consumption_data('49571273', consumptionRequest) async def test_send_consumption_information(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v2/transactions/consumption/49571273', {}, {'customerConsented': True, 'sampleContentProvided': False, 'deliveryStatus': 'DELIVERED', 'consumptionPercentage': 50000, 'refundPreference': 'GRANT_FULL'}) consumptionRequest = ConsumptionRequest( customerConsented=True, sampleContentProvided=False, deliveryStatus=DeliveryStatus.DELIVERED, consumptionPercentage=50000, refundPreference=RefundPreference.GRANT_FULL ) await client.send_consumption_information('49571273', consumptionRequest) async def test_api_error(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) 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_xcode_not_supported_error(self): try: signing_key = self.get_signing_key() AsyncAppStoreServerAPIClient(signing_key, 'keyId', 'issuerId', 'com.example', Environment.XCODE) except ValueError as e: self.assertEqual("Xcode is not a supported environment for an AppStoreServerAPIClient", e.args[0]) return self.assertFalse(True) async def test_api_too_many_requests(self): client = self.get_client_with_body_from_file('tests/resources/models/apiTooManyRequestsException.json', 'POST', 'https://local-testing-base-url/inApps/v1/notifications/test', {}, None, 429) try: await client.request_test_notification() except APIException as e: self.assertEqual(429, e.http_status_code) self.assertEqual(4290000, e.raw_api_error) self.assertEqual(APIError.RATE_LIMIT_EXCEEDED, e.api_error) self.assertEqual("Rate limit exceeded.", 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', 'https://local-testing-base-url/inApps/v1/notifications/test', {}, None, 400) try: await client.request_test_notification() except APIException as e: self.assertEqual(400, e.http_status_code) self.assertEqual(9990000, e.raw_api_error) self.assertIsNone(e.api_error) self.assertEqual("Testing error.", e.error_message) return self.assertFalse(True) async def test_get_transaction_history_with_unknown_environment(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json', 'GET', 'https://local-testing-base-url/inApps/v2/history/1234', {'revision': ['revision_input'], 'startDate': ['123455'], 'endDate': ['123456'], 'productId': ['com.example.1', 'com.example.2'], 'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'], 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], 'revoked': ['False']}, None) request = TransactionHistoryRequest( sort=Order.ASCENDING, productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], endDate=123456, startDate=123455, revoked=False, inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED, productIds=['com.example.1', 'com.example.2'], subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2) self.assertIsNone(history_response.environment) self.assertEqual("LocalTestingxxx", history_response.rawEnvironment) async def test_get_transaction_history_with_malformed_app_apple_id(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedAppAppleId.json', 'GET', 'https://local-testing-base-url/inApps/v1/history/1234', {'revision': ['revision_input'], 'startDate': ['123455'], 'endDate': ['123456'], 'productId': ['com.example.1', 'com.example.2'], 'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'], 'sort': ['ASCENDING'], 'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'], 'inAppOwnershipType': ['FAMILY_SHARED'], 'revoked': ['False']}, None) request = TransactionHistoryRequest( sort=Order.ASCENDING, productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE], endDate=123456, startDate=123455, revoked=False, inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED, productIds=['com.example.1', 'com.example.2'], subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2'] ) try: await client.get_transaction_history('1234', 'revision_input', request) except Exception: return self.assertFalse(True) async def test_set_app_account_token(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken', {}, { 'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813' }) update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813') await client.set_app_account_token('49571273', update_app_account_token_request) async def test_invalid_app_account_token_error(self): client = self.get_client_with_body_from_file('tests/resources/models/invalidAppAccountTokenUUIDError.json', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken', {}, None, 400) try: await client.set_app_account_token('49571273', None) except APIException as e: self.assertEqual(400, e.http_status_code) self.assertEqual(4000183, e.raw_api_error) self.assertEqual(APIError.INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR, e.api_error) self.assertEqual("Invalid request. The app account token field must be a valid UUID.", e.error_message) return self.assertFalse(True) async def test_family_transaction_not_supported_error(self): client = self.get_client_with_body_from_file('tests/resources/models/familyTransactionNotSupportedError.json', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/1234' '/appAccountToken', {}, None, 400) try: await client.set_app_account_token('1234', None) except APIException as e: self.assertEqual(400, e.http_status_code) self.assertEqual(4000185, e.raw_api_error) self.assertEqual(APIError.FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR, e.api_error) self.assertEqual("Invalid request. Family Sharing transactions aren't supported by this endpoint.", e.error_message) return self.assertFalse(True) async def test_transaction_id_not_original_transaction_id_error(self): client = self.get_client_with_body_from_file( 'tests/resources/models/transactionIdNotOriginalTransactionId.json', 'PUT', 'https://local-testing-base-url/inApps/v1/transactions/1234' '/appAccountToken', {}, None, 400) try: await client.set_app_account_token('1234', None) except APIException as e: self.assertEqual(400, e.http_status_code) self.assertEqual(4000187, e.raw_api_error) self.assertEqual(APIError.TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR, e.api_error) self.assertEqual("Invalid request. The transaction ID provided is not an original transaction ID.", e.error_message) return self.assertFalse(True) async def test_upload_image(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, None, 200, bytes([1, 2, 3]), 'image/png') await client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3])) async def test_delete_image(self): client = self.get_client_with_body(b'', 'DELETE', 'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, None) await client.delete_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890')) async def test_get_image_list(self): client = self.get_client_with_body_from_file('tests/resources/models/getImageListResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/messaging/image/list', {}, None) response = await client.get_image_list() self.assertIsNotNone(response) self.assertEqual(1, len(response.imageIdentifiers)) self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.imageIdentifiers[0].imageIdentifier) self.assertEqual(ImageState.APPROVED, response.imageIdentifiers[0].imageState) self.assertEqual(ImageSize.FULL_SIZE, response.imageIdentifiers[0].imageSize) async def test_upload_message(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, {'header': 'Header text', 'body': 'Body text'}) upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text') await client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body) async def test_upload_message_with_image(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, {'header': 'Header text', 'body': 'Body text', 'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'}}) image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text') upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image) await client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body) async def test_delete_message(self): client = self.get_client_with_body(b'', 'DELETE', 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, None) await client.delete_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890')) async def test_get_message_list(self): client = self.get_client_with_body_from_file('tests/resources/models/getMessageListResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/messaging/message/list', {}, None) response = await client.get_message_list() self.assertIsNotNone(response) self.assertEqual(1, len(response.messageIdentifiers)) self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifiers[0].messageIdentifier) self.assertEqual(MessageState.APPROVED, response.messageIdentifiers[0].messageState) async def test_configure_default_message(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US', {}, {'messageIdentifier': 'a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'}) default_configuration_request = DefaultConfigurationRequest(messageIdentifier=UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890')) await client.configure_default_message('com.example.product', 'en-US', default_configuration_request) async def test_delete_default_message(self): client = self.get_client_with_body(b'', 'DELETE', 'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US', {}, None) await client.delete_default_message('com.example.product', 'en-US') async def test_get_default_message(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.product/en-US', {}, None) response = await client.get_default_message('com.example.product', 'en-US') self.assertIsNotNone(response) self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifier) async def test_upload_image_with_image_size(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {'imageSize': ['FULL_SIZE']}, None, 200, bytes([1, 2, 3]), 'image/png') await client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3]), ImageSize.FULL_SIZE) async def test_configure_realtime_url(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', {}, {'realtimeURL': 'https://example.com/realtime'}) realtime_url_request = RealtimeUrlRequest(realtimeURL='https://example.com/realtime') await client.configure_realtime_url(realtime_url_request) async def test_delete_realtime_url(self): client = self.get_client_with_body(b'', 'DELETE', 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', {}, None) await client.delete_realtime_url() async def test_get_realtime_url(self): client = self.get_client_with_body_from_file('tests/resources/models/getRealtimeUrlResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/messaging/realtime/url', {}, None) response = await client.get_realtime_url() self.assertIsNotNone(response) self.assertEqual('https://example.com/realtime', response.realtimeURL) async def test_upload_message_with_bullet_points(self): client = self.get_client_with_body(b'', 'PUT', 'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', {}, {'header': 'Header text', 'body': 'Body text', 'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'}, 'headerPosition': 'ABOVE_IMAGE', 'bulletPoints': [{'text': 'Bullet 1', 'imageIdentifier': 'c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012', 'altText': 'Bullet alt'}]}) image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text') bullet_point = BulletPoint(text='Bullet 1', imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt') upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image, headerPosition=HeaderPosition.ABOVE_IMAGE, bulletPoints=[bullet_point]) await client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body) async def test_initiate_performance_test(self): client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResponse.json', 'POST', 'https://local-testing-base-url/inApps/v1/messaging/performanceTest', {}, {'originalTransactionId': '70000500092808'}) 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.assertIsNotNone(response.config) self.assertEqual(10, response.config.maxConcurrentRequests) self.assertEqual(100, response.config.totalRequests) self.assertEqual(60000, response.config.totalDuration) self.assertEqual(500, response.config.responseTimeThreshold) self.assertEqual(95, response.config.successRateThreshold) async def test_get_performance_test_results(self): client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResultResponse.json', 'GET', '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') self.assertIsNotNone(response) self.assertIsNotNone(response.config) self.assertEqual(10, response.config.maxConcurrentRequests) self.assertEqual(100, response.config.totalRequests) self.assertEqual(60000, response.config.totalDuration) self.assertEqual(500, response.config.responseTimeThreshold) self.assertEqual(95, response.config.successRateThreshold) self.assertEqual('https://example.com/retention', response.target) self.assertEqual(PerformanceTestStatus.PASS, response.result) self.assertEqual('PASS', response.rawResult) self.assertEqual(98, response.successRate) self.assertEqual(0, response.numPending) self.assertIsNotNone(response.responseTimes) self.assertEqual(120, response.responseTimes.average) self.assertEqual(100, response.responseTimes.p50) self.assertEqual(200, response.responseTimes.p90) self.assertEqual(250, response.responseTimes.p95) self.assertEqual(400, response.responseTimes.p99) self.assertEqual({SendAttemptResult.TIMED_OUT: 1, SendAttemptResult.NO_RESPONSE: 1}, response.failures) self.assertEqual({'TIMED_OUT': 1, 'NO_RESPONSE': 1}, response.rawFailures) async def test_get_app_transaction_info_success(self): client = self.get_client_with_body_from_file('tests/resources/models/appTransactionInfoResponse.json', 'GET', 'https://local-testing-base-url/inApps/v1/transactions/appTransactions/1234', {}, None) app_transaction_info_response = await client.get_app_transaction_info('1234') self.assertIsNotNone(app_transaction_info_response) self.assertEqual('signed_app_transaction_info_value', app_transaction_info_response.signedAppTransactionInfo) async def test_get_app_transaction_info_invalid_transaction_id(self): client = self.get_client_with_body_from_file('tests/resources/models/invalidTransactionIdError.json', 'GET', 'https://local-testing-base-url/inApps/v1/transactions/appTransactions/invalid_id', {}, None, 400) try: await client.get_app_transaction_info('invalid_id') 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.assertEqual("Invalid transaction id.", e.error_message) return self.assertFalse(True) async def test_get_app_transaction_info_app_transaction_does_not_exist(self): client = self.get_client_with_body_from_file('tests/resources/models/appTransactionDoesNotExistError.json', 'GET', 'https://local-testing-base-url/inApps/v1/transactions/appTransactions/nonexistent_id', {}, None, 404) try: await client.get_app_transaction_info('nonexistent_id') 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("No AppTransaction exists for the customer.", e.error_message) return self.assertFalse(True) async def test_get_app_transaction_info_transaction_id_not_found(self): client = self.get_client_with_body_from_file('tests/resources/models/transactionIdNotFoundError.json', 'GET', 'https://local-testing-base-url/inApps/v1/transactions/appTransactions/not_found_id', {}, None, 404) try: await client.get_app_transaction_info('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 get_signing_key(self): return read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') def get_client_with_body(self, body: str, expected_method: str, expected_url: str, expected_params: Dict[str, Union[str, List[str]]], expected_json: Dict[str, Any], status_code: int = 200, expected_data: bytes = None, expected_content_type: str = None): signing_key = self.get_signing_key() client = AsyncAppStoreServerAPIClient(signing_key, 'keyId', 'issuerId', 'com.example', Environment.LOCAL_TESTING) async def fake_execute_and_validate_inputs(method: bytes, url: str, params: Dict[str, Union[str, List[str]]], headers: Dict[str, str], json: Dict[str, Any], data: bytes): self.assertEqual(expected_method, method) self.assertEqual(expected_url, url) self.assertEqual(expected_params, params) self.assertTrue(headers['User-Agent'].startswith('app-store-server-library/python')) self.assertTrue(headers['Authorization'].startswith('Bearer ')) self.assertEqual('application/json', headers['Accept']) decoded_jwt = decode_json_from_signed_date(headers['Authorization'][7:]) self.assertEqual('appstoreconnect-v1', decoded_jwt['payload']['aud']) self.assertEqual('issuerId', decoded_jwt['payload']['iss']) self.assertEqual('keyId', decoded_jwt['header']['kid']) self.assertEqual('com.example', decoded_jwt['payload']['bid']) # Content-specific validation if expected_data is not None: self.assertEqual(['User-Agent', 'Authorization', 'Accept', 'Content-Type'], list(headers.keys())) self.assertEqual(expected_content_type, headers['Content-Type']) self.assertIsNone(json) self.assertEqual(expected_data, data) else: self.assertEqual(['User-Agent', 'Authorization', 'Accept'], list(headers.keys())) self.assertEqual(expected_json, json) response = Response(status_code, headers={'Content-Type': 'application/json'}, content=body) return response client._execute_request = fake_execute_and_validate_inputs return client def get_client_with_body_from_file(self, path: str, expected_method: str, expected_url: str, expected_params: Dict[str, Union[str, List[str]]], expected_json: Dict[str, Any], status_code: int = 200): body = read_data_from_binary_file(path) return self.get_client_with_body(body, expected_method, expected_url, expected_params, expected_json, status_code) ================================================ FILE: tests/test_app_data.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. import json import unittest from appstoreserverlibrary.models.AppData import AppData from appstoreserverlibrary.models.Environment import Environment from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter from tests.util import read_data_from_file class AppDataTest(unittest.TestCase): def test_app_data_deserialization(self): json_data = read_data_from_file('tests/resources/models/appData.json') app_data_dict = json.loads(json_data) app_data = _get_cattrs_converter(AppData).structure(app_data_dict, AppData) self.assertEqual(987654321, app_data.appAppleId) self.assertEqual("com.example", app_data.bundleId) self.assertEqual(Environment.SANDBOX, app_data.environment) self.assertEqual("Sandbox", app_data.rawEnvironment) self.assertEqual("signed-app-transaction-info", app_data.signedAppTransactionInfo) ================================================ FILE: tests/test_decoded_payloads.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Optional import unittest from uuid import UUID from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus 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.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.RevocationReason import RevocationReason from appstoreserverlibrary.models.RevocationType import RevocationType from appstoreserverlibrary.models.Status import Status from appstoreserverlibrary.models.Subtype import Subtype from appstoreserverlibrary.models.TransactionReason import TransactionReason from appstoreserverlibrary.models.Type import Type from tests.util import create_signed_data_from_json, get_default_signed_data_verifier, get_signed_data_verifier class DecodedPayloads(unittest.TestCase): def test_app_transaction_decoding(self): signed_app_transaction = create_signed_data_from_json('tests/resources/models/appTransaction.json') signed_data_verifier = get_default_signed_data_verifier() app_transaction = signed_data_verifier.verify_and_decode_app_transaction(signed_app_transaction) self.assertEqual(Environment.LOCAL_TESTING, app_transaction.receiptType) self.assertEqual("LocalTesting", app_transaction.rawReceiptType) self.assertEqual(531412, app_transaction.appAppleId) self.assertEqual("com.example", app_transaction.bundleId) self.assertEqual("1.2.3", app_transaction.applicationVersion) self.assertEqual(512, app_transaction.versionExternalIdentifier) self.assertEqual(1698148900000, app_transaction.receiptCreationDate) 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(1698148700000, app_transaction.preorderDate) self.assertEqual("71134", app_transaction.appTransactionId) self.assertEqual(PurchasePlatform.IOS, app_transaction.originalPlatform) self.assertEqual("iOS", app_transaction.rawOriginalPlatform) 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) self.assertEqual("12345", transaction.originalTransactionId) self.assertEqual("23456", transaction.transactionId) self.assertEqual("34343", transaction.webOrderLineItemId) self.assertEqual("com.example", transaction.bundleId) self.assertEqual("com.example.product", transaction.productId) self.assertEqual("55555", transaction.subscriptionGroupIdentifier) self.assertEqual(1698148800000, transaction.originalPurchaseDate) self.assertEqual(1698148900000, transaction.purchaseDate) self.assertEqual(1698148950000, transaction.revocationDate) self.assertEqual(1698149000000, transaction.expiresDate) 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(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType) self.assertEqual("PURCHASED", transaction.rawInAppOwnershipType) self.assertEqual(1698148900000, transaction.signedDate) self.assertEqual(RevocationReason.REFUNDED_DUE_TO_ISSUE, transaction.revocationReason) self.assertEqual(1, transaction.rawRevocationReason) self.assertEqual("abc.123", transaction.offerIdentifier) self.assertTrue(transaction.isUpgraded) self.assertEqual(OfferType.INTRODUCTORY_OFFER, transaction.offerType) self.assertEqual(1, transaction.rawOfferType) self.assertEqual("USA", transaction.storefront) self.assertEqual("143441", transaction.storefrontId) self.assertEqual(TransactionReason.PURCHASE, transaction.transactionReason) self.assertEqual("PURCHASE", transaction.rawTransactionReason) self.assertEqual(Environment.LOCAL_TESTING, transaction.environment) self.assertEqual("LocalTesting", transaction.rawEnvironment) self.assertEqual(10990, transaction.price) self.assertEqual("USD", transaction.currency) self.assertEqual(OfferDiscountType.PAY_AS_YOU_GO, transaction.offerDiscountType) self.assertEqual("PAY_AS_YOU_GO", transaction.rawOfferDiscountType) self.assertEqual("71134", transaction.appTransactionId) self.assertEqual("P1Y", transaction.offerPeriod) def test_transaction_with_revocation_decoding(self): signed_transaction = create_signed_data_from_json('tests/resources/models/signedTransactionWithRevocation.json') signed_data_verifier = get_default_signed_data_verifier() transaction = signed_data_verifier.verify_and_decode_signed_transaction(signed_transaction) self.assertEqual("12345", transaction.originalTransactionId) self.assertEqual("23456", transaction.transactionId) self.assertEqual("34343", transaction.webOrderLineItemId) self.assertEqual("com.example", transaction.bundleId) self.assertEqual("com.example.product", transaction.productId) self.assertEqual("55555", transaction.subscriptionGroupIdentifier) self.assertEqual(1698148800000, transaction.originalPurchaseDate) self.assertEqual(1698148900000, transaction.purchaseDate) self.assertEqual(1698148950000, transaction.revocationDate) self.assertEqual(1698149000000, transaction.expiresDate) 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(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType) self.assertEqual("PURCHASED", transaction.rawInAppOwnershipType) self.assertEqual(1698148900000, transaction.signedDate) self.assertEqual(RevocationReason.REFUNDED_DUE_TO_ISSUE, transaction.revocationReason) self.assertEqual(1, transaction.rawRevocationReason) self.assertEqual("abc.123", transaction.offerIdentifier) self.assertTrue(transaction.isUpgraded) self.assertEqual(OfferType.INTRODUCTORY_OFFER, transaction.offerType) self.assertEqual(1, transaction.rawOfferType) self.assertEqual("USA", transaction.storefront) self.assertEqual("143441", transaction.storefrontId) self.assertEqual(TransactionReason.PURCHASE, transaction.transactionReason) self.assertEqual("PURCHASE", transaction.rawTransactionReason) self.assertEqual(Environment.LOCAL_TESTING, transaction.environment) self.assertEqual("LocalTesting", transaction.rawEnvironment) self.assertEqual(10990, transaction.price) self.assertEqual("USD", transaction.currency) self.assertEqual(OfferDiscountType.PAY_AS_YOU_GO, transaction.offerDiscountType) self.assertEqual("PAY_AS_YOU_GO", transaction.rawOfferDiscountType) self.assertEqual("71134", transaction.appTransactionId) self.assertEqual("P1Y", transaction.offerPeriod) self.assertEqual(RevocationType.REFUND_PRORATED, transaction.revocationType) self.assertEqual("REFUND_PRORATED", transaction.rawRevocationType) self.assertEqual(50000, transaction.revocationPercentage) def test_renewal_info_decoding(self): signed_renewal_info = create_signed_data_from_json('tests/resources/models/signedRenewalInfo.json') signed_data_verifier = get_default_signed_data_verifier() renewal_info = signed_data_verifier.verify_and_decode_renewal_info(signed_renewal_info) self.assertEqual(ExpirationIntent.CUSTOMER_CANCELLED, renewal_info.expirationIntent) self.assertEqual(1, renewal_info.rawExpirationIntent) self.assertEqual("12345", renewal_info.originalTransactionId) self.assertEqual("com.example.product.2", renewal_info.autoRenewProductId) self.assertEqual("com.example.product", renewal_info.productId) self.assertEqual(AutoRenewStatus.ON, renewal_info.autoRenewStatus) self.assertEqual(1, renewal_info.rawAutoRenewStatus) self.assertTrue(renewal_info.isInBillingRetryPeriod) self.assertEqual(PriceIncreaseStatus.CUSTOMER_HAS_NOT_RESPONDED, renewal_info.priceIncreaseStatus) self.assertEqual(0, renewal_info.rawPriceIncreaseStatus) self.assertEqual(1698148900000, renewal_info.gracePeriodExpiresDate) self.assertEqual(OfferType.PROMOTIONAL_OFFER, renewal_info.offerType) self.assertEqual(2, renewal_info.rawOfferType) self.assertEqual("abc.123", renewal_info.offerIdentifier) self.assertEqual(1698148800000, renewal_info.signedDate) self.assertEqual(Environment.LOCAL_TESTING, renewal_info.environment) self.assertEqual("LocalTesting", renewal_info.rawEnvironment) self.assertEqual(1698148800000, renewal_info.recentSubscriptionStartDate) self.assertEqual(1698148850000, renewal_info.renewalDate) self.assertEqual(9990, renewal_info.renewalPrice) self.assertEqual("USD", renewal_info.currency) self.assertEqual(OfferDiscountType.PAY_AS_YOU_GO, renewal_info.offerDiscountType) self.assertEqual("PAY_AS_YOU_GO", renewal_info.rawOfferDiscountType) 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) def test_notification_decoding(self): signed_notification = create_signed_data_from_json('tests/resources/models/signedNotification.json') signed_data_verifier = get_default_signed_data_verifier() notification = signed_data_verifier.verify_and_decode_notification(signed_notification) self.assertEqual(NotificationTypeV2.SUBSCRIBED, notification.notificationType) self.assertEqual("SUBSCRIBED", notification.rawNotificationType) self.assertEqual(Subtype.INITIAL_BUY, notification.subtype) self.assertEqual("INITIAL_BUY", notification.rawSubtype) self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID) self.assertEqual("2.0", notification.version) self.assertEqual(1698148900000, notification.signedDate) self.assertIsNotNone(notification.data) self.assertIsNone(notification.summary) self.assertIsNone(notification.externalPurchaseToken) self.assertEqual(Environment.LOCAL_TESTING, notification.data.environment) self.assertEqual("LocalTesting", notification.data.rawEnvironment) self.assertEqual(41234, notification.data.appAppleId) self.assertEqual("com.example", notification.data.bundleId) self.assertEqual("1.2.3", notification.data.bundleVersion) self.assertEqual("signed_transaction_info_value", notification.data.signedTransactionInfo) self.assertEqual("signed_renewal_info_value", notification.data.signedRenewalInfo); self.assertEqual(Status.ACTIVE, notification.data.status) self.assertEqual(1, notification.data.rawStatus) self.assertIsNone(notification.data.consumptionRequestReason) self.assertIsNone(notification.data.rawConsumptionRequestReason) def test_consumption_request_notification_decoding(self): signed_notification = create_signed_data_from_json('tests/resources/models/signedConsumptionRequestNotification.json') signed_data_verifier = get_default_signed_data_verifier() notification = signed_data_verifier.verify_and_decode_notification(signed_notification) self.assertEqual(NotificationTypeV2.CONSUMPTION_REQUEST, notification.notificationType) self.assertEqual("CONSUMPTION_REQUEST", notification.rawNotificationType) self.assertIsNone(notification.subtype) self.assertIsNone(notification.rawSubtype) self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID) self.assertEqual("2.0", notification.version) self.assertEqual(1698148900000, notification.signedDate) self.assertIsNotNone(notification.data) self.assertIsNone(notification.summary) self.assertIsNone(notification.externalPurchaseToken) self.assertEqual(Environment.LOCAL_TESTING, notification.data.environment) self.assertEqual("LocalTesting", notification.data.rawEnvironment) self.assertEqual(41234, notification.data.appAppleId) self.assertEqual("com.example", notification.data.bundleId) self.assertEqual("1.2.3", notification.data.bundleVersion) self.assertEqual("signed_transaction_info_value", notification.data.signedTransactionInfo) self.assertEqual("signed_renewal_info_value", notification.data.signedRenewalInfo); self.assertEqual(Status.ACTIVE, notification.data.status) self.assertEqual(1, notification.data.rawStatus) self.assertEqual(ConsumptionRequestReason.UNINTENDED_PURCHASE, notification.data.consumptionRequestReason) self.assertEqual("UNINTENDED_PURCHASE", notification.data.rawConsumptionRequestReason) def test_summary_notification_decoding(self): signed_summary_notification = create_signed_data_from_json('tests/resources/models/signedSummaryNotification.json') signed_data_verifier = get_default_signed_data_verifier() notification = signed_data_verifier.verify_and_decode_notification(signed_summary_notification) self.assertEqual(NotificationTypeV2.RENEWAL_EXTENSION, notification.notificationType) self.assertEqual("RENEWAL_EXTENSION", notification.rawNotificationType) self.assertEqual(Subtype.SUMMARY, notification.subtype) self.assertEqual("SUMMARY", notification.rawSubtype) self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID) self.assertEqual("2.0", notification.version) self.assertEqual(1698148900000, notification.signedDate) self.assertIsNone(notification.data) self.assertIsNotNone(notification.summary) self.assertIsNone(notification.externalPurchaseToken) self.assertEqual(Environment.LOCAL_TESTING, notification.summary.environment) self.assertEqual("LocalTesting", notification.summary.rawEnvironment) 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(["CAN", "USA", "MEX"], notification.summary.storefrontCountryCodes) self.assertEqual(5, notification.summary.succeededCount) self.assertEqual(2, notification.summary.failedCount) def test_external_purchase_token_notification_decoding(self): signed_external_purchase_token_notification = create_signed_data_from_json('tests/resources/models/signedExternalPurchaseTokenNotification.json') signed_data_verifier = get_default_signed_data_verifier() def check_environment_and_bundle_id(bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]): self.assertEqual("com.example", bundle_id) self.assertEqual(55555, app_apple_id) self.assertEqual(Environment.PRODUCTION, environment) signed_data_verifier._verify_notification = check_environment_and_bundle_id notification = signed_data_verifier.verify_and_decode_notification(signed_external_purchase_token_notification) self.assertEqual(NotificationTypeV2.EXTERNAL_PURCHASE_TOKEN, notification.notificationType) self.assertEqual("EXTERNAL_PURCHASE_TOKEN", notification.rawNotificationType) self.assertEqual(Subtype.UNREPORTED, notification.subtype) self.assertEqual("UNREPORTED", notification.rawSubtype) self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID) self.assertEqual("2.0", notification.version) self.assertEqual(1698148900000, notification.signedDate) self.assertIsNone(notification.data) self.assertIsNone(notification.summary) self.assertIsNotNone(notification.externalPurchaseToken) self.assertEqual("b2158121-7af9-49d4-9561-1f588205523e", notification.externalPurchaseToken.externalPurchaseId) self.assertEqual(1698148950000, notification.externalPurchaseToken.tokenCreationDate) self.assertEqual(55555, notification.externalPurchaseToken.appAppleId) self.assertEqual("com.example", notification.externalPurchaseToken.bundleId) def test_external_purchase_token_sandbox_notification_decoding(self): signed_external_purchase_token_notification = create_signed_data_from_json('tests/resources/models/signedExternalPurchaseTokenSandboxNotification.json') signed_data_verifier = get_default_signed_data_verifier() def check_environment_and_bundle_id(bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]): self.assertEqual("com.example", bundle_id) self.assertEqual(55555, app_apple_id) self.assertEqual(Environment.SANDBOX, environment) signed_data_verifier._verify_notification = check_environment_and_bundle_id notification = signed_data_verifier.verify_and_decode_notification(signed_external_purchase_token_notification) self.assertEqual(NotificationTypeV2.EXTERNAL_PURCHASE_TOKEN, notification.notificationType) self.assertEqual("EXTERNAL_PURCHASE_TOKEN", notification.rawNotificationType) self.assertEqual(Subtype.UNREPORTED, notification.subtype) self.assertEqual("UNREPORTED", notification.rawSubtype) self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID) self.assertEqual("2.0", notification.version) self.assertEqual(1698148900000, notification.signedDate) self.assertIsNone(notification.data) self.assertIsNone(notification.summary) self.assertIsNotNone(notification.externalPurchaseToken) self.assertEqual("SANDBOX_b2158121-7af9-49d4-9561-1f588205523e", notification.externalPurchaseToken.externalPurchaseId) self.assertEqual(1698148950000, notification.externalPurchaseToken.tokenCreationDate) self.assertEqual(55555, notification.externalPurchaseToken.appAppleId) self.assertEqual("com.example", notification.externalPurchaseToken.bundleId) def test_realtime_request_decoding(self): signed_realtime_request = create_signed_data_from_json('tests/resources/models/decodedRealtimeRequest.json') signed_data_verifier = get_default_signed_data_verifier() request = signed_data_verifier.verify_and_decode_realtime_request(signed_realtime_request) self.assertEqual('99371282', request.originalTransactionId) self.assertEqual(531412, request.appAppleId) self.assertEqual('com.example.product', request.productId) self.assertEqual('en-US', request.userLocale) self.assertEqual(UUID('3db5c98d-8acf-4e29-831e-8e1f82f9f6e9'), request.requestIdentifier) self.assertEqual(Environment.LOCAL_TESTING, request.environment) self.assertEqual('LocalTesting', request.rawEnvironment) self.assertEqual(1698148900000, request.signedDate) def test_rescind_consent_notification_decoding(self): signed_notification = create_signed_data_from_json('tests/resources/models/signedRescindConsentNotification.json') signed_data_verifier = get_default_signed_data_verifier() notification = signed_data_verifier.verify_and_decode_notification(signed_notification) self.assertEqual(NotificationTypeV2.RESCIND_CONSENT, notification.notificationType) self.assertEqual("RESCIND_CONSENT", notification.rawNotificationType) self.assertIsNone(notification.subtype) self.assertIsNone(notification.rawSubtype) self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID) self.assertEqual("2.0", notification.version) self.assertEqual(1698148900000, notification.signedDate) self.assertIsNone(notification.data) self.assertIsNone(notification.summary) self.assertIsNone(notification.externalPurchaseToken) self.assertIsNotNone(notification.appData) self.assertEqual(Environment.LOCAL_TESTING, notification.appData.environment) self.assertEqual("LocalTesting", notification.appData.rawEnvironment) self.assertEqual(41234, notification.appData.appAppleId) self.assertEqual("com.example", notification.appData.bundleId) self.assertEqual("signed_app_transaction_info_value", notification.appData.signedAppTransactionInfo) ================================================ FILE: tests/test_jws_signature_creator.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. from attr import define import attr import base64 import json import jwt import unittest from appstoreserverlibrary.jws_signature_creator import AdvancedCommerceAPIInAppRequest, AdvancedCommerceAPIInAppSignatureCreator, IntroductoryOfferEligibilitySignatureCreator, PromotionalOfferV2SignatureCreator from tests.util import read_data_from_binary_file @define class TestInAppRequest(AdvancedCommerceAPIInAppRequest): test_value: str class JWSSignatureCreatorTest(unittest.TestCase): def test_promotional_offer_signature_creator(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", "transactionId") self.assertIsNotNone(signature) headers = jwt.get_unverified_header(signature) payload = jwt.decode(signature, options={"verify_signature": False}) # Header self.assertEqual("JWT", headers["typ"]) self.assertEqual("ES256", headers["alg"]) self.assertEqual("keyId", headers["kid"]) # Payload self.assertEqual("issuerId", payload['iss']) self.assertIsNotNone(payload['iat']) self.assertFalse('exp' in payload) self.assertEqual("promotional-offer", payload['aud']) self.assertEqual('bundleId', payload['bid']) self.assertIsNotNone(payload['nonce']) self.assertEqual('productId', payload['productId']) self.assertEqual('offerIdentifier', payload['offerIdentifier']) self.assertEqual('transactionId', payload['transactionId']) def test_promotional_offer_signature_creator_transaction_id_missing(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", None) payload = jwt.decode(signature, options={"verify_signature": False}) 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') with self.assertRaises(ValueError): signature_creator.create_signature("productId", None, "transactionId") def test_promotional_offer_signature_creator_product_id_missing(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') with self.assertRaises(ValueError): signature_creator.create_signature(None, "offerIdentifier", "transactionId") def test_introductory_offer_eligibility_signature_creator(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') signature = signature_creator.create_signature("productId", True, "transactionId") self.assertIsNotNone(signature) headers = jwt.get_unverified_header(signature) payload = jwt.decode(signature, options={"verify_signature": False}) # Header self.assertEqual("JWT", headers["typ"]) self.assertEqual("ES256", headers["alg"]) self.assertEqual("keyId", headers["kid"]) # Payload self.assertEqual("issuerId", payload['iss']) self.assertIsNotNone(payload['iat']) self.assertFalse('exp' in payload) self.assertEqual("introductory-offer-eligibility", payload['aud']) self.assertEqual('bundleId', payload['bid']) self.assertIsNotNone(payload['nonce']) self.assertEqual('productId', payload['productId']) self.assertEqual(True, payload['allowIntroductoryOffer']) self.assertEqual('transactionId', payload['transactionId']) def test_introductory_offer_eligibility_signature_creator_transaction_id_missing(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') with self.assertRaises(ValueError): signature_creator.create_signature("productId", True, None) def test_introductory_offer_eligibility_signature_creator_allow_introductory_offer_missing(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') with self.assertRaises(ValueError): signature_creator.create_signature("productId", None, "transactionId") def test_introductory_offer_eligibility_signature_creator_product_id_missing(self): signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8') signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId') with self.assertRaises(ValueError): signature_creator.create_signature(None, True, "transactionId") 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') request = TestInAppRequest("testValue") signature = signature_creator.create_signature(request) self.assertIsNotNone(signature) headers = jwt.get_unverified_header(signature) payload = jwt.decode(signature, options={"verify_signature": False}) # Header self.assertEqual("JWT", headers["typ"]) self.assertEqual("ES256", headers["alg"]) self.assertEqual("keyId", headers["kid"]) # Payload self.assertEqual("issuerId", payload['iss']) self.assertIsNotNone(payload['iat']) self.assertFalse('exp' in payload) self.assertEqual("advanced-commerce-api", payload['aud']) self.assertEqual('bundleId', payload['bid']) self.assertIsNotNone(payload['nonce']) request = payload['request'] decode_json = json.loads(str(base64.b64decode(request).decode('utf-8'))) self.assertEqual('testValue', decode_json['test_value']) 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') with self.assertRaises(ValueError): signature_creator.create_signature(None) ================================================ FILE: tests/test_payload_verification.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. import unittest from base64 import b64decode 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 class PayloadVerification(unittest.TestCase): 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') notification = verifier.verify_and_decode_notification(test_notification) self.assertEqual(notification.notificationType, NotificationTypeV2.TEST) def test_app_store_server_notification_decoding_production(self): verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example") test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification') with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_notification(test_notification) self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT) def test_missing_x5c_header(self): verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") missing_x5c_header_claim = read_data_from_file('tests/resources/mock_signed_data/missingX5CHeaderClaim') with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_notification(missing_x5c_header_claim) self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) def test_wrong_bundle_id_for_server_notification(self): verifier = get_signed_data_verifier(Environment.SANDBOX, "com.examplex") wrong_bundle = read_data_from_file('tests/resources/mock_signed_data/wrongBundleId') with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_notification(wrong_bundle) self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) def test_wrong_app_apple_id_for_server_notification(self): verifier = get_signed_data_verifier(Environment.PRODUCTION, "com.example", 1235) test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification') with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_notification(test_notification) self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER) def test_renewal_info_decoding(self): verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") renewal_info = read_data_from_file('tests/resources/mock_signed_data/renewalInfo') notification = verifier.verify_and_decode_renewal_info(renewal_info) self.assertEqual(notification.environment, Environment.SANDBOX) 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) self.assertEqual(notification.environment, Environment.SANDBOX) def test_malformed_jwt_with_too_many_parts(self): verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_notification("a.b.c.d") self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) def test_malformed_jwt_with_malformed_data(self): verifier = get_signed_data_verifier(Environment.SANDBOX, "com.example") with self.assertRaises(VerificationException) as context: verifier.verify_and_decode_notification("a.b.c") self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/test_promotional_offer_signature_creator.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. import unittest from appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator from tests.util import read_data_from_binary_file from uuid import UUID class PromotionalOfferSignatureCreatorTest(unittest.TestCase): 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) ================================================ FILE: tests/test_receipt_utility.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. import unittest from appstoreserverlibrary.receipt_utility import ReceiptUtility from tests.util import read_data_from_file APP_RECEIPT_EXPECTED_TRANSACTION_ID = "0" TRANSACTION_RECEIPT_EXPECTED_TRANSACTION_ID = "33993399" class ReceiptUtilityTest(unittest.TestCase): def test_xcode_app_receipt_extraction_with_no_transactions(self): receipt = read_data_from_file("tests/resources/xcode/xcode-app-receipt-empty") receipt_util = ReceiptUtility() extracted_transaction_id = receipt_util.extract_transaction_id_from_app_receipt(receipt) self.assertIsNone(extracted_transaction_id) def test_xcode_app_receipt_extraction_with_transactions(self): receipt = read_data_from_file("tests/resources/xcode/xcode-app-receipt-with-transaction") receipt_util = ReceiptUtility() extracted_transaction_id = receipt_util.extract_transaction_id_from_app_receipt(receipt) self.assertEqual(APP_RECEIPT_EXPECTED_TRANSACTION_ID, extracted_transaction_id) def test_transaction_receipt_extraction(self): receipt = read_data_from_file("tests/resources/mock_signed_data/legacyTransaction") receipt_util = ReceiptUtility() extracted_transaction_id = receipt_util.extract_transaction_id_from_transaction_receipt(receipt) self.assertEqual(TRANSACTION_RECEIPT_EXPECTED_TRANSACTION_ID, extracted_transaction_id) ================================================ FILE: tests/test_retention_messaging.py ================================================ # Copyright (c) 2025 Apple Inc. Licensed under MIT License. import unittest from uuid import UUID from appstoreserverlibrary.models.AlternateProduct import AlternateProduct from appstoreserverlibrary.models.DecodedRealtimeRequestBody import DecodedRealtimeRequestBody from appstoreserverlibrary.models.Environment import Environment from appstoreserverlibrary.models.Message import Message from appstoreserverlibrary.models.PromotionalOffer import PromotionalOffer from appstoreserverlibrary.models.PromotionalOfferSignatureV1 import PromotionalOfferSignatureV1 from appstoreserverlibrary.models.RealtimeResponseBody import RealtimeResponseBody from appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter class RetentionMessaging(unittest.TestCase): def test_realtime_response_body_with_message(self): # Create a RealtimeResponseBody with a Message message_id = UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890') message = Message(messageIdentifier=message_id) response_body = RealtimeResponseBody(message=message) # Serialize to dict c = _get_cattrs_converter(RealtimeResponseBody) json_dict = c.unstructure(response_body) # Validate structure self.assertIn('message', json_dict) self.assertIn('messageIdentifier', json_dict['message']) self.assertEqual('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', json_dict['message']['messageIdentifier']) self.assertNotIn('alternateProduct', json_dict) self.assertNotIn('promotionalOffer', json_dict) # Deserialize back deserialized = c.structure(json_dict, RealtimeResponseBody) # Verify self.assertIsNotNone(deserialized.message) self.assertEqual(message_id, deserialized.message.messageIdentifier) self.assertIsNone(deserialized.alternateProduct) self.assertIsNone(deserialized.promotionalOffer) def test_realtime_response_body_with_alternate_product(self): # Create a RealtimeResponseBody with an AlternateProduct message_id = UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901') product_id = 'com.example.alternate.product' alternate_product = AlternateProduct(messageIdentifier=message_id, productId=product_id) response_body = RealtimeResponseBody(alternateProduct=alternate_product) # Serialize to dict c = _get_cattrs_converter(RealtimeResponseBody) json_dict = c.unstructure(response_body) # Validate structure self.assertIn('alternateProduct', json_dict) self.assertIn('messageIdentifier', json_dict['alternateProduct']) self.assertIn('productId', json_dict['alternateProduct']) self.assertEqual('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', json_dict['alternateProduct']['messageIdentifier']) self.assertEqual('com.example.alternate.product', json_dict['alternateProduct']['productId']) self.assertNotIn('message', json_dict) self.assertNotIn('promotionalOffer', json_dict) # Deserialize back deserialized = c.structure(json_dict, RealtimeResponseBody) # Verify self.assertIsNone(deserialized.message) self.assertIsNotNone(deserialized.alternateProduct) self.assertEqual(message_id, deserialized.alternateProduct.messageIdentifier) self.assertEqual(product_id, deserialized.alternateProduct.productId) self.assertIsNone(deserialized.promotionalOffer) def test_realtime_response_body_with_promotional_offer_v2(self): # Create a RealtimeResponseBody with a PromotionalOffer (V2 signature) message_id = UUID('c3d4e5f6-a789-0123-c3d4-e5f6a7890123') signature_v2 = 'signature2' promotional_offer = PromotionalOffer(messageIdentifier=message_id, promotionalOfferSignatureV2=signature_v2) response_body = RealtimeResponseBody(promotionalOffer=promotional_offer) # Serialize to dict c = _get_cattrs_converter(RealtimeResponseBody) json_dict = c.unstructure(response_body) # Validate structure self.assertIn('promotionalOffer', json_dict) self.assertIn('messageIdentifier', json_dict['promotionalOffer']) self.assertIn('promotionalOfferSignatureV2', json_dict['promotionalOffer']) self.assertEqual('c3d4e5f6-a789-0123-c3d4-e5f6a7890123', json_dict['promotionalOffer']['messageIdentifier']) self.assertEqual('signature2', json_dict['promotionalOffer']['promotionalOfferSignatureV2']) self.assertNotIn('promotionalOfferSignatureV1', json_dict['promotionalOffer']) self.assertNotIn('message', json_dict) self.assertNotIn('alternateProduct', json_dict) # Deserialize back deserialized = c.structure(json_dict, RealtimeResponseBody) # Verify self.assertIsNone(deserialized.message) self.assertIsNone(deserialized.alternateProduct) self.assertIsNotNone(deserialized.promotionalOffer) self.assertEqual(message_id, deserialized.promotionalOffer.messageIdentifier) self.assertEqual(signature_v2, deserialized.promotionalOffer.promotionalOfferSignatureV2) self.assertIsNone(deserialized.promotionalOffer.promotionalOfferSignatureV1) def test_realtime_response_body_with_promotional_offer_v1(self): # Create a RealtimeResponseBody with a PromotionalOffer (V1 signature) message_id = UUID('d4e5f6a7-8901-2345-d4e5-f6a789012345') nonce = UUID('e5f6a789-0123-4567-e5f6-a78901234567') app_account_token = UUID('f6a78901-2345-6789-f6a7-890123456789') signature_v1 = PromotionalOfferSignatureV1( encodedSignature='base64encodedSignature', productId='com.example.product', nonce=nonce, timestamp=1698148900000, keyId='keyId123', offerIdentifier='offer123', appAccountToken=app_account_token ) promotional_offer = PromotionalOffer(messageIdentifier=message_id, promotionalOfferSignatureV1=signature_v1) response_body = RealtimeResponseBody(promotionalOffer=promotional_offer) # Serialize to dict c = _get_cattrs_converter(RealtimeResponseBody) json_dict = c.unstructure(response_body) # Validate structure self.assertIn('promotionalOffer', json_dict) self.assertIn('messageIdentifier', json_dict['promotionalOffer']) self.assertIn('promotionalOfferSignatureV1', json_dict['promotionalOffer']) self.assertEqual('d4e5f6a7-8901-2345-d4e5-f6a789012345', json_dict['promotionalOffer']['messageIdentifier']) v1_node = json_dict['promotionalOffer']['promotionalOfferSignatureV1'] self.assertIn('encodedSignature', v1_node) self.assertIn('productId', v1_node) self.assertIn('nonce', v1_node) self.assertIn('timestamp', v1_node) self.assertIn('keyId', v1_node) self.assertIn('offerIdentifier', v1_node) self.assertIn('appAccountToken', v1_node) self.assertEqual('base64encodedSignature', v1_node['encodedSignature']) self.assertEqual('com.example.product', v1_node['productId']) self.assertEqual('e5f6a789-0123-4567-e5f6-a78901234567', v1_node['nonce']) self.assertEqual(1698148900000, v1_node['timestamp']) self.assertEqual('keyId123', v1_node['keyId']) self.assertEqual('offer123', v1_node['offerIdentifier']) self.assertEqual('f6a78901-2345-6789-f6a7-890123456789', v1_node['appAccountToken']) self.assertNotIn('promotionalOfferSignatureV2', json_dict['promotionalOffer']) self.assertNotIn('message', json_dict) self.assertNotIn('alternateProduct', json_dict) # Deserialize back deserialized = c.structure(json_dict, RealtimeResponseBody) # Verify self.assertIsNone(deserialized.message) self.assertIsNone(deserialized.alternateProduct) self.assertIsNotNone(deserialized.promotionalOffer) self.assertEqual(message_id, deserialized.promotionalOffer.messageIdentifier) self.assertIsNone(deserialized.promotionalOffer.promotionalOfferSignatureV2) self.assertIsNotNone(deserialized.promotionalOffer.promotionalOfferSignatureV1) deserialized_v1 = deserialized.promotionalOffer.promotionalOfferSignatureV1 self.assertEqual('com.example.product', deserialized_v1.productId) self.assertEqual('offer123', deserialized_v1.offerIdentifier) self.assertEqual(nonce, deserialized_v1.nonce) self.assertEqual(1698148900000, deserialized_v1.timestamp) self.assertEqual('keyId123', deserialized_v1.keyId) self.assertEqual(app_account_token, deserialized_v1.appAccountToken) self.assertEqual('base64encodedSignature', deserialized_v1.encodedSignature) ================================================ FILE: tests/test_x509_verifiction.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. import unittest from unittest import mock from unittest.mock import MagicMock, patch from appstoreserverlibrary.signed_data_verifier import _ChainVerifier, VerificationException, VerificationStatus from base64 import b64decode, b64encode ROOT_CA_BASE64_ENCODED = "MIIBgjCCASmgAwIBAgIJALUc5ALiH5pbMAoGCCqGSM49BAMDMDYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8wHhcNMjMwMTA1MjEzMDIyWhcNMzMwMTAyMjEzMDIyWjA2MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJQ3VwZXJ0aW5vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc+/Bl+gospo6tf9Z7io5tdKdrlN1YdVnqEhEDXDShzdAJPQijamXIMHf8xWWTa1zgoYTxOKpbuJtDplz1XriTaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDRwAwRAIgemWQXnMAdTad2JDJWng9U4uBBL5mA7WI05H7oH7c6iQCIHiRqMjNfzUAyiu9h6rOU/K+iTR0I/3Y/NSWsXHX+acc" INTERMEDIATE_CA_BASE64_ENCODED = "MIIBnzCCAUWgAwIBAgIBCzAKBggqhkjOPQQDAzA2MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJQ3VwZXJ0aW5vMB4XDTIzMDEwNTIxMzEwNVoXDTMzMDEwMTIxMzEwNVowRTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xFTATBgNVBAoMDEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBUN5V9rKjfRiMAIojEA0Av5Mp0oF+O0cL4gzrTF178inUHugj7Et46NrkQ7hKgMVnjogq45Q1rMs+cMHVNILWqjNTAzMA8GA1UdEwQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMBAGCiqGSIb3Y2QGAgEEAgUAMAoGCCqGSM49BAMDA0gAMEUCIQCmsIKYs41ullssHX4rVveUT0Z7Is5/hLK1lFPTtun3hAIgc2+2RG5+gNcFVcs+XJeEl4GZ+ojl3ROOmll+ye7dynQ=" LEAF_CERT_BASE64_ENCODED = "MIIBoDCCAUagAwIBAgIBDDAKBggqhkjOPQQDAzBFMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUN1cGVydGlubzEVMBMGA1UECgwMSW50ZXJtZWRpYXRlMB4XDTIzMDEwNTIxMzEzNFoXDTMzMDEwMTIxMzEzNFowPTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xDTALBgNVBAoMBExlYWYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATitYHEaYVuc8g9AjTOwErMvGyPykPa+puvTI8hJTHZZDLGas2qX1+ErxgQTJgVXv76nmLhhRJH+j25AiAI8iGsoy8wLTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNIADBFAiBX4c+T0Fp5nJ5QRClRfu5PSByRvNPtuaTsk0vPB3WAIAIhANgaauAj/YP9s0AkEhyJhxQO/6Q2zouZ+H1CIOehnMzQ" INTERMEDIATE_CA_INVALID_OID_BASE64_ENCODED = "MIIBnjCCAUWgAwIBAgIBDTAKBggqhkjOPQQDAzA2MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJQ3VwZXJ0aW5vMB4XDTIzMDEwNTIxMzYxNFoXDTMzMDEwMTIxMzYxNFowRTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xFTATBgNVBAoMDEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBUN5V9rKjfRiMAIojEA0Av5Mp0oF+O0cL4gzrTF178inUHugj7Et46NrkQ7hKgMVnjogq45Q1rMs+cMHVNILWqjNTAzMA8GA1UdEwQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMBAGCiqGSIb3Y2QGAgIEAgUAMAoGCCqGSM49BAMDA0cAMEQCIFROtTE+RQpKxNXETFsf7Mc0h+5IAsxxo/X6oCC/c33qAiAmC5rn5yCOOEjTY4R1H1QcQVh+eUwCl13NbQxWCuwxxA==" LEAF_CERT_FOR_INTERMEDIATE_CA_INVALID_OID_BASE64_ENCODED = "MIIBnzCCAUagAwIBAgIBDjAKBggqhkjOPQQDAzBFMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUN1cGVydGlubzEVMBMGA1UECgwMSW50ZXJtZWRpYXRlMB4XDTIzMDEwNTIxMzY1OFoXDTMzMDEwMTIxMzY1OFowPTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xDTALBgNVBAoMBExlYWYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATitYHEaYVuc8g9AjTOwErMvGyPykPa+puvTI8hJTHZZDLGas2qX1+ErxgQTJgVXv76nmLhhRJH+j25AiAI8iGsoy8wLTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNHADBEAiAUAs+gzYOsEXDwQquvHYbcVymyNqDtGw9BnUFp2YLuuAIgXxQ3Ie9YU0cMqkeaFd+lyo0asv9eyzk6stwjeIeOtTU=" LEAF_CERT_INVALID_OID_BASE64_ENCODED = "MIIBoDCCAUagAwIBAgIBDzAKBggqhkjOPQQDAzBFMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUN1cGVydGlubzEVMBMGA1UECgwMSW50ZXJtZWRpYXRlMB4XDTIzMDEwNTIxMzczMVoXDTMzMDEwMTIxMzczMVowPTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xDTALBgNVBAoMBExlYWYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATitYHEaYVuc8g9AjTOwErMvGyPykPa+puvTI8hJTHZZDLGas2qX1+ErxgQTJgVXv76nmLhhRJH+j25AiAI8iGsoy8wLTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsCBAIFADAKBggqhkjOPQQDAwNIADBFAiAb+7S3i//bSGy7skJY9+D4VgcQLKFeYfIMSrUCmdrFqwIhAIMVwzD1RrxPRtJyiOCXLyibIvwcY+VS73HYfk0O9lgz" LEAF_CERT_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4rWBxGmFbnPIPQI0zsBKzLxsj8pD\n2vqbr0yPISUx2WQyxmrNql9fhK8YEEyYFV7++p5i4YUSR/o9uQIgCPIhrA==\n-----END PUBLIC KEY-----\n" REAL_APPLE_ROOT_BASE64_ENCODED = "MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtfTjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySrMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM6BgD56KyKA==" REAL_APPLE_INTERMEDIATE_BASE64_ENCODED = "MIIDFjCCApygAwIBAgIUIsGhRwp0c2nvU4YSycafPTjzbNcwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMjEwMzE3MjAzNzEwWhcNMzYwMzE5MDAwMDAwWjB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbsQKC94PrlWmZXnXgtxzdVJL8T0SGYngDRGpngn3N6PT8JMEb7FDi4bBmPhCnZ3/sq6PF/cGcKXWsL5vOteRhyJ45x3ASP7cOB+aao90fcpxSv/EZFbniAbNgZGhIhpIo4H6MIH3MBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUu7DeoVgziJqkipnevr3rr9rLJKswRgYIKwYBBQUHAQEEOjA4MDYGCCsGAQUFBzABhipodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLWFwcGxlcm9vdGNhZzMwNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NybC5hcHBsZS5jb20vYXBwbGVyb290Y2FnMy5jcmwwHQYDVR0OBBYEFD8vlCNR01DJmig97bB85c+lkGKZMA4GA1UdDwEB/wQEAwIBBjAQBgoqhkiG92NkBgIBBAIFADAKBggqhkjOPQQDAwNoADBlAjBAXhSq5IyKogMCPtw490BaB677CaEGJXufQB/EqZGd6CSjiCtOnuMTbXVXmxxcxfkCMQDTSPxarZXvNrkxU3TkUMI33yzvFVVRT4wxWJC994OsdcZ4+RGNsYDyR5gmdr0nDGg=" REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED = "MIIEMTCCA7agAwIBAgIQR8KHzdn554Z/UoradNx9tzAKBggqhkjOPQQDAzB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTI1MDkxOTE5NDQ1MVoXDTI3MTAxMzE3NDcyM1owgZIxQDA+BgNVBAMMN1Byb2QgRUNDIE1hYyBBcHAgU3RvcmUgYW5kIGlUdW5lcyBTdG9yZSBSZWNlaXB0IFNpZ25pbmcxLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNnVvhcv7iT+7Ex5tBMBgrQspHzIsXRi0Yxfek7lv8wEmj/bHiWtNwJqc2BoHzsQiEjP7KFIIKg4Y8y0/nynuAmjggIIMIICBDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFD8vlCNR01DJmig97bB85c+lkGKZMHAGCCsGAQUFBwEBBGQwYjAtBggrBgEFBQcwAoYhaHR0cDovL2NlcnRzLmFwcGxlLmNvbS93d2RyZzYuZGVyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLXd3ZHJnNjAyMIIBHgYDVR0gBIIBFTCCAREwggENBgoqhkiG92NkBQYBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wHQYDVR0OBBYEFIFioG4wMMVA1ku9zJmGNPAVn3eqMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNpADBmAjEA+qXnREC7hXIWVLsLxznjRpIzPf7VHz9V/CTm8+LJlrQepnmcPvGLNcX6XPnlcgLAAjEA5IjNZKgg5pQ79knF4IbTXdKv8vutIDMXDmjPVT3dGvFtsGRwXOywR2kZCdSrfeot" EFFECTIVE_DATE = 1761962975 CLOCK_DATE = 41231 class X509Verification(unittest.TestCase): def test_valid_chain_without_ocsp(self): verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False) public_key = verifier.verify_chain([ LEAF_CERT_BASE64_ENCODED, INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], False, EFFECTIVE_DATE) self.assertEqual(LEAF_CERT_PUBLIC_KEY, public_key) def test_valid_chain_invalid_intermediate_OID_without_OCSP(self): verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False) with self.assertRaises(VerificationException) as context: verifier.verify_chain([ LEAF_CERT_FOR_INTERMEDIATE_CA_INVALID_OID_BASE64_ENCODED, INTERMEDIATE_CA_INVALID_OID_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], False, EFFECTIVE_DATE) self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) def test_valid_chain_invalid_leaf_OID_without_OCSP(self): verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False) with self.assertRaises(VerificationException) as context: verifier.verify_chain([ LEAF_CERT_INVALID_OID_BASE64_ENCODED, INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], False, EFFECTIVE_DATE) self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) def test_invalid_chain_length(self): verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False) with self.assertRaises(VerificationException) as context: verifier.verify_chain([INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED], False, EFFECTIVE_DATE) self.assertIn('Verification failed with status INVALID_CHAIN_LENGTH', str(context.exception)) def test_invalid_base64_in_certificate_list(self): verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)]) with self.assertRaises(VerificationException) as context: verifier.verify_chain([ "abc", INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], False, EFFECTIVE_DATE) self.assertEqual(VerificationStatus.INVALID_CERTIFICATE, context.exception.status) def test_invalid_data_in_certificate_list(self): verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False) with self.assertRaises(VerificationException) as context: verifier.verify_chain([ str(b64encode(b"abc")), INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], False, EFFECTIVE_DATE) self.assertEqual(VerificationStatus.INVALID_CERTIFICATE, context.exception.status) def test_malformed_root_cert(self): verifier = _ChainVerifier([b64decode(b64encode(b"abc"))]) with self.assertRaises(Exception) 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_chain_different_than_root_certificate(self): verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)], False) with self.assertRaises(Exception) as context: verifier.verify_chain([ LEAF_CERT_BASE64_ENCODED, INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], False, EFFECTIVE_DATE) self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) def test_valid_expired_chain(self): verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False) with self.assertRaises(VerificationException) as context: verifier.verify_chain([ LEAF_CERT_BASE64_ENCODED, INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], False, 2280946846) self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status) def test_apple_chain_is_valid_with_ocsp_and_strict(self): verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)]) verifier.verify_chain([ REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED, REAL_APPLE_INTERMEDIATE_BASE64_ENCODED, REAL_APPLE_ROOT_BASE64_ENCODED ], True, EFFECTIVE_DATE) def test_ocsp_response_caching(self): verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)]) magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED) verifier._verify_chain_without_caching = magic_mock with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): verifier.verify_chain([ REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED, REAL_APPLE_INTERMEDIATE_BASE64_ENCODED, REAL_APPLE_ROOT_BASE64_ENCODED ], True, EFFECTIVE_DATE) self.assertEqual(1, magic_mock.call_count) with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE + 1)): # 1 second verifier.verify_chain([ REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED, REAL_APPLE_INTERMEDIATE_BASE64_ENCODED, REAL_APPLE_ROOT_BASE64_ENCODED ], True, EFFECTIVE_DATE) self.assertEqual(1, magic_mock.call_count) def test_ocsp_response_caching_has_expiration(self): verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)]) magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED) verifier._verify_chain_without_caching = magic_mock with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): verifier.verify_chain([ REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED, REAL_APPLE_INTERMEDIATE_BASE64_ENCODED, REAL_APPLE_ROOT_BASE64_ENCODED ], True, EFFECTIVE_DATE) self.assertEqual(1, magic_mock.call_count) with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE + 900)): # 15 minutes verifier.verify_chain([ REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED, REAL_APPLE_INTERMEDIATE_BASE64_ENCODED, REAL_APPLE_ROOT_BASE64_ENCODED ], True, EFFECTIVE_DATE) self.assertEqual(2, magic_mock.call_count) def test_ocsp_response_caching_with_different_chain(self): verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)]) magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED) verifier._verify_chain_without_caching = magic_mock with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): verifier.verify_chain([ LEAF_CERT_BASE64_ENCODED, INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], True, EFFECTIVE_DATE) self.assertEqual(1, magic_mock.call_count) with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): # Same verifier.verify_chain([ REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED, REAL_APPLE_INTERMEDIATE_BASE64_ENCODED, REAL_APPLE_ROOT_BASE64_ENCODED ], True, EFFECTIVE_DATE) self.assertEqual(2, magic_mock.call_count) def test_ocsp_response_caching_with_slightly_different_chain(self): verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)]) magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED) verifier._verify_chain_without_caching = magic_mock with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): verifier.verify_chain([ LEAF_CERT_BASE64_ENCODED, INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED ], True, EFFECTIVE_DATE) self.assertEqual(1, magic_mock.call_count) with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): # Same verifier.verify_chain([ LEAF_CERT_BASE64_ENCODED, INTERMEDIATE_CA_BASE64_ENCODED, REAL_APPLE_ROOT_BASE64_ENCODED ], True, EFFECTIVE_DATE) self.assertEqual(2, magic_mock.call_count) if __name__ == '__main__': unittest.main() ================================================ FILE: tests/test_xcode_signed_data.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. import unittest 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 tests.util import get_signed_data_verifier, read_data_from_file XCODE_BUNDLE_ID = "com.example.naturelab.backyardbirds.example" class ReceiptUtilityTest(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") app_transaction = verifier.verify_and_decode_app_transaction(encoded_app_transaction) self.assertIsNotNone(app_transaction) self.assertIsNone(app_transaction.appAppleId) self.assertEqual(XCODE_BUNDLE_ID, app_transaction.bundleId) self.assertEqual("1", app_transaction.applicationVersion) self.assertIsNone(app_transaction.versionExternalIdentifier) 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.assertIsNone(app_transaction.preorderDate) self.assertEqual(Environment.XCODE, app_transaction.receiptType) self.assertEqual("Xcode", app_transaction.rawReceiptType) 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) self.assertEqual("0", transaction.originalTransactionId) self.assertEqual("0", transaction.transactionId) self.assertEqual("0", transaction.webOrderLineItemId) self.assertEqual(XCODE_BUNDLE_ID, transaction.bundleId) self.assertEqual("pass.premium", transaction.productId) self.assertEqual("6F3A93AB", transaction.subscriptionGroupIdentifier) self.assertEqual(1697679936049, transaction.purchaseDate) self.assertEqual(1697679936049, transaction.originalPurchaseDate) self.assertEqual(1700358336049, transaction.expiresDate) self.assertEqual(1, transaction.quantity) self.assertEqual(Type.AUTO_RENEWABLE_SUBSCRIPTION, transaction.type) self.assertEqual("Auto-Renewable Subscription", transaction.rawType) self.assertIsNone(transaction.appAccountToken) self.assertEqual(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType) self.assertEqual("PURCHASED", transaction.rawInAppOwnershipType) self.assertEqual(1697679936056, transaction.signedDate) self.assertIsNone(transaction.revocationReason) self.assertIsNone(transaction.revocationDate) self.assertFalse(transaction.isUpgraded) self.assertEqual(OfferType.INTRODUCTORY_OFFER, transaction.offerType) self.assertEqual(1, transaction.rawOfferType) self.assertIsNone(transaction.offerIdentifier) self.assertEqual(Environment.XCODE, transaction.environment) self.assertEqual("Xcode", transaction.rawEnvironment) self.assertEqual("USA", transaction.storefront) self.assertEqual("143441", transaction.storefrontId) self.assertEqual(TransactionReason.PURCHASE, transaction.transactionReason) self.assertEqual("PURCHASE", transaction.rawTransactionReason) def test_xcode_signed_renewal_info(self): verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID) encoded_renewal_info = read_data_from_file("tests/resources/xcode/xcode-signed-renewal-info") renewal_info = verifier.verify_and_decode_renewal_info(encoded_renewal_info) self.assertIsNone(renewal_info.expirationIntent) self.assertEqual("0", renewal_info.originalTransactionId) self.assertEqual("pass.premium", renewal_info.autoRenewProductId) self.assertEqual("pass.premium", renewal_info.productId) self.assertEqual(AutoRenewStatus.ON, renewal_info.autoRenewStatus) self.assertEqual(1, renewal_info.rawAutoRenewStatus) self.assertIsNone(renewal_info.isInBillingRetryPeriod) self.assertIsNone(renewal_info.priceIncreaseStatus) self.assertIsNone(renewal_info.gracePeriodExpiresDate) self.assertIsNone(renewal_info.offerType) self.assertIsNone(renewal_info.offerIdentifier) self.assertEqual(1697679936711, renewal_info.signedDate) self.assertEqual(Environment.XCODE, renewal_info.environment) self.assertEqual("Xcode", renewal_info.rawEnvironment) self.assertEqual(1697679936049, renewal_info.recentSubscriptionStartDate) self.assertEqual(1700358336049, renewal_info.renewalDate) 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: verifier.verify_and_decode_app_transaction(encoded_app_transaction) except VerificationException: return self.assertFalse(True) ================================================ FILE: tests/util.py ================================================ # Copyright (c) 2023 Apple Inc. Licensed under MIT License. from typing import Any, Dict import jwt from jwt.api_jwt import decode_complete import json import os from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import serialization from appstoreserverlibrary.models.Environment import Environment 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) 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') 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() return decode_complete(jwt=data, key=public_key, algorithms=['ES256'], options={"verify_signature": False}) def read_data_from_file(path: str) -> str: full_path = os.path.join(path) with open(full_path, mode='r') as test_file: return test_file.read() def read_data_from_binary_file(path: str) -> str: full_path = os.path.join(path) with open(full_path, mode='rb') as test_file: return test_file.read() def get_signed_data_verifier(env: Environment, bundle_id: str, app_apple_id: int = 1234) -> SignedDataVerifier: verifier = SignedDataVerifier([read_data_from_binary_file('tests/resources/certs/testCA.der')], False, env, bundle_id, app_apple_id) verifier._chain_verifier.enable_strict_checks = False # We don't have authority identifiers on test certs return verifier def get_default_signed_data_verifier(): return get_signed_data_verifier(Environment.LOCAL_TESTING, "com.example")