[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n      time: \"02:00\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n      time: \"02:00\"\n"
  },
  {
    "path": ".github/workflows/ci-prb.yml",
    "content": "name: PR Builder\npermissions:\n  contents: read\non:\n  pull_request:\n    branches: [ main ]\n  push:\n    branches: [ main ]\njobs:\n  build:\n    name: Python ${{ matrix.python }} ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        python: [ \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\" ]\n        os: [ ubuntu-latest ]\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v6\n      - name: Set up Python ${{ matrix.python }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python }}\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n      - name: Run tests\n        run: |\n          python -m unittest\n"
  },
  {
    "path": ".github/workflows/ci-release-docs.yml",
    "content": "name: Doc Builder\npermissions:\n  contents: read\non:\n  release:\n    types: [published]\njobs:\n  build:\n    name: Python Doc Builder\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: \"3.14\"\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install -r requirements.txt\n        pip install -r docs/requirements.txt\n    - name: Sphinx Api Docs\n      run: sphinx-apidoc -F -H \"App Store Server Library\" -A \"Apple Inc.\" -V \"0.2.1\" -e -a -o _staging . tests pyproject.toml\n    - name: Spinx build\n      run: sphinx-build -b html _staging _build\n    - name: Upload docs\n      uses: actions/upload-pages-artifact@v4\n      with:\n        path: _build\n  deploy:\n    permissions:\n      pages: write\n      id-token: write\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    needs: build\n    runs-on: ubuntu-latest\n    name: Deploy docs\n    steps:\n      - name: Deploy\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/ci-release.yml",
    "content": "name: Release Builder\npermissions:\n  contents: read\non:\n  release:\n    types: [published]\njobs:\n  build:\n    # Only non-pre-release builds trigger a release\n    if: \"!github.event.release.prerelease\"\n    name: Python Release Builder\n    runs-on: ubuntu-latest\n    environment: pypi\n    permissions:\n      id-token: write\n    steps:\n    - uses: actions/checkout@v6\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: \"3.14\"\n    - name: Install build\n      run: >-\n        python3 -m\n        pip install\n        build\n        --user\n    - name: Build the sdist and wheel\n      run: >-\n        python3 -m\n        build\n    - name: Publish to PyPI\n      uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/ci-snapshot.yml",
    "content": "name: Snapshot Builder\npermissions:\n  contents: read\non:\n  release:\n    types: [published]\njobs:\n  build:\n    # Pre-release builds trigger a snapshot being created\n    if: \"github.event.release.prerelease\"\n    name: Python Snapshot Builder\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v6\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: \"3.14\"\n    - name: Install build\n      run: >-\n        python3 -m\n        pip install\n        build\n        --user\n    - name: Build the sdist and wheel\n      run: >-\n        python3 -m\n        build\n    - name: Publish to Test PyPI\n      uses: pypa/gh-action-pypi-publish@release/v1\n      with:\n        password: ${{ secrets.TEST_PYPI_API_TOKEN }}\n        repository-url: https://test.pypi.org/legacy/"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__/\nbuild/\ndist/\n*.egg-info/\n.pytest_cache/\n\n# pyenv\n.python-version\n\n# Environments\n.env\n.venv\nvenv\n\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# JetBrains\n.idea/\n\n/coverage.xml\n/.coverage\n\n.DS_Store\n\n_build\n_staging\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## Version 3.0.0\n- Incorporate changes for App Store Server API v1.19 [https://github.com/apple/app-store-server-library-python/pull/172] from @riyazpanjwani\n  - 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\n\n## Version 2.0.0\n- Support Retention Messaging API [https://github.com/apple/app-store-server-library-python/pull/160]\n  - This changes internal details of BaseAppStoreServerAPIClient, which is a breaking change for subclassing clients\n- Incorporate changes for App Store Server API v1.17 [https://github.com/apple/app-store-server-library-python/pull/162] from @riyazpanjwani\n- Add a new VerificationStatus case for retryable OCSP network failures [https://github.com/apple/app-store-server-library-python/pull/163]\n- Add timeout to the AppStoreServerAPIClient [https://github.com/apple/app-store-server-library-python/pull/164]\n- Incorporate changes for App Store Server API v1.18 [https://github.com/apple/app-store-server-library-python/pull/166] from @izanger\n  - This changes OfferType's case SUBSCRIPTION_OFFER_CODE to OFFER_CODE, which is a breaking change\n\n## Version 1.9.0\n- Incorporate changes for App Store Server API v1.16 [https://github.com/apple/app-store-server-library-python/pull/141] from @riyazpanjwani\n- Fix SyntaxWarning in regex pattern string [https://github.com/apple/app-store-server-library-python/pull/138] from @krepe90\n\n## Version 1.8.0\n- 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]\n\n## Version 1.7.0\n- Update the SignedDataVerifier to cache verified certificate chains, improving performance [https://github.com/apple/app-store-server-library-python/pull/122]\n\n## Version 1.6.0\n- Update README to improve Dependabot link discovery [https://github.com/apple/app-store-server-library-python/pull/119]\n\n## Version 1.5.0\n- Add an async client built on httpx [https://github.com/apple/app-store-server-library-python/pull/105]\n- Drop Python 3.7 support [https://github.com/apple/app-store-server-library-python/pull/106]\n- Add check for original transaction id in legacy receipts [https://github.com/apple/app-store-server-library-python/pull/104] from @willhnation\n\n## Version 1.4.0\n- 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]\n- Remove the upper limit on libraries that are unlikely to break upon upgrade [https://github.com/apple/app-store-server-library-python/pull/101]\n\n## Version 1.3.0\n- 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]\n- Fix deprecation warnings from cryptography [https://github.com/apple/app-store-server-library-python/pull/94] from @WFT\n- Replace use of deprecated datetime.utcnow() [https://github.com/apple/app-store-server-library-python/pull/93] from @WFT\n- Cache cattrs values to prevent memory leak [https://github.com/apple/app-store-server-library-python/pull/92] from @Reskov\n\n## Version 1.2.1\n- Fix issue with OfferType in JWSTransactionDecodedPayload [https://github.com/apple/app-store-server-library-python/pull/88] from @devinwang\n\n## Version 1.2.0\n- 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]\n- Various documentation and quality of life improvements, including contributions from @CallumWatkins, @hakusai22, and @sunny-dubey\n\n## Version 1.1.0\n- Support App Store Server Notifications v2.10 [https://github.com/apple/app-store-server-library-python/pull/65]\n- 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]\n- Require appAppleId in SignedDataVerifier for the Production environment [https://github.com/apple/app-store-server-library-python/pull/60]\n\n## 1.0.0\n- Add error message to APIException [https://github.com/apple/app-store-server-library-python/pull/52]\n\n## 0.3.0\n- Add missing status field to the Data model [https://github.com/apple/app-store-server-library-python/pull/33]\n- Add error codes from App Store Server API v1.9 [https://github.com/apple/app-store-server-library-python/pull/39]\n- Add new fields from App Store Server API v1.10 [https://github.com/apple/app-store-server-library-python/pull/46]\n- Add support for reading unknown enum values [https://github.com/apple/app-store-server-library-python/pull/45]\n- Add support for Xcode and LocalTesting environments [https://github.com/apple/app-store-server-library-python/pull/44]\n\n## 0.2.1\n- Add py.typed file [https://github.com/apple/app-store-server-library-python/pull/19]\n- Correct type annotation in PromotionalOfferSignatureCreator [https://github.com/apple/app-store-server-library-python/pull/17]\n\n## 0.2.0\n\n- Correct type in LastTransactionsItem's status field [https://github.com/apple/app-store-server-library-python/pull/11]\n- Fix default value None for fields should require an Optional type [https://github.com/apple/app-store-server-library-python/pull/6]\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, caste, color, religion, or sexual\nidentity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the overall\n  community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or advances of\n  any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email address,\n  without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n[opensource-conduct@group.apple.com](mailto:opensource-conduct@group.apple.com).\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of\nactions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or permanent\nban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the\ncommunity.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.1, available at\n[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].\n\nCommunity Impact Guidelines were inspired by\n[Mozilla's code of conduct enforcement ladder][Mozilla CoC].\n\nFor answers to common questions about this code of conduct, see the FAQ at\n[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at\n[https://www.contributor-covenant.org/translations][translations].\n\n[homepage]: https://www.contributor-covenant.org\n[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html\n[Mozilla CoC]: https://github.com/mozilla/diversity\n[FAQ]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nThank you for your interest in contributing! \n\n## Reporting Bugs\n\nPlease report bugs by creating [Github issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/about-issues).\nTo help the community understand the bug and get it fixed faster, please provide the following information when creating a new issue:\n- A clear and descriptive title\n- The exact steps to reproduce the bug\n- The observed behavior and expected behavior\n\nIf possible, also include payloads, commands, screenshots, etc to help the community identify the problem. Do not include any personal or sensitive data.\n\n## Suggesting Improvements\n\nYou can suggest improvements also by creating Github issues.\nWhen creating a new suggestion, please provide the following information:\n- A clear and descriptive title\n- A description of the proposed improvement in as many details as possible\n- Explain why the improvement is important\n\n## Documentation Contribution\n\nDocumentation contribution will make it easier for the community to work on the project.\nYou 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. \n\n## Code Contribution\n\nFor minor changes (like small bug fixes or typo correction), feel free to open up a PR directly.\nFor 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.\n\n## Project Licensing\n\nAll contributions (including Pull Requests) to this project are provided under the terms of the project’s [LICENSE](LICENSE.txt)"
  },
  {
    "path": "LICENSE.txt",
    "content": "Copyright 2023 Apple Inc.\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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."
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md"
  },
  {
    "path": "NOTICE.txt",
    "content": "Acknowledgements\nPortions of this App Store Server Library software may utilize the following copyrighted \nmaterial, the use of which is hereby acknowledged.\n\n_____________________\n\nHynek Schlawack and the attrs contributors (attrs)\nThe MIT License (MIT)\n\nCopyright (c) 2015 Hynek Schlawack and the attrs contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n_____________________\n\nJosé Padilla (pyjwt)\n\nThe MIT License (MIT)\n\nCopyright (c) 2015-2022 José Padilla\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n_____________________\n\nrequests contributors (requests)\n\n             Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n_____________________\n\ncryptography contributors (pyca/cryptography)\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        https://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       https://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n_____________________\n\npyOpenSSL contributors (pyOpenSSL)\n\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n_____________________\n\nThe Python-ASN1 authors. (python-asn1)\n\nCopyright (c) 2007-2021 the Python-ASN1 authors.\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n\n_____________________\n\nTin Tvrtković (cattrs)\n\nMIT License\n\nCopyright (c) 2016, Tin Tvrtković\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n\n_____________________\n\nEncode OSS Ltd. (httpx)\n\nCopyright © 2019, Encode OSS Ltd. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\nRedistributions 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.\n\nNeither 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.\n\nTHIS 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."
  },
  {
    "path": "README.md",
    "content": "# Apple App Store Server Python Library\nThe [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).\n\n## Table of Contents\n1. [Installation](#installation)\n2. [Documentation](#documentation)\n3. [Usage](#usage)\n4. [Support](#support)\n\n## Installation\n\n#### Requirements\n\n- Python 3.8+\n\n### pip\n```sh\npip install app-store-server-library\n```\n\n## Documentation\n\n[Documentation](https://apple.github.io/app-store-server-library-python/)\n\n[WWDC Video](https://developer.apple.com/videos/play/wwdc2023/10143/)\n\n### Obtaining an In-App Purchase key from App Store Connect\n\nTo 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.\n\n### Obtaining Apple Root Certificates\n\nDownload 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.\n\n## Usage\n\n### API Usage\n\n```python\nfrom appstoreserverlibrary.api_client import AppStoreServerAPIClient, APIException\nfrom appstoreserverlibrary.models.Environment import Environment\n\nprivate_key = read_private_key(\"/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8\") # Implementation will vary\n\nkey_id = \"ABCDEFGHIJ\"\nissuer_id = \"99b16628-15e4-4668-972b-eeff55eeff55\"\nbundle_id = \"com.example\"\nenvironment = Environment.SANDBOX\n\nclient = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)\n\ntry:    \n    response = client.request_test_notification()\n    print(response)\nexcept APIException as e:\n    print(e)\n```\n\n### Verification Usage\n\n```python\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.signed_data_verifier import VerificationException, SignedDataVerifier\n\nroot_certificates = load_root_certificates()\nenable_online_checks = True\nbundle_id = \"com.example\"\nenvironment = Environment.SANDBOX\napp_apple_id = None # appAppleId must be provided for the Production environment\nsigned_data_verifier = SignedDataVerifier(root_certificates, enable_online_checks, environment, bundle_id, app_apple_id)\n\ntry:    \n    signed_notification = \"ey..\"\n    payload = signed_data_verifier.verify_and_decode_notification(signed_notification)\n    print(payload)\nexcept VerificationException as e:\n    print(e)\n```\n\n### Receipt Usage\n\n```python\nfrom appstoreserverlibrary.api_client import AppStoreServerAPIClient, APIException, GetTransactionHistoryVersion\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.receipt_utility import ReceiptUtility\nfrom appstoreserverlibrary.models.HistoryResponse import HistoryResponse\nfrom appstoreserverlibrary.models.TransactionHistoryRequest import TransactionHistoryRequest, ProductType, Order\n\nprivate_key = read_private_key(\"/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8\") # Implementation will vary\n\nkey_id = \"ABCDEFGHIJ\"\nissuer_id = \"99b16628-15e4-4668-972b-eeff55eeff55\"\nbundle_id = \"com.example\"\nenvironment = Environment.SANDBOX\n\nclient = AppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)\nreceipt_util = ReceiptUtility()\napp_receipt = \"MI..\"\n\ntry:    \n    transaction_id = receipt_util.extract_transaction_id_from_app_receipt(app_receipt)\n    if transaction_id != None:\n        transactions = []\n        response: HistoryResponse = None\n        request: TransactionHistoryRequest = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            revoked=False,\n            productTypes=[ProductType.AUTO_RENEWABLE]\n        )\n        while response == None or response.hasMore:\n            revision = response.revision if response != None else None\n            response = client.get_transaction_history(transaction_id, revision, request, GetTransactionHistoryVersion.V2)\n            for transaction in response.signedTransactions:\n                transactions.append(transaction)\n        print(transactions)\nexcept APIException as e:\n    print(e)\n\n```\n\n### Promotional Offer Signature Creation\n\n```python\nfrom appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator\nimport time\n\nprivate_key = read_private_key(\"/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8\") # Implementation will vary\n\nkey_id = \"ABCDEFGHIJ\"\nbundle_id = \"com.example\"\n\npromotion_code_signature_generator = PromotionalOfferSignatureCreator(private_key, key_id, bundle_id)\n\nproduct_id = \"<product_id>\"\nsubscription_offer_id = \"<subscription_offer_id>\"\napplication_username = \"<application_username>\"\nnonce = \"<nonce>\"\ntimestamp = round(time.time()*1000)\nbase64_encoded_signature = promotion_code_signature_generator.create_signature(product_id, subscription_offer_id, application_username, nonce, timestamp)\n```\n\n### Async HTTPX Support\n\n#### Pip\nInclude the optional async dependency\n```sh\npip install app-store-server-library[async]\n```\n\n#### API Usage\n```python\nfrom appstoreserverlibrary.api_client import AsyncAppStoreServerAPIClient, APIException\nfrom appstoreserverlibrary.models.Environment import Environment\n\nprivate_key = read_private_key(\"/path/to/key/SubscriptionKey_ABCDEFGHIJ.p8\") # Implementation will vary\n\nkey_id = \"ABCDEFGHIJ\"\nissuer_id = \"99b16628-15e4-4668-972b-eeff55eeff55\"\nbundle_id = \"com.example\"\nenvironment = Environment.SANDBOX\n\nclient = AsyncAppStoreServerAPIClient(private_key, key_id, issuer_id, bundle_id, environment)\n\ntry:    \n    response = await client.request_test_notification()\n    print(response)\nexcept APIException as e:\n    print(e)\n\n# Once client use is finished\nawait client.async_close()\n```\n\n## Support\n\nOnly the latest major version of the library will receive updates, including security updates. Therefore, it is recommended to update to new major versions.\n"
  },
  {
    "path": "appstoreserverlibrary/__init__.py",
    "content": ""
  },
  {
    "path": "appstoreserverlibrary/api_client.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nimport calendar\nimport datetime\nimport warnings\nfrom enum import IntEnum, Enum\nfrom typing import Any, Dict, List, MutableMapping, Optional, Type, TypeVar, Union\nfrom attr import define\nimport requests\n\nimport jwt\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import serialization\n\nfrom appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter\nfrom .models.CheckTestNotificationResponse import CheckTestNotificationResponse\nfrom .models.ConsumptionRequest import ConsumptionRequest\nfrom .models.ConsumptionRequestV1 import ConsumptionRequestV1\nfrom .models.DefaultConfigurationRequest import DefaultConfigurationRequest\nfrom .models.DefaultConfigurationResponse import DefaultConfigurationResponse\nfrom .models.Environment import Environment\nfrom .models.ExtendRenewalDateRequest import ExtendRenewalDateRequest\nfrom .models.ExtendRenewalDateResponse import ExtendRenewalDateResponse\nfrom .models.GetImageListResponse import GetImageListResponse\nfrom .models.GetMessageListResponse import GetMessageListResponse\nfrom .models.HistoryResponse import HistoryResponse\nfrom .models.ImageSize import ImageSize\nfrom .models.MassExtendRenewalDateRequest import MassExtendRenewalDateRequest\nfrom .models.MassExtendRenewalDateResponse import MassExtendRenewalDateResponse\nfrom .models.MassExtendRenewalDateStatusResponse import MassExtendRenewalDateStatusResponse\nfrom .models.NotificationHistoryRequest import NotificationHistoryRequest\nfrom .models.NotificationHistoryResponse import NotificationHistoryResponse\nfrom .models.OrderLookupResponse import OrderLookupResponse\nfrom .models.PerformanceTestRequest import PerformanceTestRequest\nfrom .models.PerformanceTestResponse import PerformanceTestResponse\nfrom .models.PerformanceTestResultResponse import PerformanceTestResultResponse\nfrom .models.RealtimeUrlRequest import RealtimeUrlRequest\nfrom .models.RealtimeUrlResponse import RealtimeUrlResponse\nfrom .models.RefundHistoryResponse import RefundHistoryResponse\nfrom .models.SendTestNotificationResponse import SendTestNotificationResponse\nfrom .models.Status import Status\nfrom .models.StatusResponse import StatusResponse\nfrom .models.TransactionHistoryRequest import TransactionHistoryRequest\nfrom .models.TransactionInfoResponse import TransactionInfoResponse\nfrom .models.AppTransactionInfoResponse import AppTransactionInfoResponse\nfrom .models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest\nfrom .models.UploadMessageRequestBody import UploadMessageRequestBody\nfrom uuid import UUID\n\nT = TypeVar('T')\n\nclass APIError(IntEnum):\n    GENERAL_BAD_REQUEST = 4000000\n    \"\"\"\n    An error that indicates an invalid request.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/generalbadrequesterror\n    \"\"\"\n\n    INVALID_APP_IDENTIFIER = 4000002\n    \"\"\"\n    An error that indicates an invalid app identifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidappidentifiererror\n    \"\"\"\n\n    INVALID_REQUEST_REVISION = 4000005\n    \"\"\"\n    An error that indicates an invalid request revision.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidrequestrevisionerror\n    \"\"\"\n\n    INVALID_TRANSACTION_ID = 4000006\n    \"\"\"\n    An error that indicates an invalid transaction identifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidtransactioniderror\n    \"\"\"\n\n    INVALID_ORIGINAL_TRANSACTION_ID = 4000008\n    \"\"\"\n    An error that indicates an invalid original transaction identifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidoriginaltransactioniderror\n    \"\"\"\n\n    INVALID_EXTEND_BY_DAYS = 4000009\n    \"\"\"\n    An error that indicates an invalid extend-by-days value.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidextendbydayserror\n    \"\"\"\n\n    INVALID_EXTEND_REASON_CODE = 4000010\n    \"\"\"\n    An error that indicates an invalid reason code.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidextendreasoncodeerror\n    \"\"\"\n\n    INVALID_REQUEST_IDENTIFIER = 4000011\n    \"\"\"\n    An error that indicates an invalid request identifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidrequestidentifiererror\n    \"\"\"\n\n    START_DATE_TOO_FAR_IN_PAST = 4000012\n    \"\"\"\n    An error that indicates that the start date is earlier than the earliest allowed date.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/startdatetoofarinpasterror\n    \"\"\"\n\n    START_DATE_AFTER_END_DATE = 4000013\n    \"\"\"\n    An error that indicates that the end date precedes the start date, or the two dates are equal.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/startdateafterenddateerror\n    \"\"\"\n\n    INVALID_PAGINATION_TOKEN = 4000014\n    \"\"\"\n    An error that indicates the pagination token is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidpaginationtokenerror\n    \"\"\"\n\n    INVALID_START_DATE = 4000015\n    \"\"\"\n    An error that indicates the start date is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidstartdateerror\n    \"\"\"\n\n    INVALID_END_DATE = 4000016\n    \"\"\"\n    An error that indicates the end date is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidenddateerror\n    \"\"\"\n    \n    PAGINATION_TOKEN_EXPIRED = 4000017\n    \"\"\"\n    An error that indicates the pagination token expired.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/paginationtokenexpirederror\n    \"\"\"\n\n    INVALID_NOTIFICATION_TYPE = 4000018\n    \"\"\"\n    An error that indicates the notification type or subtype is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidnotificationtypeerror\n    \"\"\"\n\n    MULTIPLE_FILTERS_SUPPLIED = 4000019\n    \"\"\"\n    An error that indicates the request is invalid because it has too many constraints applied.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/multiplefilterssuppliederror\n    \"\"\"\n\n    INVALID_TEST_NOTIFICATION_TOKEN = 4000020\n    \"\"\"\n    An error that indicates the test notification token is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidtestnotificationtokenerror\n    \"\"\"\n\n    INVALID_SORT = 4000021\n    \"\"\"\n    An error that indicates an invalid sort parameter.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidsorterror\n    \"\"\"\n\n    INVALID_PRODUCT_TYPE = 4000022\n    \"\"\"\n    An error that indicates an invalid product type parameter.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidproducttypeerror\n    \"\"\"\n\n    INVALID_PRODUCT_ID = 4000023\n    \"\"\"\n    An error that indicates the product ID parameter is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidproductiderror\n    \"\"\"\n\n    INVALID_SUBSCRIPTION_GROUP_IDENTIFIER = 4000024\n    \"\"\"\n    An error that indicates an invalid subscription group identifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidsubscriptiongroupidentifiererror\n    \"\"\"\n\n    INVALID_EXCLUDE_REVOKED = 4000025\n    \"\"\"\n    An error that indicates the query parameter exclude-revoked is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidexcluderevokederror\n\n    .. deprecated:: 1.5\n    \"\"\"\n\n    INVALID_IN_APP_OWNERSHIP_TYPE = 4000026\n    \"\"\"\n    An error that indicates an invalid in-app ownership type parameter.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidinappownershiptypeerror\n    \"\"\"\n\n    INVALID_EMPTY_STOREFRONT_COUNTRY_CODE_LIST = 4000027\n    \"\"\"\n    An error that indicates a required storefront country code is empty.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidemptystorefrontcountrycodelisterror\n    \"\"\"\n\n    INVALID_STOREFRONT_COUNTRY_CODE = 4000028\n    \"\"\"\n    An error that indicates a storefront code is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidstorefrontcountrycodeerror\n    \"\"\"\n\n    INVALID_REVOKED = 4000030\n    \"\"\"\n    An error that indicates the revoked parameter contains an invalid value.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidrevokederror\n    \"\"\"\n\n    INVALID_STATUS = 4000031\n    \"\"\"\n    An error that indicates the status parameter is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidstatuserror\n    \"\"\"\n\n    INVALID_ACCOUNT_TENURE = 4000032\n    \"\"\"\n    An error that indicates the value of the account tenure field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidaccounttenureerror\n    \"\"\"\n\n    INVALID_APP_ACCOUNT_TOKEN = 4000033\n    \"\"\"\n    An error that indicates the value of the app account token field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidappaccounttokenerror\n    \"\"\"\n\n    INVALID_CONSUMPTION_STATUS = 4000034\n    \"\"\"\n    An error that indicates the value of the consumption status field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidconsumptionstatuserror\n    \"\"\"\n\n    INVALID_CUSTOMER_CONSENTED = 4000035\n    \"\"\"\n    An error that indicates the customer consented field is invalid or doesn’t indicate that the customer consented.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidcustomerconsentederror\n    \"\"\"\n\n    INVALID_DELIVERY_STATUS = 4000036\n    \"\"\"\n    An error that indicates the value in the delivery status field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invaliddeliverystatuserror\n    \"\"\"\n\n    INVALID_LIFETIME_DOLLARS_PURCHASED = 4000037\n    \"\"\"\n    An error that indicates the value in the lifetime dollars purchased field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidlifetimedollarspurchasederror\n    \"\"\"\n\n    INVALID_LIFETIME_DOLLARS_REFUNDED = 4000038\n    \"\"\"\n    An error that indicates the value in the lifetime dollars refunded field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidlifetimedollarsrefundederror\n    \"\"\"\n\n    INVALID_PLATFORM = 4000039\n    \"\"\"\n    An error that indicates the value in the platform field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidplatformerror\n    \"\"\"\n\n    INVALID_PLAY_TIME = 4000040\n    \"\"\"\n    An error that indicates the value in the playtime field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidplaytimeerror\n    \"\"\"\n\n    INVALID_SAMPLE_CONTENT_PROVIDED = 4000041\n    \"\"\"\n    An error that indicates the value in the sample content provided field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidsamplecontentprovidederror\n    \"\"\"\n\n    INVALID_USER_STATUS = 4000042\n    \"\"\"\n    An error that indicates the value in the user status field is invalid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invaliduserstatuserror\n    \"\"\"\n\n    INVALID_TRANSACTION_NOT_CONSUMABLE = 4000043\n    \"\"\"\n    An error that indicates the transaction identifier doesn’t represent a consumable in-app purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidtransactionnotconsumableerror\n\n    .. deprecated:: 1.11\n    \"\"\"\n\n    INVALID_TRANSACTION_TYPE_NOT_SUPPORTED = 4000047\n    \"\"\"\n    An error that indicates the transaction identifier represents an unsupported in-app purchase type.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror\n    \"\"\"\n\n    APP_TRANSACTION_ID_NOT_SUPPORTED_ERROR = 4000048\n    \"\"\"\n    An error that indicates the endpoint doesn't support an app transaction ID.\n\n    https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror\n    \"\"\"\n\n    INVALID_IMAGE = 4000161\n    \"\"\"\n    An error that indicates the image that's uploading is invalid.\n\n    https://developer.apple.com/documentation/retentionmessaging/invalidimageerror\n    \"\"\"\n\n    HEADER_TOO_LONG = 4000162\n    \"\"\"\n    An error that indicates the header text is too long.\n\n    https://developer.apple.com/documentation/retentionmessaging/headertoolongerror\n    \"\"\"\n\n    BODY_TOO_LONG = 4000163\n    \"\"\"\n    An error that indicates the body text is too long.\n\n    https://developer.apple.com/documentation/retentionmessaging/bodytoolongerror\n    \"\"\"\n\n    INVALID_LOCALE = 4000164\n    \"\"\"\n    An error that indicates the locale is invalid.\n\n    https://developer.apple.com/documentation/retentionmessaging/invalidlocaleerror\n    \"\"\"\n\n    ALT_TEXT_TOO_LONG = 4000175\n    \"\"\"\n    An error that indicates the alternative text for an image is too long.\n\n    https://developer.apple.com/documentation/retentionmessaging/alttexttoolongerror\n    \"\"\"\n\n    INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR = 4000183\n    \"\"\"\n    An error that indicates the app account token value is not a valid UUID.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/invalidappaccounttokenuuiderror\n    \"\"\"\n\n    FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR = 4000185\n    \"\"\"\n    An error that indicates the transaction is for a product the customer obtains through Family Sharing, \n    which the endpoint doesn’t support.\n\n    https://developer.apple.com/documentation/appstoreserverapi/familytransactionnotsupportederror\n    \"\"\"\n\n    TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR = 4000187\n    \"\"\"\n    An error that indicates the endpoint expects an original transaction identifier.\n\n    https://developer.apple.com/documentation/appstoreserverapi/transactionidisnotoriginaltransactioniderror\n    \"\"\"\n\n    INVALID_PERFORMANCE_TEST_REQUEST = 4000211\n    \"\"\"\n    An error the API returns that indicates the performance test request is invalid.\n\n    https://developer.apple.com/documentation/retentionmessaging/invalidperformancetestrequesterror\n    \"\"\"\n\n    INVALID_REQUEST_ID = 4000212\n    \"\"\"\n    An error that indicates the request ID is invalid.\n\n    https://developer.apple.com/documentation/retentionmessaging/invalidrequestiderror\n    \"\"\"\n\n    EXISTING_PERFORMANCE_TEST_RUN = 4000213\n    \"\"\"\n    An error that indicates an error with an existing test.\n\n    https://developer.apple.com/documentation/retentionmessaging/existingperformancetestrunerror\n    \"\"\"\n\n    BAD_REQUEST_REALTIME_URL = 4000215\n    \"\"\"\n    An error that indicates the URL is invalid.\n\n    https://developer.apple.com/documentation/retentionmessaging/badrequestrealtimeurlerror\n    \"\"\"\n\n    BAD_REQUEST_IMAGE_SIZE = 4000216\n    \"\"\"\n    An error that indicates the image size provided is invalid.\n\n    https://developer.apple.com/documentation/retentionmessaging/badrequestimagesizeerror\n    \"\"\"\n\n    BAD_REQUEST_TOO_MANY_BULLET_POINTS = 4000218\n    \"\"\"\n    An error that indicates there are too many bullet points.\n\n    https://developer.apple.com/documentation/retentionmessaging/badrequesttoomanybulletpointserror\n    \"\"\"\n\n    BAD_REQUEST_BULLET_POINT_TEXT_TOO_LONG = 4000219\n    \"\"\"\n    An error that indicates the text for a bullet point is too long.\n\n    https://developer.apple.com/documentation/retentionmessaging/badrequestbulletpointtexttoolongerror\n    \"\"\"\n\n    BAD_REQUEST_ABOVE_IMAGE_REQUIRES_AN_IMAGE = 4000224\n    \"\"\"\n    An error that indicates that no image object is included, but the request indicates that the header should be placed above the image.\n\n    https://developer.apple.com/documentation/retentionmessaging/badrequestaboveimagerequiresanimageerror\n    \"\"\"\n\n    SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030004\n    \"\"\"\n    An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/subscriptionextensionineligibleerror\n    \"\"\"\n\n    SUBSCRIPTION_MAX_EXTENSION = 4030005\n    \"\"\"\n    An error that indicates the subscription doesn’t qualify for a renewal-date extension because it has already received the maximum extensions.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/subscriptionmaxextensionerror\n    \"\"\"\n\n    FAMILY_SHARED_SUBSCRIPTION_EXTENSION_INELIGIBLE = 4030007\n    \"\"\"\n    An error that indicates a subscription isn't directly eligible for a renewal date extension because the user obtained it through Family Sharing.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/familysharedsubscriptionextensionineligibleerror\n    \"\"\"\n\n    MAXIMUM_NUMBER_OF_IMAGES_REACHED = 4030014\n    \"\"\"\n    An error that indicates when you reach the maximum number of uploaded images.\n\n    https://developer.apple.com/documentation/retentionmessaging/maximumnumberofimagesreachederror\n    \"\"\"\n\n    MAXIMUM_NUMBER_OF_MESSAGES_REACHED = 4030016\n    \"\"\"\n    An error that indicates when you reach the maximum number of uploaded messages.\n\n    https://developer.apple.com/documentation/retentionmessaging/maximumnumberofmessagesreachederror\n    \"\"\"\n\n    MESSAGE_NOT_APPROVED = 4030017\n    \"\"\"\n    An error that indicates the message isn't in the approved state, so you can't configure it as a default message.\n\n    https://developer.apple.com/documentation/retentionmessaging/messagenotapprovederror\n    \"\"\"\n\n    IMAGE_NOT_APPROVED = 4030018\n    \"\"\"\n    An error that indicates the image isn't in the approved state, so you can't configure it as part of a default message.\n\n    https://developer.apple.com/documentation/retentionmessaging/imagenotapprovederror\n    \"\"\"\n\n    IMAGE_IN_USE = 4030019\n    \"\"\"\n    An error that indicates the image is currently in use as part of a message, so you can't delete it.\n\n    https://developer.apple.com/documentation/retentionmessaging/imageinuseerror\n    \"\"\"\n\n    FORBIDDEN_NO_PASSING_TEST = 4030026\n    \"\"\"\n    An error that indicates that passing a performance test is required before you can set a URL for the production environment.\n\n    https://developer.apple.com/documentation/retentionmessaging/forbiddennopassingtesterror\n    \"\"\"\n\n    ACCOUNT_NOT_FOUND = 4040001\n    \"\"\"\n    An error that indicates the App Store account wasn’t found.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/accountnotfounderror\n    \"\"\"\n\n    ACCOUNT_NOT_FOUND_RETRYABLE = 4040002\n    \"\"\"\n    An error response that indicates the App Store account wasn’t found, but you can try again.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/accountnotfoundretryableerror\n    \"\"\"\n\n    APP_NOT_FOUND = 4040003\n    \"\"\"\n    An error that indicates the app wasn’t found.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/appnotfounderror\n    \"\"\"\n\n    APP_NOT_FOUND_RETRYABLE = 4040004\n    \"\"\"\n    An error response that indicates the app wasn’t found, but you can try again.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/appnotfoundretryableerror\n    \"\"\"\n\n    ORIGINAL_TRANSACTION_ID_NOT_FOUND = 4040005\n    \"\"\"\n    An error that indicates an original transaction identifier wasn't found.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originaltransactionidnotfounderror\n    \"\"\"\n\n    ORIGINAL_TRANSACTION_ID_NOT_FOUND_RETRYABLE = 4040006\n    \"\"\"\n    An error response that indicates the original transaction identifier wasn’t found, but you can try again.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originaltransactionidnotfoundretryableerror\n    \"\"\"\n\n    SERVER_NOTIFICATION_URL_NOT_FOUND = 4040007\n    \"\"\"\n    An error that indicates that the App Store server couldn’t find a notifications URL for your app in this environment.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/servernotificationurlnotfounderror\n    \"\"\"\n\n    TEST_NOTIFICATION_NOT_FOUND = 4040008\n    \"\"\"\n    An error that indicates that the test notification token is expired or the test notification status isn’t available.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/testnotificationnotfounderror\n    \"\"\"\n\n    STATUS_REQUEST_NOT_FOUND = 4040009\n    \"\"\"\n    An error that indicates the server didn't find a subscription-renewal-date extension request for the request identifier and product identifier you provided.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/statusrequestnotfounderror\n    \"\"\"\n\n    TRANSACTION_ID_NOT_FOUND = 4040010\n    \"\"\"\n    An error that indicates a transaction identifier wasn't found.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/transactionidnotfounderror\n    \"\"\"\n\n    IMAGE_NOT_FOUND = 4040014\n    \"\"\"\n    An error that indicates the system can't find the image identifier.\n\n    https://developer.apple.com/documentation/retentionmessaging/imagenotfounderror\n    \"\"\"\n\n    MESSAGE_NOT_FOUND = 4040015\n    \"\"\"\n    An error that indicates the system can't find the message identifier.\n\n    https://developer.apple.com/documentation/retentionmessaging/messagenotfounderror\n    \"\"\"\n\n    PERFORMANCE_TEST_RUN_NOT_FOUND = 4040018\n    \"\"\"\n    An error the API returns if the service can't find the specified test run.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestrunnotfounderror\n    \"\"\"\n\n    APP_TRANSACTION_DOES_NOT_EXIST_ERROR = 4040019\n    \"\"\"\n    An error response that indicates an app transaction doesn’t exist for the specified customer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/apptransactiondoesnotexisterror\n    \"\"\"\n\n    DEFAULT_MESSAGE_NOT_FOUND = 4040020\n    \"\"\"\n    An error that indicates a default message isn’t configured.\n\n    https://developer.apple.com/documentation/retentionmessaging/defaultmessagenotfounderror\n    \"\"\"\n\n    REALTIME_URL_NOT_FOUND = 4040021\n    \"\"\"\n    An error that indicates that the URL for your endpoint isn’t configured.\n\n    https://developer.apple.com/documentation/retentionmessaging/realtimeurlnotfounderror\n    \"\"\"\n\n    IMAGE_ALREADY_EXISTS = 4090000\n    \"\"\"\n    An error that indicates the image identifier already exists.\n\n    https://developer.apple.com/documentation/retentionmessaging/imagealreadyexistserror\n    \"\"\"\n\n    MESSAGE_ALREADY_EXISTS = 4090001\n    \"\"\"\n    An error that indicates the message identifier already exists.\n\n    https://developer.apple.com/documentation/retentionmessaging/messagealreadyexistserror\n    \"\"\"\n\n    RATE_LIMIT_EXCEEDED = 4290000\n    \"\"\"\n    An error that indicates that the request exceeded the rate limit.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/ratelimitexceedederror\n    \"\"\"\n\n    GENERAL_INTERNAL = 5000000\n    \"\"\"\n    An error that indicates a general internal error.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/generalinternalerror\n    \"\"\"\n\n    GENERAL_INTERNAL_RETRYABLE = 5000001\n    \"\"\"\n    An error response that indicates an unknown error occurred, but you can try again.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/generalinternalretryableerror\n    \"\"\"\n\n\n@define\nclass APIException(Exception):\n    http_status_code: int\n    api_error: Optional[APIError]\n    raw_api_error: Optional[int]\n    error_message: Optional[str]\n\n    def __init__(self, http_status_code: int, raw_api_error: Optional[int] = None, error_message: Optional[str] = None):\n        self.http_status_code = http_status_code\n        self.raw_api_error = raw_api_error\n        self.api_error = None\n        self.error_message = error_message\n        try:\n            if raw_api_error is not None:\n                self.api_error = APIError(raw_api_error)\n        except ValueError:\n            pass\n\nclass GetTransactionHistoryVersion(str, Enum):\n    V1 = \"v1\"\n    \"\"\"\n    .. deprecated:: 1.3.0\n    \"\"\"\n\n    V2 = \"v2\"\n\nclass BaseAppStoreServerAPIClient:\n    def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment):\n        if environment == Environment.XCODE:\n            raise ValueError(\"Xcode is not a supported environment for an AppStoreServerAPIClient\")\n        if environment == Environment.PRODUCTION:\n            self._base_url = \"https://api.storekit.itunes.apple.com\"\n        elif environment == Environment.LOCAL_TESTING:\n            self._base_url = \"https://local-testing-base-url\"\n        elif environment == Environment.SANDBOX:\n            self._base_url = \"https://api.storekit-sandbox.itunes.apple.com\"\n        else:\n            raise ValueError(\"Invalid environment provided\")\n        self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend())\n        self._key_id = key_id\n        self._issuer_id = issuer_id\n        self._bundle_id = bundle_id\n\n    def _generate_token(self) -> str:\n        future_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=5)\n        return jwt.encode(\n            {\n                \"bid\": self._bundle_id,\n                \"iss\": self._issuer_id,\n                \"aud\": \"appstoreconnect-v1\",\n                \"exp\": calendar.timegm(future_time.timetuple()),\n            },\n            self._signing_key,\n            algorithm=\"ES256\",\n            headers={\"kid\": self._key_id},\n        )\n    \n    def _get_full_url(self, path) -> str:\n        return self._base_url + path\n    \n    def _get_headers(self) -> Dict[str, str]:\n        return {\n            'User-Agent': \"app-store-server-library/python/3.0.0\",\n            'Authorization': f'Bearer {self._generate_token()}',\n            'Accept': 'application/json'\n        }\n    \n    def _get_request_json(self, body) -> Dict[str, Any]:\n        c = _get_cattrs_converter(type(body)) if body is not None else None\n        return c.unstructure(body) if body is not None else None\n\n    def _parse_response(self, status_code: int, headers: MutableMapping, json_supplier, destination_class: Type[T]) -> T:\n        if 200 <= status_code < 300:\n            if destination_class is None:\n                return\n            c = _get_cattrs_converter(destination_class)\n            response_body = json_supplier()\n            return c.structure(response_body, destination_class)\n        else:\n            # Best effort parsing of the response body\n            if not 'content-type' in headers or headers['content-type'] != 'application/json':\n                raise APIException(status_code)\n            try:\n                response_body = json_supplier()\n                raise APIException(status_code, response_body['errorCode'], response_body['errorMessage'])\n            except APIException as e:\n                raise e\n            except Exception as e:\n                raise APIException(status_code) from e\n\n\nclass AppStoreServerAPIClient(BaseAppStoreServerAPIClient):\n    def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment):\n        super().__init__(signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id, environment=environment)\n    \n    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:\n        url = self._get_full_url(path)\n        headers = self._get_headers()\n\n        if isinstance(body, bytes):\n            if content_type:\n                headers['Content-Type'] = content_type\n            response = self._execute_request(method, url, queryParameters, headers, None, body)\n        else:\n            json = self._get_request_json(body)\n            response = self._execute_request(method, url, queryParameters, headers, json, None)\n\n        return self._parse_response(response.status_code, response.headers, lambda: response.json(), destination_class)\n\n    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:\n        return requests.request(method, url, params=params, headers=headers, json=json, data=data, timeout=30)\n\n    def extend_renewal_date_for_all_active_subscribers(self, mass_extend_renewal_date_request: MassExtendRenewalDateRequest) -> MassExtendRenewalDateResponse: \n        \"\"\"\n        Uses a subscription's product identifier to extend the renewal date for all of its eligible active subscribers.\n        https://developer.apple.com/documentation/appstoreserverapi/extend_subscription_renewal_dates_for_all_active_subscribers\n        \n        :param mass_extend_renewal_date_request: The request body for extending a subscription renewal date for all of its active subscribers.\n        :return: A response that indicates the server successfully received the subscription-renewal-date extension request.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return self._make_request(\"/inApps/v1/subscriptions/extend/mass\", \"POST\", {}, mass_extend_renewal_date_request, MassExtendRenewalDateResponse, None)\n\n    def extend_subscription_renewal_date(self, original_transaction_id: str, extend_renewal_date_request: ExtendRenewalDateRequest) -> ExtendRenewalDateResponse:\n        \"\"\"\n        Extends the renewal date of a customer's active subscription using the original transaction identifier.\n        https://developer.apple.com/documentation/appstoreserverapi/extend_a_subscription_renewal_date\n        \n        :param original_transaction_id:    The original transaction identifier of the subscription receiving a renewal date extension.\n        :param extend_renewal_date_request: The request body containing subscription-renewal-extension data.\n        :return: A response that indicates whether an individual renewal-date extension succeeded, and related details.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return self._make_request(f\"/inApps/v1/subscriptions/extend/{original_transaction_id}\", \"PUT\", {}, extend_renewal_date_request, ExtendRenewalDateResponse, None)\n    \n    def get_all_subscription_statuses(self, transaction_id: str, status: Optional[List[Status]] = None) -> StatusResponse:\n        \"\"\"\n        Get the statuses for all of a customer's auto-renewable subscriptions in your app.\n        https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses\n        \n        :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.\n        :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.\n        :return: A response that contains status information for all of a customer's auto-renewable subscriptions in your app.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        queryParameters: Dict[str, List[str]] = dict()\n        if status is not None:\n            queryParameters[\"status\"] = [s.value for s in status]\n        \n        return self._make_request(f\"/inApps/v1/subscriptions/{transaction_id}\", \"GET\", queryParameters, None, StatusResponse, None)\n    \n    def get_refund_history(self, transaction_id: str, revision: Optional[str]) -> RefundHistoryResponse:\n        \"\"\"\n        Get a paginated list of all of a customer's refunded in-app purchases for your app.\n        https://developer.apple.com/documentation/appstoreserverapi/get_refund_history\n\n        :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.\n        :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.\n        :return: A response that contains status information for all of a customer's auto-renewable subscriptions in your app.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n\n        queryParameters: Dict[str, List[str]] = dict()\n        if revision is not None:\n            queryParameters[\"revision\"] = [revision]\n        \n        return self._make_request(f\"/inApps/v2/refund/lookup/{transaction_id}\", \"GET\", queryParameters, None, RefundHistoryResponse, None)\n    \n    def get_status_of_subscription_renewal_date_extensions(self, request_identifier: str, product_id: str) -> MassExtendRenewalDateStatusResponse:\n        \"\"\"\n        Checks whether a renewal date extension request completed, and provides the final count of successful or failed extensions.\n        https://developer.apple.com/documentation/appstoreserverapi/get_status_of_subscription_renewal_date_extensions\n\n        :param request_identifier: The UUID that represents your request to the Extend Subscription Renewal Dates for All Active Subscribers endpoint.\n        :param product_id: The product identifier of the auto-renewable subscription that you request a renewal-date extension for.\n        :return: A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return self._make_request(f\"/inApps/v1/subscriptions/extend/mass/{product_id}/{request_identifier}\", \"GET\", {}, None, MassExtendRenewalDateStatusResponse, None)\n    \n    def get_test_notification_status(self, test_notification_token: str) -> CheckTestNotificationResponse:\n        \"\"\"\n        Check the status of the test App Store server notification sent to your server.\n        https://developer.apple.com/documentation/appstoreserverapi/get_test_notification_status\n\n        :param test_notification_token: The test notification token received from the Request a Test Notification endpoint\n        :return: A response that contains the contents of the test notification sent by the App Store server and the result from your server.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return self._make_request(f\"/inApps/v1/notifications/test/{test_notification_token}\", \"GET\", {}, None, CheckTestNotificationResponse, None)\n    \n    def get_notification_history(self, pagination_token: Optional[str], notification_history_request: NotificationHistoryRequest) -> NotificationHistoryResponse:\n        \"\"\"\n        Get a list of notifications that the App Store server attempted to send to your server.\n        https://developer.apple.com/documentation/appstoreserverapi/get_notification_history\n\n        :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.\n        :param notification_history_request: The request body that includes the start and end dates, and optional query constraints.\n        :return: A response that contains the App Store Server Notifications history for your app.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        queryParameters: Dict[str, List[str]] = dict()\n        if pagination_token is not None:\n            queryParameters[\"paginationToken\"] = [pagination_token]\n        \n        return self._make_request(\"/inApps/v1/notifications/history\", \"POST\", queryParameters, notification_history_request, NotificationHistoryResponse, None)\n\n    def get_transaction_history(self, transaction_id: str, revision: Optional[str], transaction_history_request: TransactionHistoryRequest, version: GetTransactionHistoryVersion = GetTransactionHistoryVersion.V1) -> HistoryResponse:\n        \"\"\"\n        Get a customer's in-app purchase transaction history for your app.\n        https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history\n\n        :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.\n        :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.\n        :param transaction_history_request: The request parameters that includes the startDate,endDate,productIds,productTypes and optional query constraints.\n        :param version: The version of the Get Transaction History endpoint to use. V2 is recommended.\n        :return: A response that contains the customer's transaction history for an app.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        queryParameters: Dict[str, List[str]] = dict()\n        if revision is not None:\n            queryParameters[\"revision\"] = [revision]\n        \n        if transaction_history_request.startDate is not None:\n            queryParameters[\"startDate\"] = [str(transaction_history_request.startDate)]\n        \n        if transaction_history_request.endDate is not None:\n            queryParameters[\"endDate\"] = [str(transaction_history_request.endDate)]\n        \n        if transaction_history_request.productIds is not None:\n            queryParameters[\"productId\"] = transaction_history_request.productIds\n        \n        if transaction_history_request.productTypes is not None:\n            queryParameters[\"productType\"] = [product_type.value for product_type in transaction_history_request.productTypes]\n        \n        if transaction_history_request.sort is not None:\n            queryParameters[\"sort\"] = [transaction_history_request.sort.value]\n        \n        if transaction_history_request.subscriptionGroupIdentifiers is not None:\n            queryParameters[\"subscriptionGroupIdentifier\"] = transaction_history_request.subscriptionGroupIdentifiers\n        \n        if transaction_history_request.inAppOwnershipType is not None:\n            queryParameters[\"inAppOwnershipType\"] = [transaction_history_request.inAppOwnershipType.value]\n        \n        if transaction_history_request.revoked is not None:\n            queryParameters[\"revoked\"] = [str(transaction_history_request.revoked)]\n        \n        return self._make_request(\"/inApps/{}/history/{}\".format(version.value, transaction_id), \"GET\", queryParameters, None, HistoryResponse, None)\n    \n    def get_transaction_info(self, transaction_id: str) -> TransactionInfoResponse:\n        \"\"\"\n        Get information about a single transaction for your app.\n        https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info\n        \n        :param transaction_id The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.\n        :return: A response that contains signed transaction information for a single transaction.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return self._make_request(f\"/inApps/v1/transactions/{transaction_id}\", \"GET\", {}, None, TransactionInfoResponse, None)\n\n    def look_up_order_id(self, order_id: str) -> OrderLookupResponse:\n        \"\"\"\n        Get a customer's in-app purchases from a receipt using the order ID.\n        https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id\n        \n        :param order_id: The order ID for in-app purchases that belong to the customer.\n        :return: A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return self._make_request(f\"/inApps/v1/lookup/{order_id}\", \"GET\", {}, None, OrderLookupResponse, None)\n    \n    def request_test_notification(self) -> SendTestNotificationResponse:\n        \"\"\"\n        Ask App Store Server Notifications to send a test notification to your server.\n        https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification\n\n        :return: A response that contains the test notification token.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return self._make_request(\"/inApps/v1/notifications/test\", \"POST\", {}, None, SendTestNotificationResponse, None)\n\n    def send_consumption_data(self, transaction_id: str, consumption_request: ConsumptionRequestV1):\n        \"\"\"\n        Send consumption information about a consumable in-app purchase to the App Store after your server receives a consumption request notification.\n        https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1\n\n        .. deprecated::\n            Use :func:`send_consumption_information` instead.\n\n        :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.\n        :param consumption_request:    The request body containing consumption information.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        warnings.warn(\"send_consumption_data is deprecated, use send_consumption_information instead\", DeprecationWarning, stacklevel=2)\n        self._make_request(f\"/inApps/v1/transactions/consumption/{transaction_id}\", \"PUT\", {}, consumption_request, None, None)\n\n    def send_consumption_information(self, transaction_id: str, consumption_request: ConsumptionRequest):\n        \"\"\"\n        Send consumption information about an In-App Purchase to the App Store after your server receives a consumption request notification.\n        https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information\n\n        :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.\n        :param consumption_request:    The request body containing consumption information.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        self._make_request(f\"/inApps/v2/transactions/consumption/{transaction_id}\", \"PUT\", {}, consumption_request, None, None)\n\n    def set_app_account_token(self, original_transaction_id: str, update_app_account_token_request: UpdateAppAccountTokenRequest):\n        \"\"\"\n        Sets the app account token value for a purchase the customer makes outside your app, or updates its value in an existing transaction.\n        https://developer.apple.com/documentation/appstoreserverapi/set-app-account-token\n\n        :param original_transaction_id The original transaction identifier of the transaction to receive the app account token update.\n        :param update_app_account_token_request The request body that contains a valid app account token value.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        self._make_request(f\"/inApps/v1/transactions/{original_transaction_id}/appAccountToken\", \"PUT\", {}, update_app_account_token_request, None, None)\n\n    def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optional[ImageSize] = None):\n        \"\"\"\n        Upload an image to use for retention messaging.\n\n        :param image_identifier: A UUID you provide to uniquely identify the image you upload.\n        :param image: The image file to upload.\n        :param image_size: The size of the image you upload.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/upload-image\n        \"\"\"\n        query_parameters = {}\n        if image_size is not None:\n            query_parameters[\"imageSize\"] = [image_size.value]\n        self._make_request(f\"/inApps/v1/messaging/image/{image_identifier}\", \"PUT\", query_parameters, image, None, \"image/png\")\n\n    def delete_image(self, image_identifier: UUID):\n        \"\"\"\n        Delete a previously uploaded image.\n\n        :param image_identifier: The identifier of the image to delete.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/delete-image\n        \"\"\"\n        self._make_request(f\"/inApps/v1/messaging/image/{image_identifier}\", \"DELETE\", {}, None, None, None)\n\n    def get_image_list(self) -> GetImageListResponse:\n        \"\"\"\n        Get the image identifier and state for all uploaded images.\n\n        :return: A response that contains status information for all images.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-image-list\n        \"\"\"\n        return self._make_request(\"/inApps/v1/messaging/image/list\", \"GET\", {}, None, GetImageListResponse, None)\n\n    def upload_message(self, message_identifier: UUID, upload_message_request_body: UploadMessageRequestBody):\n        \"\"\"\n        Upload a message to use for retention messaging.\n\n        :param message_identifier: A UUID you provide to uniquely identify the message you upload.\n        :param upload_message_request_body: The message text to upload.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/upload-message\n        \"\"\"\n        self._make_request(f\"/inApps/v1/messaging/message/{message_identifier}\", \"PUT\", {}, upload_message_request_body, None, None)\n\n    def delete_message(self, message_identifier: UUID):\n        \"\"\"\n        Delete a previously uploaded message.\n\n        :param message_identifier: The identifier of the message to delete.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/delete-message\n        \"\"\"\n        self._make_request(f\"/inApps/v1/messaging/message/{message_identifier}\", \"DELETE\", {}, None, None, None)\n\n    def get_message_list(self) -> GetMessageListResponse:\n        \"\"\"\n        Get the message identifier and state of all uploaded messages.\n\n        :return: A response that contains status information for all messages.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-message-list\n        \"\"\"\n        return self._make_request(\"/inApps/v1/messaging/message/list\", \"GET\", {}, None, GetMessageListResponse, None)\n\n    def configure_default_message(self, product_id: str, locale: str, default_configuration_request: DefaultConfigurationRequest):\n        \"\"\"\n        Configure a default message for a specific product in a specific locale.\n\n        :param product_id: The product identifier for the default configuration.\n        :param locale: The locale for the default configuration.\n        :param default_configuration_request: The request body that includes the message identifier to configure as the default message.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/configure-default-message\n        \"\"\"\n        self._make_request(f\"/inApps/v1/messaging/default/{product_id}/{locale}\", \"PUT\", {}, default_configuration_request, None, None)\n\n    def delete_default_message(self, product_id: str, locale: str):\n        \"\"\"\n        Delete a default message for a product in a locale.\n\n        :param product_id: The product ID of the default message configuration.\n        :param locale: The locale of the default message configuration.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/delete-default-message\n        \"\"\"\n        self._make_request(f\"/inApps/v1/messaging/default/{product_id}/{locale}\", \"DELETE\", {}, None, None, None)\n\n    def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurationResponse:\n        \"\"\"\n        Gets the default message for a specific product in a specific locale, if it’s configured.\n\n        :param product_id: The product identifier of the message.\n        :param locale: The locale of the message.\n        :return: The response body that contains the default configuration information.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-default-message\n        \"\"\"\n        return self._make_request(f\"/inApps/v1/messaging/default/{product_id}/{locale}\", \"GET\", {}, None, DefaultConfigurationResponse, None)\n\n    def configure_realtime_url(self, realtime_url_request: RealtimeUrlRequest):\n        \"\"\"\n        Configures the URL for your Get Retention Message endpoint in the sandbox and production environments.\n\n        :param realtime_url_request: The request body that includes your endpoint’s URL.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/configure-realtime-url\n        \"\"\"\n        self._make_request(\"/inApps/v1/messaging/realtime/url\", \"PUT\", {}, realtime_url_request, None, None)\n\n    def delete_realtime_url(self):\n        \"\"\"\n        Deletes the URL for your Get Retention Message endpoint, in the sandbox or production environments.\n\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/delete-realtime-url\n        \"\"\"\n        self._make_request(\"/inApps/v1/messaging/realtime/url\", \"DELETE\", {}, None, None, None)\n\n    def get_realtime_url(self) -> RealtimeUrlResponse:\n        \"\"\"\n        Gets the URL for real-time messages that points to your Get Retention Message endpoint, which you previously configured.\n\n        :return: The response body that contains the URL for your Get Retention Message endpoint.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-realtime-url\n        \"\"\"\n        return self._make_request(\"/inApps/v1/messaging/realtime/url\", \"GET\", {}, None, RealtimeUrlResponse, None)\n\n    def initiate_performance_test(self, performance_test_request: PerformanceTestRequest) -> PerformanceTestResponse:\n        \"\"\"\n        Initiates a performance test of your Get Retention Message endpoint in the sandbox environment.\n\n        :param performance_test_request: The request body which specifies a transaction identifier of an In-App Purchase to use for this test.\n        :return: The performance test response object.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/initiate-performance-test\n        \"\"\"\n        return self._make_request(\"/inApps/v1/messaging/performanceTest\", \"POST\", {}, performance_test_request, PerformanceTestResponse, None)\n\n    def get_performance_test_results(self, request_id: str) -> PerformanceTestResultResponse:\n        \"\"\"\n        Gets the results of the performance test for the specified identifier.\n\n        :param request_id: The ID of the performance test to return, which you receive in the PerformanceTestResponse when you call Initiate Performance Test.\n        :return: An object the API returns that describes the performance test results.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-performance-test-results\n        \"\"\"\n        return self._make_request(f\"/inApps/v1/messaging/performanceTest/result/{request_id}\", \"GET\", {}, None, PerformanceTestResultResponse, None)\n\n    def get_app_transaction_info(self, transaction_id: str) -> AppTransactionInfoResponse:\n        \"\"\"\n        Get a customer's app transaction information for your app.\n        \n        :param transaction_id Any originalTransactionId, transactionId or appTransactionId that belongs to the customer for your app.\n        :return: A response that contains signed app transaction information for a customer.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/appstoreserverapi/get-app-transaction-info\n        \"\"\"\n        return self._make_request(f\"/inApps/v1/transactions/appTransactions/{transaction_id}\", \"GET\", {}, None, AppTransactionInfoResponse, None)\n\nclass AsyncAppStoreServerAPIClient(BaseAppStoreServerAPIClient):\n    def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str, environment: Environment):\n        super().__init__(signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id, environment=environment)\n        try:\n            import httpx\n            self.http_client = httpx.AsyncClient()\n        except:\n            raise ModuleNotFoundError(\"httpx not found but attempting to instantiate an async client\")\n\n    async def async_close(self):\n        await self.http_client.aclose()\n    \n    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:\n        url = self._get_full_url(path)\n        headers = self._get_headers()\n\n        if isinstance(body, bytes):\n            # For binary data like images\n            if content_type:\n                headers['Content-Type'] = content_type\n            response = await self._execute_request(method, url, queryParameters, headers, None, body)\n        else:\n            # For JSON data\n            json = self._get_request_json(body)\n            response = await self._execute_request(method, url, queryParameters, headers, json, None)\n\n        return self._parse_response(response.status_code, response.headers, lambda: response.json(), destination_class)\n\n    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]):\n        return await self.http_client.request(method, url, params=params, headers=headers, json=json, data=data, timeout=30)\n\n    async def extend_renewal_date_for_all_active_subscribers(self, mass_extend_renewal_date_request: MassExtendRenewalDateRequest) -> MassExtendRenewalDateResponse: \n        \"\"\"\n        Uses a subscription's product identifier to extend the renewal date for all of its eligible active subscribers.\n        https://developer.apple.com/documentation/appstoreserverapi/extend_subscription_renewal_dates_for_all_active_subscribers\n        \n        :param mass_extend_renewal_date_request: The request body for extending a subscription renewal date for all of its active subscribers.\n        :return: A response that indicates the server successfully received the subscription-renewal-date extension request.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return await self._make_request(\"/inApps/v1/subscriptions/extend/mass\", \"POST\", {}, mass_extend_renewal_date_request, MassExtendRenewalDateResponse, None)\n\n    async def extend_subscription_renewal_date(self, original_transaction_id: str, extend_renewal_date_request: ExtendRenewalDateRequest) -> ExtendRenewalDateResponse:\n        \"\"\"\n        Extends the renewal date of a customer's active subscription using the original transaction identifier.\n        https://developer.apple.com/documentation/appstoreserverapi/extend_a_subscription_renewal_date\n        \n        :param original_transaction_id:    The original transaction identifier of the subscription receiving a renewal date extension.\n        :param extend_renewal_date_request: The request body containing subscription-renewal-extension data.\n        :return: A response that indicates whether an individual renewal-date extension succeeded, and related details.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return await self._make_request(f\"/inApps/v1/subscriptions/extend/{original_transaction_id}\", \"PUT\", {}, extend_renewal_date_request, ExtendRenewalDateResponse, None)\n    \n    async def get_all_subscription_statuses(self, transaction_id: str, status: Optional[List[Status]] = None) -> StatusResponse:\n        \"\"\"\n        Get the statuses for all of a customer's auto-renewable subscriptions in your app.\n        https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses\n        \n        :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.\n        :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.\n        :return: A response that contains status information for all of a customer's auto-renewable subscriptions in your app.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        queryParameters: Dict[str, List[str]] = dict()\n        if status is not None:\n            queryParameters[\"status\"] = [s.value for s in status]\n        \n        return await self._make_request(f\"/inApps/v1/subscriptions/{transaction_id}\", \"GET\", queryParameters, None, StatusResponse, None)\n    \n    async def get_refund_history(self, transaction_id: str, revision: Optional[str]) -> RefundHistoryResponse:\n        \"\"\"\n        Get a paginated list of all of a customer's refunded in-app purchases for your app.\n        https://developer.apple.com/documentation/appstoreserverapi/get_refund_history\n\n        :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.\n        :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.\n        :return: A response that contains status information for all of a customer's auto-renewable subscriptions in your app.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n\n        queryParameters: Dict[str, List[str]] = dict()\n        if revision is not None:\n            queryParameters[\"revision\"] = [revision]\n        \n        return await self._make_request(f\"/inApps/v2/refund/lookup/{transaction_id}\", \"GET\", queryParameters, None, RefundHistoryResponse, None)\n    \n    async def get_status_of_subscription_renewal_date_extensions(self, request_identifier: str, product_id: str) -> MassExtendRenewalDateStatusResponse:\n        \"\"\"\n        Checks whether a renewal date extension request completed, and provides the final count of successful or failed extensions.\n        https://developer.apple.com/documentation/appstoreserverapi/get_status_of_subscription_renewal_date_extensions\n\n        :param request_identifier: The UUID that represents your request to the Extend Subscription Renewal Dates for All Active Subscribers endpoint.\n        :param product_id: The product identifier of the auto-renewable subscription that you request a renewal-date extension for.\n        :return: A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return await self._make_request(f\"/inApps/v1/subscriptions/extend/mass/{product_id}/{request_identifier}\", \"GET\", {}, None, MassExtendRenewalDateStatusResponse, None)\n    \n    async def get_test_notification_status(self, test_notification_token: str) -> CheckTestNotificationResponse:\n        \"\"\"\n        Check the status of the test App Store server notification sent to your server.\n        https://developer.apple.com/documentation/appstoreserverapi/get_test_notification_status\n\n        :param test_notification_token: The test notification token received from the Request a Test Notification endpoint\n        :return: A response that contains the contents of the test notification sent by the App Store server and the result from your server.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return await self._make_request(f\"/inApps/v1/notifications/test/{test_notification_token}\", \"GET\", {}, None, CheckTestNotificationResponse, None)\n    \n    async def get_notification_history(self, pagination_token: Optional[str], notification_history_request: NotificationHistoryRequest) -> NotificationHistoryResponse:\n        \"\"\"\n        Get a list of notifications that the App Store server attempted to send to your server.\n        https://developer.apple.com/documentation/appstoreserverapi/get_notification_history\n\n        :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.\n        :param notification_history_request: The request body that includes the start and end dates, and optional query constraints.\n        :return: A response that contains the App Store Server Notifications history for your app.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        queryParameters: Dict[str, List[str]] = dict()\n        if pagination_token is not None:\n            queryParameters[\"paginationToken\"] = [pagination_token]\n        \n        return await self._make_request(\"/inApps/v1/notifications/history\", \"POST\", queryParameters, notification_history_request, NotificationHistoryResponse, None)\n\n    async def get_transaction_history(self, transaction_id: str, revision: Optional[str], transaction_history_request: TransactionHistoryRequest, version: GetTransactionHistoryVersion = GetTransactionHistoryVersion.V1) -> HistoryResponse:\n        \"\"\"\n        Get a customer's in-app purchase transaction history for your app.\n        https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history\n\n        :param transaction_id: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.\n        :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.\n        :param transaction_history_request: The request parameters that includes the startDate,endDate,productIds,productTypes and optional query constraints.\n        :param version: The version of the Get Transaction History endpoint to use. V2 is recommended.\n        :return: A response that contains the customer's transaction history for an app.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        queryParameters: Dict[str, List[str]] = dict()\n        if revision is not None:\n            queryParameters[\"revision\"] = [revision]\n        \n        if transaction_history_request.startDate is not None:\n            queryParameters[\"startDate\"] = [str(transaction_history_request.startDate)]\n        \n        if transaction_history_request.endDate is not None:\n            queryParameters[\"endDate\"] = [str(transaction_history_request.endDate)]\n        \n        if transaction_history_request.productIds is not None:\n            queryParameters[\"productId\"] = transaction_history_request.productIds\n        \n        if transaction_history_request.productTypes is not None:\n            queryParameters[\"productType\"] = [product_type.value for product_type in transaction_history_request.productTypes]\n        \n        if transaction_history_request.sort is not None:\n            queryParameters[\"sort\"] = [transaction_history_request.sort.value]\n        \n        if transaction_history_request.subscriptionGroupIdentifiers is not None:\n            queryParameters[\"subscriptionGroupIdentifier\"] = transaction_history_request.subscriptionGroupIdentifiers\n        \n        if transaction_history_request.inAppOwnershipType is not None:\n            queryParameters[\"inAppOwnershipType\"] = [transaction_history_request.inAppOwnershipType.value]\n        \n        if transaction_history_request.revoked is not None:\n            queryParameters[\"revoked\"] = [str(transaction_history_request.revoked)]\n        \n        return await self._make_request(\"/inApps/\" + version + \"/history/\" + transaction_id, \"GET\", queryParameters, None, HistoryResponse, None)\n    \n    async def get_transaction_info(self, transaction_id: str) -> TransactionInfoResponse:\n        \"\"\"\n        Get information about a single transaction for your app.\n        https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info\n        \n        :param transaction_id The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.\n        :return: A response that contains signed transaction information for a single transaction.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return await self._make_request(f\"/inApps/v1/transactions/{transaction_id}\", \"GET\", {}, None, TransactionInfoResponse, None)\n\n    async def look_up_order_id(self, order_id: str) -> OrderLookupResponse:\n        \"\"\"\n        Get a customer's in-app purchases from a receipt using the order ID.\n        https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id\n        \n        :param order_id: The order ID for in-app purchases that belong to the customer.\n        :return: A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return await self._make_request(f\"/inApps/v1/lookup/{order_id}\", \"GET\", {}, None, OrderLookupResponse, None)\n    async def request_test_notification(self) -> SendTestNotificationResponse:\n        \"\"\"\n        Ask App Store Server Notifications to send a test notification to your server.\n        https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification\n\n        :return: A response that contains the test notification token.\n        :throws APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        return await self._make_request(\"/inApps/v1/notifications/test\", \"POST\", {}, None, SendTestNotificationResponse, None)\n\n    async def send_consumption_data(self, transaction_id: str, consumption_request: ConsumptionRequestV1):\n        \"\"\"\n        Send consumption information about a consumable in-app purchase to the App Store after your server receives a consumption request notification.\n        https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1\n\n        .. deprecated::\n            Use :func:`send_consumption_information` instead.\n\n        :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.\n        :param consumption_request:    The request body containing consumption information.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        warnings.warn(\"send_consumption_data is deprecated, use send_consumption_information instead\", DeprecationWarning, stacklevel=2)\n        await self._make_request(f\"/inApps/v1/transactions/consumption/{transaction_id}\", \"PUT\", {}, consumption_request, None, None)\n\n    async def send_consumption_information(self, transaction_id: str, consumption_request: ConsumptionRequest):\n        \"\"\"\n        Send consumption information about an In-App Purchase to the App Store after your server receives a consumption request notification.\n        https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information\n\n        :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.\n        :param consumption_request:    The request body containing consumption information.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        await self._make_request(f\"/inApps/v2/transactions/consumption/{transaction_id}\", \"PUT\", {}, consumption_request, None, None)\n\n    async def set_app_account_token(self, original_transaction_id: str, update_app_account_token_request: UpdateAppAccountTokenRequest):\n        \"\"\"\n        Sets the app account token value for a purchase the customer makes outside your app, or updates its value in an existing transaction.\n        https://developer.apple.com/documentation/appstoreserverapi/set-app-account-token\n\n        :param original_transaction_id The original transaction identifier of the transaction to receive the app account token update.\n        :param update_app_account_token_request The request body that contains a valid app account token value.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        \"\"\"\n        await self._make_request(f\"/inApps/v1/transactions/{original_transaction_id}/appAccountToken\", \"PUT\", {}, update_app_account_token_request, None, None)\n\n    async def upload_image(self, image_identifier: UUID, image: bytes, image_size: Optional[ImageSize] = None):\n        \"\"\"\n        Upload an image to use for retention messaging.\n\n        :param image_identifier: A UUID you provide to uniquely identify the image you upload.\n        :param image: The image file to upload.\n        :param image_size: The optional size of the image.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/upload-image\n        \"\"\"\n        query_parameters = {}\n        if image_size is not None:\n            query_parameters[\"imageSize\"] = [image_size.value]\n        await self._make_request(f\"/inApps/v1/messaging/image/{image_identifier}\", \"PUT\", query_parameters, image, None, \"image/png\")\n\n    async def delete_image(self, image_identifier: UUID):\n        \"\"\"\n        Delete a previously uploaded image.\n\n        :param image_identifier: The identifier of the image to delete.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/delete-image\n        \"\"\"\n        await self._make_request(f\"/inApps/v1/messaging/image/{image_identifier}\", \"DELETE\", {}, None, None, None)\n\n    async def get_image_list(self) -> GetImageListResponse:\n        \"\"\"\n        Get the image identifier and state for all uploaded images.\n\n        :return: A response that contains status information for all images.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-image-list\n        \"\"\"\n        return await self._make_request(\"/inApps/v1/messaging/image/list\", \"GET\", {}, None, GetImageListResponse, None)\n\n    async def upload_message(self, message_identifier: UUID, upload_message_request_body: UploadMessageRequestBody):\n        \"\"\"\n        Upload a message to use for retention messaging.\n\n        :param message_identifier: A UUID you provide to uniquely identify the message you upload.\n        :param upload_message_request_body: The message text to upload.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/upload-message\n        \"\"\"\n        await self._make_request(f\"/inApps/v1/messaging/message/{message_identifier}\", \"PUT\", {}, upload_message_request_body, None, None)\n\n    async def delete_message(self, message_identifier: UUID):\n        \"\"\"\n        Delete a previously uploaded message.\n\n        :param message_identifier: The identifier of the message to delete.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/delete-message\n        \"\"\"\n        await self._make_request(f\"/inApps/v1/messaging/message/{message_identifier}\", \"DELETE\", {}, None, None, None)\n\n    async def get_message_list(self) -> GetMessageListResponse:\n        \"\"\"\n        Get the message identifier and state of all uploaded messages.\n\n        :return: A response that contains status information for all messages.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-message-list\n        \"\"\"\n        return await self._make_request(\"/inApps/v1/messaging/message/list\", \"GET\", {}, None, GetMessageListResponse, None)\n\n    async def configure_default_message(self, product_id: str, locale: str, default_configuration_request: DefaultConfigurationRequest):\n        \"\"\"\n        Configure a default message for a specific product in a specific locale.\n\n        :param product_id: The product identifier for the default configuration.\n        :param locale: The locale for the default configuration.\n        :param default_configuration_request: The request body that includes the message identifier to configure as the default message.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/configure-default-message\n        \"\"\"\n        await self._make_request(f\"/inApps/v1/messaging/default/{product_id}/{locale}\", \"PUT\", {}, default_configuration_request, None, None)\n\n    async def delete_default_message(self, product_id: str, locale: str):\n        \"\"\"\n        Delete a default message for a product in a locale.\n\n        :param product_id: The product ID of the default message configuration.\n        :param locale: The locale of the default message configuration.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/delete-default-message\n        \"\"\"\n        await self._make_request(f\"/inApps/v1/messaging/default/{product_id}/{locale}\", \"DELETE\", {}, None, None, None)\n\n    async def get_default_message(self, product_id: str, locale: str) -> DefaultConfigurationResponse:\n        \"\"\"\n        Get the default message for a specific product in a specific locale.\n\n        :param product_id: The product identifier for the default configuration.\n        :param locale: The locale for the default configuration.\n        :return: A response that contains the default configuration information.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-default-message\n        \"\"\"\n        return await self._make_request(f\"/inApps/v1/messaging/default/{product_id}/{locale}\", \"GET\", {}, None, DefaultConfigurationResponse, None)\n\n    async def configure_realtime_url(self, realtime_url_request: RealtimeUrlRequest):\n        \"\"\"\n        Configure the real-time URL for retention messaging.\n\n        :param realtime_url_request: The request body that contains the real-time URL.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/configure-realtime-url\n        \"\"\"\n        await self._make_request(\"/inApps/v1/messaging/realtime/url\", \"PUT\", {}, realtime_url_request, None, None)\n\n    async def delete_realtime_url(self):\n        \"\"\"\n        Delete the real-time URL for retention messaging.\n\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/delete-realtime-url\n        \"\"\"\n        await self._make_request(\"/inApps/v1/messaging/realtime/url\", \"DELETE\", {}, None, None, None)\n\n    async def get_realtime_url(self) -> RealtimeUrlResponse:\n        \"\"\"\n        Get the real-time URL for retention messaging.\n\n        :return: A response that contains the real-time URL information.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-realtime-url\n        \"\"\"\n        return await self._make_request(\"/inApps/v1/messaging/realtime/url\", \"GET\", {}, None, RealtimeUrlResponse, None)\n\n    async def initiate_performance_test(self, performance_test_request: PerformanceTestRequest) -> PerformanceTestResponse:\n        \"\"\"\n        Initiates a performance test of your Get Retention Message endpoint in the sandbox environment.\n\n        :param performance_test_request: The request body which specifies a transaction identifier of an In-App Purchase to use for this test.\n        :return: The performance test response object.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/initiate-performance-test\n        \"\"\"\n        return await self._make_request(\"/inApps/v1/messaging/performanceTest\", \"POST\", {}, performance_test_request, PerformanceTestResponse, None)\n\n    async def get_performance_test_results(self, request_id: str) -> PerformanceTestResultResponse:\n        \"\"\"\n        Gets the results of the performance test for the specified identifier.\n\n        :param request_id: The ID of the performance test to return, which you receive in the PerformanceTestResponse when you call Initiate Performance Test.\n        :return: An object the API returns that describes the performance test results.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/retentionmessaging/get-performance-test-results\n        \"\"\"\n        return await self._make_request(f\"/inApps/v1/messaging/performanceTest/result/{request_id}\", \"GET\", {}, None, PerformanceTestResultResponse, None)\n\n    async def get_app_transaction_info(self, transaction_id: str) -> AppTransactionInfoResponse:\n        \"\"\"\n        Get a customer's app transaction information for your app.\n        \n        :param transaction_id Any originalTransactionId, transactionId or appTransactionId that belongs to the customer for your app.\n        :return: A response that contains signed app transaction information for a customer.\n        :raises APIException: If a response was returned indicating the request could not be processed\n        :see: https://developer.apple.com/documentation/appstoreserverapi/get-app-transaction-info\n        \"\"\"\n        return await self._make_request(f\"/inApps/v1/transactions/appTransactions/{transaction_id}\", \"GET\", {}, None, AppTransactionInfoResponse, None)\n    \n"
  },
  {
    "path": "appstoreserverlibrary/jws_signature_creator.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nimport datetime\nfrom typing import Any, Dict, Optional\nimport base64\nimport json\nimport jwt\nimport uuid\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import serialization\n\nfrom appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter\n\nclass AdvancedCommerceAPIInAppRequest:\n    def __init__(self):\n        pass\n\nclass JWSSignatureCreator:\n    def __init__(self, audience: str, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str):\n        self._audience = audience\n        self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend())\n        self._key_id = key_id\n        self._issuer_id = issuer_id\n        self._bundle_id = bundle_id\n\n    def _create_signature(self, feature_specific_claims: Dict[str, Any]) -> str:\n        claims = feature_specific_claims\n        current_time = datetime.datetime.now(datetime.timezone.utc)\n\n        claims[\"bid\"] = self._bundle_id\n        claims[\"iss\"] = self._issuer_id\n        claims[\"aud\"] = self._audience\n        claims[\"iat\"] = current_time\n        claims[\"nonce\"] = str(uuid.uuid4())\n\n        return jwt.encode(claims,\n            self._signing_key,\n            algorithm=\"ES256\",\n            headers={\"kid\": self._key_id},\n        )\n    \nclass PromotionalOfferV2SignatureCreator(JWSSignatureCreator):\n    def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str):\n        \"\"\"\n        Create a PromotionalOfferV2SignatureCreator\n\n        :param signing_key: Your private key downloaded from App Store Connect\n        :param key_id: Your private key ID from App Store Connect\n        :param issuer_id: Your issuer ID from the Keys page in App Store Connect\n        :param bundle_id: Your app's bundle ID\n        \"\"\"\n        super().__init__(audience=\"promotional-offer\", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id)\n\n    def create_signature(self, product_id: str, offer_identifier: str, transaction_id: Optional[str]) -> str:\n        \"\"\"\n        Create a promotional offer V2 signature.\n        https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests\n        \n        :param product_id: The unique identifier of the product\n        :param offer_identifier: The promotional offer identifier that you set up in App Store Connect\n        :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.\n        :return: The signed JWS.\n        \"\"\"\n        if product_id is None:\n            raise ValueError(\"product_id cannot be null\")\n        if offer_identifier is None:\n            raise ValueError(\"offer_identifier cannot be null\")\n        feature_specific_claims = {\n            \"productId\": product_id,\n            \"offerIdentifier\": offer_identifier\n        }\n        if transaction_id is not None:\n            feature_specific_claims[\"transactionId\"] = transaction_id\n        return self._create_signature(feature_specific_claims=feature_specific_claims)\n    \nclass IntroductoryOfferEligibilitySignatureCreator(JWSSignatureCreator):\n    def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str):\n        \"\"\"\n        Create an IntroductoryOfferEligibilitySignatureCreator\n\n        :param signing_key: Your private key downloaded from App Store Connect\n        :param key_id: Your private key ID from App Store Connect\n        :param issuer_id: Your issuer ID from the Keys page in App Store Connect\n        :param bundle_id: Your app's bundle ID\n        \"\"\"\n        super().__init__(audience=\"introductory-offer-eligibility\", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id)\n\n    def create_signature(self, product_id: str, allow_introductory_offer: bool, transaction_id: str) -> str:\n        \"\"\"\n        Create an introductory offer eligibility signature.\n        https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests\n        \n        :param product_id: The unique identifier of the product\n        :param allow_introductory_offer: A boolean value that determines whether the customer is eligible for an introductory offer\n        :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.\n        :return: The signed JWS.\n        \"\"\"\n        if product_id is None:\n            raise ValueError(\"product_id cannot be null\")\n        if allow_introductory_offer is None:\n            raise ValueError(\"allow_introductory_offer cannot be null\")\n        if transaction_id is None:\n            raise ValueError(\"transaction_id cannot be null\")\n        feature_specific_claims = {\n            \"productId\": product_id,\n            \"allowIntroductoryOffer\": allow_introductory_offer,\n            \"transactionId\": transaction_id\n        }\n        return self._create_signature(feature_specific_claims=feature_specific_claims)\n    \nclass AdvancedCommerceAPIInAppSignatureCreator(JWSSignatureCreator):\n    def __init__(self, signing_key: bytes, key_id: str, issuer_id: str, bundle_id: str):\n        \"\"\"\n        Create an AdvancedCommerceAPIInAppSignatureCreator\n\n        :param signing_key: Your private key downloaded from App Store Connect\n        :param key_id: Your private key ID from App Store Connect\n        :param issuer_id: Your issuer ID from the Keys page in App Store Connect\n        :param bundle_id: Your app's bundle ID\n        \"\"\"\n        super().__init__(audience=\"advanced-commerce-api\", signing_key=signing_key, key_id=key_id, issuer_id=issuer_id, bundle_id=bundle_id)\n\n    def create_signature(self, advanced_commerce_in_app_request: AdvancedCommerceAPIInAppRequest) -> str:\n        \"\"\"\n        Create an Advanced Commerce in-app signed request.\n        https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests\n        \n        :param advanced_commerce_in_app_request: The request to be signed.\n        :return: The signed JWS.\n        \"\"\"\n        if advanced_commerce_in_app_request is None:\n            raise ValueError(\"advanced_commerce_in_app_request cannot be null\")\n        c = _get_cattrs_converter(type(advanced_commerce_in_app_request))\n        request = c.unstructure(advanced_commerce_in_app_request)\n        encoded_request = base64.b64encode(json.dumps(request).encode(encoding='utf-8')).decode('utf-8')\n        feature_specific_claims = {\n            \"request\": encoded_request\n        }\n        return self._create_signature(feature_specific_claims=feature_specific_claims)"
  },
  {
    "path": "appstoreserverlibrary/models/AbstractAdvancedCommerceBaseItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom abc import ABC\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass AbstractAdvancedCommerceBaseItem(AttrsRawValueAware, ABC):\n    SKU: str = attr.ib(validator=AdvancedCommerceValidationUtils.sku_validator)\n    \"\"\"\n    The product identifier of an in-app purchase product you manage in your own system.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/sku\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AbstractAdvancedCommerceInAppRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom abc import ABC\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceRequest import AdvancedCommerceRequest\nfrom ..jws_signature_creator import AdvancedCommerceAPIInAppRequest\n\n@define\nclass AbstractAdvancedCommerceInAppRequest(AdvancedCommerceRequest, AdvancedCommerceAPIInAppRequest, ABC):\n    operation: str = attr.ib()\n    version: str = attr.ib()"
  },
  {
    "path": "appstoreserverlibrary/models/AbstractAdvancedCommerceItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nimport attr\n\nfrom .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n@define\nclass AbstractAdvancedCommerceItem(AbstractAdvancedCommerceBaseItem):\n    description: str = attr.ib(validator=AdvancedCommerceValidationUtils.description_validator)\n    \"\"\"\n    A string you provide that describes a SKU.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/description\n    \"\"\"\n\n    displayName: str = attr.ib(validator=AdvancedCommerceValidationUtils.display_name_validator)\n    \"\"\"\n    A string with a product name that you can localize and is suitable for display to customers.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/displayname\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AbstractAdvancedCommerceResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom abc import ABC\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass AbstractAdvancedCommerceResponse(ABC):\n    signedRenewalInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo\n    \"\"\"\n    \n    signedTransactionInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    Transaction information signed by the App Store, in JSON Web Signature (JWS) Compact Serialization format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AccountTenure.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AccountTenure(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The age of the customer's account.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/accounttenure\n    \"\"\"\n    UNDECLARED = 0\n    ZERO_TO_THREE_DAYS = 1\n    THREE_DAYS_TO_TEN_DAYS = 2\n    TEN_DAYS_TO_THIRTY_DAYS = 3\n    THIRTY_DAYS_TO_NINETY_DAYS = 4\n    NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS = 5\n    ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS = 6\n    GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS = 7\n"
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceDescriptors.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n@define\nclass AdvancedCommerceDescriptors:\n    \"\"\"\n    The display name and description of a subscription product.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/descriptors\n    \"\"\"\n    description: str = attr.ib(validator=AdvancedCommerceValidationUtils.description_validator)\n    \"\"\"\n    A string you provide that describes a SKU.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/description\n    \"\"\"\n\n    displayName: str = attr.ib(validator=AdvancedCommerceValidationUtils.display_name_validator)\n    \"\"\"\n    A string with a product name that you can localize and is suitable for display to customers.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/displayname\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceEffective.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AdvancedCommerceEffective(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A string value that indicates when a requested change to an auto-renewable subscription goes into effect.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/effective\n    \"\"\"\n    IMMEDIATELY = \"IMMEDIATELY\"\n    NEXT_BILL_CYCLE = \"NEXT_BILL_CYCLE\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceOffer.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceOfferPeriod import AdvancedCommerceOfferPeriod\nfrom .AdvancedCommerceOfferReason import AdvancedCommerceOfferReason\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass AdvancedCommerceOffer(AttrsRawValueAware):\n    \"\"\"\n    A discount offer for an auto-renewable subscription.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/offer\n    \"\"\"\n\n    periodCount: int = attr.ib(validator=AdvancedCommerceValidationUtils.period_count_validator)\n    \"\"\"\n    The number of periods the offer is active.\n    \"\"\"\n\n    price: int = attr.ib()\n    \"\"\"\n    The offer price, in milliunits.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/price\n    \"\"\"\n\n    period: AdvancedCommerceOfferPeriod = AdvancedCommerceOfferPeriod.create_main_attr('rawPeriod', raw_required=True)\n    \"\"\"\n    The period of the offer.\n    \"\"\"\n\n    rawPeriod: str = AdvancedCommerceOfferPeriod.create_raw_attr('period', required=True)\n    \"\"\"\n    See period\n    \"\"\"\n\n    reason: AdvancedCommerceOfferReason = AdvancedCommerceOfferReason.create_main_attr('rawReason', raw_required=True)\n    \"\"\"\n    The reason for the offer.\n    \"\"\"\n\n    rawReason: str = AdvancedCommerceOfferReason.create_raw_attr('reason', required=True)\n    \"\"\"\n    See reason\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceOfferPeriod.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AdvancedCommerceOfferPeriod(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The period of the offer.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/offer\n    \"\"\"\n    P3D = \"P3D\"\n    P1W = \"P1W\"\n    P2W = \"P2W\"\n    P1M = \"P1M\"\n    P2M = \"P2M\"\n    P3M = \"P3M\"\n    P6M = \"P6M\"\n    P9M = \"P9M\"\n    P1Y = \"P1Y\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceOfferReason.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AdvancedCommerceOfferReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The reason for the offer.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/offer\n    \"\"\"\n    ACQUISITION = \"ACQUISITION\"\n    WIN_BACK = \"WIN_BACK\"\n    RETENTION = \"RETENTION\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeCreateRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest\nfrom .AdvancedCommerceOneTimeChargeItem import AdvancedCommerceOneTimeChargeItem\n\n@define\nclass AdvancedCommerceOneTimeChargeCreateRequest(AbstractAdvancedCommerceInAppRequest):\n    \"\"\"\n    The request data your app provides when a customer purchases a one-time-charge product.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/onetimechargecreaterequest\n    \"\"\"\n\n    currency: str = attr.ib()\n    \"\"\"\n    The currency of the price of the product.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/currency\n    \"\"\"\n\n    item: AdvancedCommerceOneTimeChargeItem = attr.ib()\n    \"\"\"\n    The details of the product for purchase.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/onetimechargeitem\n    \"\"\"\n\n    taxCode: str = attr.ib()\n    \"\"\"\n    The tax code for this product.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/taxCode\n    \"\"\"\n\n    operation: str = attr.ib(init=False, default=\"CREATE_ONE_TIME_CHARGE\", on_setattr=attr.setters.frozen)\n    \"\"\"\n    The constant that represents the operation of this request.\n    \"\"\"\n\n    version: str = attr.ib(init=False, default=\"1\", on_setattr=attr.setters.frozen)\n    \"\"\"\n    The version number of the API.\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The storefront for the transaction.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceOneTimeChargeItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nimport attr\n\nfrom .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem\n\n@define\nclass AdvancedCommerceOneTimeChargeItem(AbstractAdvancedCommerceItem):\n    \"\"\"\n    The details of a one-time charge product, including its display name, price, SKU, and metadata.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/onetimechargeitem\n    \"\"\"\n\n    price: int = attr.ib()\n    \"\"\"\n    The price, in milliunits of the currency, of the one-time charge product.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/price\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommercePeriod.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AdvancedCommercePeriod(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The duration of a single cycle of an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/period\n    \"\"\"\n    \n    P1W = \"P1W\"\n    P1M = \"P1M\"\n    P2M = \"P2M\"\n    P3M = \"P3M\"\n    P6M = \"P6M\"\n    P1Y = \"P1Y\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceReason.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AdvancedCommerceReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The data your app provides to change an item of an auto-renewable subscription.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem\n    \"\"\"\n    UPGRADE = \"UPGRADE\"\n    DOWNGRADE = \"DOWNGRADE\"\n    APPLY_OFFER = \"APPLY_OFFER\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceRefundReason.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AdvancedCommerceRefundReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A reason to request a refund.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/refundreason\n    \"\"\"\n    \n    UNINTENDED_PURCHASE = \"UNINTENDED_PURCHASE\"\n    FULFILLMENT_ISSUE = \"FULFILLMENT_ISSUE\"\n    UNSATISFIED_WITH_PURCHASE = \"UNSATISFIED_WITH_PURCHASE\"\n    LEGAL = \"LEGAL\"\n    OTHER = \"OTHER\"\n    MODIFY_ITEMS_REFUND = \"MODIFY_ITEMS_REFUND\"\n    SIMULATE_REFUND_DECLINE = \"SIMULATE_REFUND_DECLINE\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceRefundType.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AdvancedCommerceRefundType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    Information about the refund request for an item, such as its SKU, the refund amount, reason, and type.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem\n    \"\"\"\n    FULL = \"FULL\"\n    PRORATED = \"PRORATED\"\n    CUSTOM = \"CUSTOM\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom abc import ABC\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass AdvancedCommerceRequest(AttrsRawValueAware, ABC):\n    requestInfo: AdvancedCommerceRequestInfo = attr.ib()\n    \"\"\"\n    The metadata to include in server requests.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/requestinfo\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceRequestInfo.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\n@define\nclass AdvancedCommerceRequestInfo:\n    \"\"\"\n    The metadata to include in server requests.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/requestinfo\n    \"\"\"\n\n    requestReferenceId: UUID = attr.ib()\n    \"\"\"\n    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.\n    \"\"\"\n\n    appAccountToken: Optional[UUID] = attr.ib(default=None)\n    \"\"\"\n    A UUID that represents an app account token, to associate with the transaction in the request.\n    \"\"\"\n\n    consistencyToken: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The value of the advancedCommerceConsistencyToken that you receive in the JWSRenewalInfo renewal information for a subscription. Don’t generate this value.\n    \n    https://developer.apple.com/documentation/AppStoreServerAPI/advancedCommerceConsistencyToken\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceRequestRefundItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem\nfrom .AdvancedCommerceRefundReason import AdvancedCommerceRefundReason\nfrom .AdvancedCommerceRefundType import AdvancedCommerceRefundType\n\n@define\nclass AdvancedCommerceRequestRefundItem(AbstractAdvancedCommerceBaseItem):\n    \"\"\"\n    Information about the refund request for an item, such as its SKU, the refund amount, reason, and type.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem\n    \"\"\"\n\n    revoke: bool = attr.ib()\n\n    refundReason: AdvancedCommerceRefundReason = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', raw_required=True)\n    \"\"\"\n    The reason for the refund request.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/refundreason\n    \"\"\"\n\n    rawRefundReason: str = AdvancedCommerceRefundReason.create_raw_attr('refundReason', required=True)\n    \"\"\"\n    See refundReason\n    \"\"\"\n\n    refundType: AdvancedCommerceRefundType = AdvancedCommerceRefundType.create_main_attr('rawRefundType', raw_required=True)\n    \"\"\"\n    The type of refund requested.\n    \"\"\"\n\n    rawRefundType: str = AdvancedCommerceRefundType.create_raw_attr('refundType', required=True)\n    \"\"\"\n    See refundType\n    \"\"\"\n\n    refundAmount: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The refund amount you're requesting for the SKU, in milliunits of the currency.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/refundamount\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceRequestRefundRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, List\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceRequest import AdvancedCommerceRequest\nfrom .AdvancedCommerceRequestRefundItem import AdvancedCommerceRequestRefundItem\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n@define\nclass AdvancedCommerceRequestRefundRequest(AdvancedCommerceRequest):\n    \"\"\"\n    The request body for requesting a refund for a transaction.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/requestrefundrequest\n    \"\"\"\n\n    items: List[AdvancedCommerceRequestRefundItem] = attr.ib(validator=AdvancedCommerceValidationUtils.items_validator)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/requestrefunditem\n    \"\"\"\n\n    refundRiskingPreference: bool = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/refundriskingpreference\n    \"\"\"\n\n    currency: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The currency of the transaction.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/currency\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceRequestRefundResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse\n\nclass AdvancedCommerceRequestRefundResponse(AbstractAdvancedCommerceResponse):\n    \"\"\"\n    The response body for a transaction refund request.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/requestrefundresponse\n    \"\"\"\n\n    def __init__(self, signedTransactionInfo: str):\n        super().__init__(signedRenewalInfo=None, signedTransactionInfo=signedTransactionInfo)"
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceRequest import AdvancedCommerceRequest\n\n@define\nclass AdvancedCommerceSubscriptionCancelRequest(AdvancedCommerceRequest):\n    \"\"\"\n    The request body for turning off automatic renewal of a subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncancelrequest\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionCancelResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\n\nfrom .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse\n\n@define\nclass AdvancedCommerceSubscriptionCancelResponse(AbstractAdvancedCommerceResponse):\n    \"\"\"\n    The response body for a successful subscription cancellation.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncancelresponse\n    \"\"\"\n    pass"
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataDescriptors.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceEffective import AdvancedCommerceEffective\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n@define\nclass AdvancedCommerceSubscriptionChangeMetadataDescriptors():\n    \"\"\"\n    The subscription metadata to change, specifically the description and display name.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadatadescriptors\n    \"\"\"\n\n    effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True)\n    \"\"\"\n    The string that determines when the metadata change goes into effect.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/effective\n    \"\"\"\n\n    rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True)\n    \"\"\"\n    See effective\n    \"\"\"\n\n    description: Optional[str] = attr.ib(\n        default=None,\n        validator=attr.validators.optional(AdvancedCommerceValidationUtils.description_validator)\n    )\n    \"\"\"\n    The new description for the subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/description\n    \"\"\"\n\n    displayName: Optional[str] = attr.ib(\n        default=None,\n        validator=attr.validators.optional(AdvancedCommerceValidationUtils.display_name_validator)\n    )\n    \"\"\"\n    The new display name for the subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/displayname\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceEffective import AdvancedCommerceEffective\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n@define\nclass AdvancedCommerceSubscriptionChangeMetadataItem():\n    \"\"\"\n    The metadata to change for an item, specifically its SKU, description, and display name.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadataitem\n    \"\"\"\n\n    currentSKU: str = attr.ib(validator=AdvancedCommerceValidationUtils.sku_validator)\n    \"\"\"\n    The original SKU of the item.\n    \"\"\"\n\n    effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True)\n    \"\"\"\n    The string that determines when the metadata change goes into effect.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/effective\n    \"\"\"\n\n    rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True)\n    \"\"\"\n    See effective\n    \"\"\"\n\n    description: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.description_validator))\n    \"\"\"\n    The new description for the item.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/description\n    \"\"\"\n\n    displayName: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.display_name_validator))\n    \"\"\"\n    The new display name for the item.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/displayname\n    \"\"\"   \n    \n    SKU: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.sku_validator))\n    \"\"\"\n    The new SKU of the item.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/sku\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, List\n\nfrom attr import define\nimport attr\n\nfrom .AdvancedCommerceRequest import AdvancedCommerceRequest\nfrom .AdvancedCommerceSubscriptionChangeMetadataDescriptors import AdvancedCommerceSubscriptionChangeMetadataDescriptors\nfrom .AdvancedCommerceSubscriptionChangeMetadataItem import AdvancedCommerceSubscriptionChangeMetadataItem\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n@define\nclass AdvancedCommerceSubscriptionChangeMetadataRequest(AdvancedCommerceRequest):\n    \"\"\"\n    The request body you provide to change the metadata of a subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadatarequest\n    \"\"\"\n\n    descriptors: Optional[AdvancedCommerceSubscriptionChangeMetadataDescriptors] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadatadescriptors\n    \"\"\"\n\n    items: Optional[List[AdvancedCommerceSubscriptionChangeMetadataItem]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.items_validator))\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadataitem\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\"\n\n    taxCode: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/taxcode\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionChangeMetadataResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse\n\nclass AdvancedCommerceSubscriptionChangeMetadataResponse(AbstractAdvancedCommerceResponse):\n    \"\"\"\n    The response body for a successful subscription metadata change.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionchangemetadataresponse\n    \"\"\"\n\n    def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str):\n        super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo)"
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nimport attr\nfrom attr import define\nfrom .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem\nfrom .AdvancedCommerceOffer import AdvancedCommerceOffer\n\n\n@define\nclass AdvancedCommerceSubscriptionCreateItem(AbstractAdvancedCommerceItem):\n    \"\"\"\n    The data that describes a subscription item.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncreateitem\n    \"\"\"\n\n    price: int = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/price\n    \"\"\"\n\n    offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/offer\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionCreateRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, List\nimport attr\nfrom attr import define\nfrom .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest\nfrom .AdvancedCommerceDescriptors import AdvancedCommerceDescriptors\nfrom .AdvancedCommerceSubscriptionCreateItem import AdvancedCommerceSubscriptionCreateItem\nfrom .AdvancedCommercePeriod import AdvancedCommercePeriod\n\n\n@define\nclass AdvancedCommerceSubscriptionCreateRequest(AbstractAdvancedCommerceInAppRequest):\n    \"\"\"\n    The request data your app provides when a customer purchases an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncreaterequest\n    \"\"\"\n\n    operation: str = attr.ib(init=False, default=\"CREATE_SUBSCRIPTION\", on_setattr=attr.setters.frozen)\n\n    version: str = attr.ib(init=False, default=\"1\", on_setattr=attr.setters.frozen)\n\n    currency: str = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/currency\n    \"\"\"\n\n    descriptors: AdvancedCommerceDescriptors = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/descriptors\n    \"\"\"\n\n    items: List[AdvancedCommerceSubscriptionCreateItem] = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptioncreateitem\n    \"\"\"\n\n    taxCode: str = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/taxCode\n    \"\"\"\n\n    period: AdvancedCommercePeriod = AdvancedCommercePeriod.create_main_attr('rawPeriod', raw_required=True)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/period\n    \"\"\"\n\n    rawPeriod: str = AdvancedCommercePeriod.create_raw_attr('period', required=True)\n    \"\"\"\n    See period\n    \"\"\"\n\n    previousTransactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/transactionid\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateDescriptors.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom .AdvancedCommerceDescriptors import AdvancedCommerceDescriptors\n\n\n@define\nclass AdvancedCommerceSubscriptionMigrateDescriptors(AdvancedCommerceDescriptors):\n    \"\"\"\n    The description and display name of the subscription to migrate to that you manage.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigratedescriptors\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem\n\n\n@define\nclass AdvancedCommerceSubscriptionMigrateItem(AbstractAdvancedCommerceItem):\n    \"\"\"\n    The SKU, description, and display name to use for a migrated subscription item.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigrateitem\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRenewalItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem\n\n\n@define\nclass AdvancedCommerceSubscriptionMigrateRenewalItem(AbstractAdvancedCommerceItem):\n    \"\"\"\n    The item information that replaces a migrated subscription item when the subscription renews.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigraterenewalitem\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, List\nimport attr\nfrom attr import define\nfrom .AdvancedCommerceRequest import AdvancedCommerceRequest\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\nfrom .AdvancedCommerceSubscriptionMigrateDescriptors import AdvancedCommerceSubscriptionMigrateDescriptors\nfrom .AdvancedCommerceSubscriptionMigrateItem import AdvancedCommerceSubscriptionMigrateItem\nfrom .AdvancedCommerceSubscriptionMigrateRenewalItem import AdvancedCommerceSubscriptionMigrateRenewalItem\n\n@define\nclass AdvancedCommerceSubscriptionMigrateRequest(AdvancedCommerceRequest):\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigraterequest\n    \"\"\"\n\n    descriptors: AdvancedCommerceSubscriptionMigrateDescriptors = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigratedescriptors\n    \"\"\"\n\n    items: List[AdvancedCommerceSubscriptionMigrateItem] = attr.ib(validator=AdvancedCommerceValidationUtils.items_validator)\n    \"\"\"\n    An array of one or more SKUs, along with descriptions and display names, that are included in the subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigrateitem\n    \"\"\"\n\n    targetProductId: str = attr.ib()\n    \"\"\"\n    Your generic product ID for an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/targetproductid\n    \"\"\"\n\n    taxCode: str = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/taxcode\n    \"\"\"\n\n    renewalItems: Optional[List[AdvancedCommerceSubscriptionMigrateRenewalItem]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.items_validator))\n    \"\"\"\n    An optional array of subscription items that represents the items that renew at the next renewal period, if they differ from items.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigraterenewalitem\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionMigrateResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse\n\n\n@define\nclass AdvancedCommerceSubscriptionMigrateResponse(AbstractAdvancedCommerceResponse):\n    \"\"\"\n    A response that contains signed renewal and transaction information after a subscription successfully migrates to the Advanced Commerce API.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmigrateresponse\n    \"\"\"\n    def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str):\n        super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo)"
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyAddItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nimport attr\nfrom attr import define\nfrom .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem\nfrom .AdvancedCommerceOffer import AdvancedCommerceOffer\n\n\n@define\nclass AdvancedCommerceSubscriptionModifyAddItem(AbstractAdvancedCommerceItem):\n    \"\"\"\n    The data your app provides to add items when it makes changes to an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyadditem\n    \"\"\"\n\n    price: int = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/price\n    \"\"\"\n\n    offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None)\n    \"\"\"\n    A discount offer for an auto-renewable subscription.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/offer\n    \"\"\"\n\n    proratedPrice: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/proratedprice\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyChangeItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nimport attr\nfrom attr import define\nfrom .AbstractAdvancedCommerceItem import AbstractAdvancedCommerceItem\nfrom .AdvancedCommerceEffective import AdvancedCommerceEffective\nfrom .AdvancedCommerceOffer import AdvancedCommerceOffer\nfrom .AdvancedCommerceReason import AdvancedCommerceReason\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n\n@define\nclass AdvancedCommerceSubscriptionModifyChangeItem(AbstractAdvancedCommerceItem):\n    \"\"\"\n    The data your app provides to change an item of an auto-renewable subscription.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem\n    \"\"\"\n\n    currentSKU: str = attr.ib(validator=AdvancedCommerceValidationUtils.sku_validator)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/sku\n    \"\"\"\n\n    price: int = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/price\n    \"\"\"\n\n    reason: AdvancedCommerceReason = AdvancedCommerceReason.create_main_attr('rawReason', raw_required=True)\n\n    rawReason: str = AdvancedCommerceReason.create_raw_attr('reason', required=True)\n    \"\"\"\n    See reason\n    \"\"\"\n\n    effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/effective\n    \"\"\"\n\n    rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True)\n    \"\"\"\n    See effective\n    \"\"\"\n\n    offer: Optional[AdvancedCommerceOffer] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/offer\n    \"\"\"\n\n    proratedPrice: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/proratedprice\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyDescriptors.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nimport attr\nfrom attr import define\nfrom .AdvancedCommerceEffective import AdvancedCommerceEffective\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n@define\nclass AdvancedCommerceSubscriptionModifyDescriptors():\n    \"\"\"\n    The data your app provides to change the description and display name of an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifydescriptors\n    \"\"\"\n\n    effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/effective\n    \"\"\"\n\n    rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True)\n    \"\"\"\n    See effective\n    \"\"\"\n\n    description: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.description_validator))\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/description\n    \"\"\"\n\n    displayName: Optional[str] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.display_name_validator)\n    )\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/displayname\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyInAppRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, List\nimport attr\nfrom attr import define\nfrom .AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\nfrom .AdvancedCommerceSubscriptionModifyAddItem import AdvancedCommerceSubscriptionModifyAddItem\nfrom .AdvancedCommerceSubscriptionModifyChangeItem import AdvancedCommerceSubscriptionModifyChangeItem\nfrom .AdvancedCommerceSubscriptionModifyDescriptors import AdvancedCommerceSubscriptionModifyDescriptors\nfrom .AdvancedCommerceSubscriptionModifyPeriodChange import AdvancedCommerceSubscriptionModifyPeriodChange\nfrom .AdvancedCommerceSubscriptionModifyRemoveItem import AdvancedCommerceSubscriptionModifyRemoveItem\n\n\n@define\nclass AdvancedCommerceSubscriptionModifyInAppRequest(AbstractAdvancedCommerceInAppRequest):\n    \"\"\"\n    The request data your app provides to make changes to an auto-renewable subscription.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyinapprequest\n    \"\"\"\n\n    operation: str = attr.ib(default=\"MODIFY_SUBSCRIPTION\", init=False, on_setattr=attr.setters.frozen)\n\n    version: str = attr.ib(default=\"1\", init=False, on_setattr=attr.setters.frozen)\n\n    transactionId: str = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/transactionid\n    \"\"\"\n\n    retainBillingCycle: bool = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/retainbillingcycle\n    \"\"\"\n\n    addItems: Optional[List[AdvancedCommerceSubscriptionModifyAddItem]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.items_validator))\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyadditem\n    \"\"\"\n\n    changeItems: Optional[List[AdvancedCommerceSubscriptionModifyChangeItem]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.items_validator))\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifychangeitem\n    \"\"\"\n\n    currency: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/currency\n    \"\"\"\n\n    descriptors: Optional[AdvancedCommerceSubscriptionModifyDescriptors] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifydescriptors\n    \"\"\"\n\n    periodChange: Optional[AdvancedCommerceSubscriptionModifyPeriodChange] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyperiodchange\n    \"\"\"\n\n    removeItems: Optional[List[AdvancedCommerceSubscriptionModifyRemoveItem]] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyremoveitem\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\"\n\n    taxCode: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/taxcode\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyPeriodChange.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nimport attr\nfrom attr import define\nfrom .AdvancedCommerceEffective import AdvancedCommerceEffective\nfrom .AdvancedCommercePeriod import AdvancedCommercePeriod\nfrom .LibraryUtility import AttrsRawValueAware\n\n\n@define\nclass AdvancedCommerceSubscriptionModifyPeriodChange(AttrsRawValueAware):\n    \"\"\"\n    The data your app provides to change the period of an auto-renewable subscription.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyperiodchange\n    \"\"\"\n\n    effective: AdvancedCommerceEffective = AdvancedCommerceEffective.create_main_attr('rawEffective', raw_required=True)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/effective\n    \"\"\"\n\n    rawEffective: str = AdvancedCommerceEffective.create_raw_attr('effective', required=True)\n    \"\"\"\n    See effective\n    \"\"\"\n\n    period: AdvancedCommercePeriod = AdvancedCommercePeriod.create_main_attr('rawPeriod', raw_required=True)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/period\n    \"\"\"\n\n    rawPeriod: str = AdvancedCommercePeriod.create_raw_attr('period', required=True)\n    \"\"\"\n    See period\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionModifyRemoveItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem\n\n\n@define\nclass AdvancedCommerceSubscriptionModifyRemoveItem(AbstractAdvancedCommerceBaseItem):\n    \"\"\"\n    The data your app provides to remove an item from an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionmodifyremoveitem\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nimport attr\nfrom attr import define\nfrom typing import List, Optional  \nfrom .AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem\nfrom .AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\n\n@define\nclass AdvancedCommerceSubscriptionPriceChangeItem(AbstractAdvancedCommerceBaseItem):\n    \"\"\"\n    The data your app provides to change a subscription price.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionpricechangeitem\n    \"\"\"\n\n    price: int = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/price\n    \"\"\"\n\n    dependentSKUs: Optional[List[str]] = attr.ib(default=None, validator=attr.validators.optional(AdvancedCommerceValidationUtils.dependent_skus_validator))\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/dependentsku\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, List\nimport attr\nfrom attr import define\nfrom .AdvancedCommerceRequest import AdvancedCommerceRequest\nfrom .AdvancedCommerceSubscriptionPriceChangeItem import AdvancedCommerceSubscriptionPriceChangeItem\n\n@define\nclass AdvancedCommerceSubscriptionPriceChangeRequest(AdvancedCommerceRequest):\n    \"\"\"\n    The request body you use to change the price of an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionpricechangerequest\n    \"\"\"\n\n    items: List[AdvancedCommerceSubscriptionPriceChangeItem] = attr.ib()\n    \"\"\"\n    An array that contains one or more SKUs and the changed price for each SKU.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionpricechangeitem\n    \"\"\"\n\n    currency: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The currency of the prices.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/currency\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The App Store storefront of the subscription.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionPriceChangeResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom .AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse\n\n\n@define\nclass AdvancedCommerceSubscriptionPriceChangeResponse(AbstractAdvancedCommerceResponse):\n    \"\"\"\n    A response that contains signed JWS renewal and JWS transaction information after a subscription price change request.\n    \n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionpricechangeresponse\n    \"\"\"\n    def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str):\n        super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo)"
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateInAppRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom __future__ import annotations\nfrom typing import List, Optional\nimport attr\nfrom appstoreserverlibrary.models.AbstractAdvancedCommerceInAppRequest import AbstractAdvancedCommerceInAppRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionReactivateItem import AdvancedCommerceSubscriptionReactivateItem\n\n@attr.define\nclass AdvancedCommerceSubscriptionReactivateInAppRequest(AbstractAdvancedCommerceInAppRequest):\n    \"\"\"\n    The request your app provides to reactivate a subscription that has automatic renewal turned off.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateinapprequest\n    \"\"\"\n    operation: str = attr.ib(init=False, default=\"REACTIVATE_SUBSCRIPTION\", on_setattr=attr.setters.frozen)\n\n    version: str = attr.ib(init=False, default=\"1\", on_setattr=attr.setters.frozen)\n\n    transactionId: str = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/appstoreserverapi/transactionid\n    \"\"\"\n    \n    items: Optional[List[AdvancedCommerceSubscriptionReactivateItem]] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateitem\n    \"\"\"\n    \n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionReactivateItem.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom appstoreserverlibrary.models.AbstractAdvancedCommerceBaseItem import AbstractAdvancedCommerceBaseItem\n\n\n@define\nclass AdvancedCommerceSubscriptionReactivateItem(AbstractAdvancedCommerceBaseItem):\n    \"\"\"\n    An item in a subscription to reactive.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionreactivateitem\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom __future__ import annotations\nfrom typing import Optional\nimport attr\nfrom appstoreserverlibrary.models.AdvancedCommerceRequest import AdvancedCommerceRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceRefundReason import AdvancedCommerceRefundReason\nfrom appstoreserverlibrary.models.AdvancedCommerceRefundType import AdvancedCommerceRefundType\n\n\n@attr.define\nclass AdvancedCommerceSubscriptionRevokeRequest(AdvancedCommerceRequest):\n    \"\"\"\n    The request body you provide to terminate a subscription and all its items immediately.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionrevokerequest\n    \"\"\"\n\n    refundRiskingPreference: bool = attr.ib()\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/refundriskingpreference\n    \"\"\"\n\n    refundType: AdvancedCommerceRefundType = AdvancedCommerceRefundType.create_main_attr('rawRefundType', raw_required=True)\n\n    rawRefundType: str = AdvancedCommerceRefundType.create_raw_attr('refundType', required=True)\n    \"\"\"\n    See refundType\n    \"\"\"\n\n    refundReason: AdvancedCommerceRefundReason = AdvancedCommerceRefundReason.create_main_attr('rawRefundReason', raw_required=True)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/refundreason\n    \"\"\"\n\n    rawRefundReason: str = AdvancedCommerceRefundReason.create_raw_attr('refundReason', required=True)\n    \"\"\"\n    See refundReason\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    https://developer.apple.com/documentation/advancedcommerceapi/storefront\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceSubscriptionRevokeResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom appstoreserverlibrary.models.AbstractAdvancedCommerceResponse import AbstractAdvancedCommerceResponse\n\n\n@define\nclass AdvancedCommerceSubscriptionRevokeResponse(AbstractAdvancedCommerceResponse):\n    \"\"\"\n    The response body for a successful revoke-subscription request.\n\n    https://developer.apple.com/documentation/advancedcommerceapi/subscriptionrevokeresponse\n    \"\"\"\n    def __init__(self, signedRenewalInfo: str, signedTransactionInfo: str):\n        super().__init__(signedRenewalInfo=signedRenewalInfo, signedTransactionInfo=signedTransactionInfo)"
  },
  {
    "path": "appstoreserverlibrary/models/AdvancedCommerceValidationUtils.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import TypeVar\n\nT = TypeVar('T')\n\n\nclass AdvancedCommerceValidationUtils:\n    MAXIMUM_DESCRIPTION_LENGTH = 45\n    MAXIMUM_DISPLAY_NAME_LENGTH = 30\n    MAXIMUM_SKU_LENGTH = 128\n    MIN_PERIOD = 1\n    MAX_PERIOD = 12\n\n    @staticmethod\n    def description_validator(instance, attribute, value):\n        \"\"\"\n        Validates description is not None and does not exceed maximum length.\n\n        Raises:\n            ValueError: If description exceeds maximum length\n        \"\"\"\n        if len(value) > AdvancedCommerceValidationUtils.MAXIMUM_DESCRIPTION_LENGTH:\n            raise ValueError(\n                f\"Description length cannot exceed \"\n                f\"{AdvancedCommerceValidationUtils.MAXIMUM_DESCRIPTION_LENGTH} characters\"\n            )\n\n    @staticmethod\n    def display_name_validator(instance, attribute, value):\n        \"\"\"\n        Validates display name is not None and does not exceed maximum length.\n\n        Raises:\n            ValueError: If display name exceeds maximum length\n        \"\"\"\n        if len(value) > AdvancedCommerceValidationUtils.MAXIMUM_DISPLAY_NAME_LENGTH:\n            raise ValueError(\n                f\"Display name length cannot exceed \"\n                f\"{AdvancedCommerceValidationUtils.MAXIMUM_DISPLAY_NAME_LENGTH} characters\"\n            )\n\n    @staticmethod\n    def sku_validator(instance, attribute, value):\n        \"\"\"\n        Validates SKU does not exceed maximum length.\n\n        Raises:\n            ValueError: If SKU exceeds maximum length\n        \"\"\"\n        if len(value) > AdvancedCommerceValidationUtils.MAXIMUM_SKU_LENGTH:\n            raise ValueError(\n                f\"SKU length cannot exceed \"\n                f\"{AdvancedCommerceValidationUtils.MAXIMUM_SKU_LENGTH} characters\"\n            )\n\n    @staticmethod\n    def period_count_validator(instance, attribute, value):\n        \"\"\"\n        Validates periodCount is not None and between `MIN_PERIOD` and `MAX_PERIOD` inclusive.\n\n        Raises:\n            ValueError: If period_count is out of range\n        \"\"\"\n        if (value < AdvancedCommerceValidationUtils.MIN_PERIOD or\n            value > AdvancedCommerceValidationUtils.MAX_PERIOD):\n            raise ValueError(\n                f\"Period count must be between \"\n                f\"{AdvancedCommerceValidationUtils.MIN_PERIOD} and \"\n                f\"{AdvancedCommerceValidationUtils.MAX_PERIOD}\"\n            )\n\n    @staticmethod\n    def items_validator(instance, attribute, value):\n        \"\"\"\n        Validates a list of items is not None, not empty, and contains no None elements.\n\n        Raises:\n            ValueError: If list is empty or contains None elements\n        \"\"\"\n        if not value:\n            raise ValueError(\"Items list cannot be empty\")\n\n        for i, item in enumerate(value):\n            if item is None:\n                raise ValueError(f\"Item at index {i} in the list cannot be None\")\n\n    @staticmethod\n    def dependent_skus_validator(instance, attribute, value):\n        \"\"\"\n        Validates that each SKU in the dependentSKUs list does not exceed maximum length.\n\n        Raises:\n            ValueError: If any SKU exceeds maximum length\n        \"\"\"\n        for sku in value:\n            if len(sku) > AdvancedCommerceValidationUtils.MAXIMUM_SKU_LENGTH:\n                raise ValueError(\n                    f\"SKU length cannot exceed \"\n                    f\"{AdvancedCommerceValidationUtils.MAXIMUM_SKU_LENGTH} characters\"\n                )\n"
  },
  {
    "path": "appstoreserverlibrary/models/AlternateProduct.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\n@define\nclass AlternateProduct:\n    \"\"\"\n    A switch-plan message and product ID you provide in a real-time response to your Get Retention Message endpoint.\n\n    https://developer.apple.com/documentation/retentionmessaging/alternateproduct\n    \"\"\"\n\n    messageIdentifier: Optional[UUID] = attr.ib(default=None)\n    \"\"\"\n    The message identifier of the text to display in the switch-plan retention message.\n\n    https://developer.apple.com/documentation/retentionmessaging/messageidentifier\n    \"\"\"\n\n    productId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The product identifier of the subscription the retention message suggests for your customer to switch to.\n\n    https://developer.apple.com/documentation/retentionmessaging/productid\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/AppData.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .Environment import Environment\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass AppData(AttrsRawValueAware):\n    \"\"\"\n    The object that contains the app metadata and signed app transaction information.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/appdata\n    \"\"\"\n\n    appAppleId: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of the app that the notification applies to.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/appappleid\n    \"\"\"\n\n    bundleId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The bundle identifier of the app.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/bundleid\n    \"\"\"\n\n    environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment')\n    \"\"\"\n    The server environment that the notification applies to, either sandbox or production.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/environment\n    \"\"\"\n\n    rawEnvironment: Optional[str] = Environment.create_raw_attr('environment')\n    \"\"\"\n    See environment\n    \"\"\"\n\n    signedAppTransactionInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    App transaction information signed by the App Store, in JSON Web Signature (JWS) format.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/jwsapptransaction\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/AppTransaction.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .LibraryUtility import AttrsRawValueAware\n\nfrom .Environment import Environment\nfrom .PurchasePlatform import PurchasePlatform\n\n@define\nclass AppTransaction(AttrsRawValueAware):\n    \"\"\"\n    A decoded payload that contains app transaction information.\n\n    https://developer.apple.com/documentation/storekit/apptransaction\n    https://developer.apple.com/documentation/appstoreserverapi/jwsapptransactiondecodedpayload\n    \"\"\"\n\n    receiptType: Optional[Environment] = Environment.create_main_attr('rawReceiptType')\n    \"\"\"\n    The date that the App Store signed the JWS app transaction.\n\n    https://developer.apple.com/documentation/appstoreserverapi/environment\n    \"\"\"\n\n    rawReceiptType: Optional[str] = Environment.create_raw_attr('receiptType')\n    \"\"\"\n    See receiptType\n    \"\"\"\n\n    appAppleId: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier the App Store uses to identify the app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/appappleid\n    \"\"\"\n\n    bundleId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The bundle identifier that the app transaction applies to.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/bundleid\n    \"\"\"\n\n    applicationVersion: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The app version that the app transaction applies to.\n    \n    https://developer.apple.com/documentation/storekit/apptransaction/appversion\n    \"\"\"\n\n    versionExternalIdentifier: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The version external identifier of the app\n    \n    https://developer.apple.com/documentation/storekit/apptransaction/appversionid\n    \"\"\"\n\n    receiptCreationDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The date that the App Store signed the JWS app transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/receiptcreationdate\n    \"\"\"\n\n    originalPurchaseDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The date the customer originally purchased the app from the App Store.\n\n    https://developer.apple.com/documentation/appstoreserverapi/originalpurchasedate\n    \"\"\"\n\n    originalApplicationVersion: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The app version that the user originally purchased from the App Store.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originalapplicationversion\n    \"\"\"\n\n    deviceVerification: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The Base64 device verification value to use to verify whether the app transaction belongs to the device.\n    \n    https://developer.apple.com/documentation/storekit/apptransaction/deviceverification\n    \"\"\"\n\n    deviceVerificationNonce: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The UUID used to compute the device verification value.\n    \n    https://developer.apple.com/documentation/storekit/apptransaction/deviceverificationnonce\n    \"\"\"\n\n    preorderDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The date the customer placed an order for the app before it's available in the App Store.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/preorderdate\n    \"\"\"\n\n    appTransactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of the app download transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/apptransactionid\n    \"\"\"\n\n    originalPlatform: Optional[PurchasePlatform] = PurchasePlatform.create_main_attr('rawOriginalPlatform')\n    \"\"\"\n    The platform on which the customer originally purchased the app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originalplatform\n    \"\"\"\n\n    rawOriginalPlatform: Optional[str] = PurchasePlatform.create_raw_attr('originalPlatform')\n    \"\"\"\n    See originalPlatform\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AppTransactionInfoResponse.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass AppTransactionInfoResponse:\n    \"\"\"\n    A response that contains signed app transaction information for a customer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/apptransactioninforesponse\n    \"\"\"\n    \n    signedAppTransactionInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A customer’s app transaction information, signed by Apple, in JSON Web Signature (JWS) format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwsapptransaction\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/AutoRenewStatus.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass AutoRenewStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The renewal status for an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus\n    \"\"\"\n    OFF = 0\n    ON = 1\n"
  },
  {
    "path": "appstoreserverlibrary/models/BulletPoint.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\n@define\nclass BulletPoint:\n    \"\"\"\n    The text and its bullet-point image to include in a retention message’s bulleted list.\n\n    https://developer.apple.com/documentation/retentionmessaging/bulletpoint\n    \"\"\"\n\n    text: str = attr.ib(validator=attr.validators.max_len(66))\n    \"\"\"\n    The text of the individual bullet point.\n\n    https://developer.apple.com/documentation/retentionmessaging/bulletpointtext\n    \"\"\"\n\n    imageIdentifier: UUID = attr.ib()\n    \"\"\"\n    The identifier of the image to use as the bullet point.\n\n    https://developer.apple.com/documentation/retentionmessaging/imageidentifier\n    \"\"\"\n\n    altText: str = attr.ib(validator=attr.validators.max_len(150))\n    \"\"\"\n    The alternative text you provide for the corresponding image of the bullet point.\n\n    https://developer.apple.com/documentation/retentionmessaging/alttext\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/CheckTestNotificationResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import List, Optional\n\nfrom attr import define\nimport attr\n\nfrom .SendAttemptItem import SendAttemptItem\n\n@define\nclass CheckTestNotificationResponse: \n    \"\"\"\n    A response that contains the contents of the test notification sent by the App Store server and the result from your server.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/checktestnotificationresponse\n    \"\"\"\n\n    signedPayload: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/signedpayload\n    \"\"\"\n\n    sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/ConsumptionRequest.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .DeliveryStatus import DeliveryStatus\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .RefundPreference import RefundPreference\n\n@define\nclass ConsumptionRequest(AttrsRawValueAware):\n    \"\"\"\n    The request body that contains consumption information for an In-App Purchase.\n\n    https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest\n    \"\"\"\n\n    customerConsented: bool = attr.ib()\n    \"\"\"\n    A Boolean value that indicates whether the customer consented to provide consumption data to the App Store.\n\n    https://developer.apple.com/documentation/appstoreserverapi/customerconsented\n    \"\"\"\n\n    sampleContentProvided: bool = attr.ib()\n    \"\"\"\n    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.\n\n    https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided\n    \"\"\"\n\n    deliveryStatus: Optional[DeliveryStatus] = DeliveryStatus.create_main_attr('rawDeliveryStatus', raw_required=True)\n    \"\"\"\n    A value that indicates whether the app successfully delivered an in-app purchase that works properly.\n\n    https://developer.apple.com/documentation/appstoreserverapi/deliverystatus\n    \"\"\"\n    \n    consumptionPercentage: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    An integer that indicates the percentage, in milliunits, of the In-App Purchase the customer consumed.\n\n    https://developer.apple.com/documentation/appstoreserverapi/consumptionpercentage\n    \"\"\"\n\n    rawDeliveryStatus: str = DeliveryStatus.create_raw_attr('deliveryStatus', required=True)\n    \"\"\"\n    See deliveryStatus\n    \"\"\"\n\n    refundPreference: Optional[RefundPreference] = RefundPreference.create_main_attr('rawRefundPreference')\n    \"\"\"\n    A value that indicates your preferred outcome for the refund request.\n\n    https://developer.apple.com/documentation/appstoreserverapi/refundpreference\n    \"\"\"\n\n    rawRefundPreference: Optional[str] = RefundPreference.create_raw_attr('refundPreference')\n    \"\"\"\n    See refundPreference\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/ConsumptionRequestReason.py",
    "content": "# Copyright (c) 2024 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass ConsumptionRequestReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The customer-provided reason for a refund request.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason\n    \"\"\"\n    UNINTENDED_PURCHASE = \"UNINTENDED_PURCHASE\"\n    FULFILLMENT_ISSUE = \"FULFILLMENT_ISSUE\"\n    UNSATISFIED_WITH_PURCHASE = \"UNSATISFIED_WITH_PURCHASE\"\n    LEGAL = \"LEGAL\"\n    OTHER = \"OTHER\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/ConsumptionRequestV1.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .AccountTenure import AccountTenure\nfrom .ConsumptionStatus import ConsumptionStatus\nfrom .DeliveryStatusV1 import DeliveryStatusV1\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .LifetimeDollarsPurchased import LifetimeDollarsPurchased\nfrom .LifetimeDollarsRefunded import LifetimeDollarsRefunded\nfrom .Platform import Platform\nfrom .PlayTime import PlayTime\nfrom .RefundPreferenceV1 import RefundPreferenceV1\nfrom .UserStatus import UserStatus\n\n@define\nclass ConsumptionRequestV1(AttrsRawValueAware):\n    \"\"\"\n    The request body containing consumption information.\n\n    .. deprecated::\n        Use :class:`ConsumptionRequest` instead.\n\n    https://developer.apple.com/documentation/appstoreserverapi/consumptionrequestv1\n    \"\"\"\n\n    customerConsented: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    A Boolean value that indicates whether the customer consented to provide consumption data to the App Store.\n\n    https://developer.apple.com/documentation/appstoreserverapi/customerconsented\n    \"\"\"\n\n    consumptionStatus: Optional[ConsumptionStatus] = ConsumptionStatus.create_main_attr('rawConsumptionStatus')\n    \"\"\"\n    A value that indicates the extent to which the customer consumed the in-app purchase.\n\n    https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus\n    \"\"\"\n\n    rawConsumptionStatus: Optional[int] = ConsumptionStatus.create_raw_attr('consumptionStatus')\n    \"\"\"\n    See consumptionStatus\n    \"\"\"\n\n    platform: Optional[Platform] = Platform.create_main_attr('rawPlatform')\n    \"\"\"\n    A value that indicates the platform on which the customer consumed the in-app purchase.\n\n    https://developer.apple.com/documentation/appstoreserverapi/platform\n    \"\"\"\n\n    rawPlatform: Optional[int] = Platform.create_raw_attr('platform')\n    \"\"\"\n    See platform\n    \"\"\"\n\n    sampleContentProvided: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    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.\n\n    https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided\n    \"\"\"\n\n    deliveryStatus: Optional[DeliveryStatusV1] = DeliveryStatusV1.create_main_attr('rawDeliveryStatus')\n    \"\"\"\n    A value that indicates whether the app successfully delivered an in-app purchase that works properly.\n\n    https://developer.apple.com/documentation/appstoreserverapi/deliverystatus\n    \"\"\"\n\n    rawDeliveryStatus: Optional[int] = DeliveryStatusV1.create_raw_attr('deliveryStatus')\n    \"\"\"\n    See deliveryStatus\n    \"\"\"\n\n    appAccountToken: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction.\n\n    https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken\n    \"\"\"\n\n    accountTenure: Optional[AccountTenure] = AccountTenure.create_main_attr('rawAccountTenure')\n    \"\"\"\n    The age of the customer's account.\n\n    https://developer.apple.com/documentation/appstoreserverapi/accounttenure\n    \"\"\"\n\n    rawAccountTenure: Optional[int] = AccountTenure.create_raw_attr('accountTenure')\n    \"\"\"\n    See accountTenure\n    \"\"\"\n\n    playTime: Optional[PlayTime] = PlayTime.create_main_attr('rawPlayTime')\n    \"\"\"\n    A value that indicates the amount of time that the customer used the app.\n\n    https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest\n    \"\"\"\n\n    rawPlayTime: Optional[int] = PlayTime.create_raw_attr('playTime')\n    \"\"\"\n    See playTime\n    \"\"\"\n\n    lifetimeDollarsRefunded: Optional[LifetimeDollarsRefunded] = LifetimeDollarsRefunded.create_main_attr('rawLifetimeDollarsRefunded')\n    \"\"\"\n    A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms.\n\n    https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded\n    \"\"\"\n\n    rawLifetimeDollarsRefunded: Optional[int] = LifetimeDollarsRefunded.create_raw_attr('lifetimeDollarsRefunded')\n    \"\"\"\n    See lifetimeDollarsRefunded\n    \"\"\"\n\n    lifetimeDollarsPurchased: Optional[LifetimeDollarsPurchased] = LifetimeDollarsPurchased.create_main_attr('rawLifetimeDollarsPurchased')\n    \"\"\"\n    A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms.\n\n    https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased\n    \"\"\"\n\n    rawLifetimeDollarsPurchased: Optional[int] = LifetimeDollarsPurchased.create_raw_attr('lifetimeDollarsPurchased')\n    \"\"\"\n    See lifetimeDollarsPurchased\n    \"\"\"\n\n    userStatus: Optional[UserStatus] =  UserStatus.create_main_attr('rawUserStatus')\n    \"\"\"\n    The status of the customer's account.\n\n    https://developer.apple.com/documentation/appstoreserverapi/userstatus\n    \"\"\"\n\n    rawUserStatus: Optional[int] = UserStatus.create_raw_attr('userStatus')\n    \"\"\"\n    See userStatus\n    \"\"\"\n\n    refundPreference: Optional[RefundPreferenceV1] =  RefundPreferenceV1.create_main_attr('rawRefundPreference')\n    \"\"\"\n    A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund.\n\n    https://developer.apple.com/documentation/appstoreserverapi/refundpreference\n    \"\"\"\n\n    rawRefundPreference: Optional[int] = RefundPreferenceV1.create_raw_attr('refundPreference')\n    \"\"\"\n    See refundPreference\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/ConsumptionStatus.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass ConsumptionStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates the extent to which the customer consumed the in-app purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus\n    \"\"\"\n    UNDECLARED = 0\n    NOT_CONSUMED = 1\n    PARTIALLY_CONSUMED = 2\n    FULLY_CONSUMED = 3\n"
  },
  {
    "path": "appstoreserverlibrary/models/Data.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .ConsumptionRequestReason import ConsumptionRequestReason\nfrom .Environment import Environment\nfrom .Status import Status\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass Data(AttrsRawValueAware):\n    \"\"\"\n    The app metadata and the signed renewal and transaction information.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/data\n    \"\"\"\n    environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment')\n    \"\"\"\n    The server environment that the notification applies to, either sandbox or production.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/environment\n    \"\"\"\n    rawEnvironment: Optional[str] = Environment.create_raw_attr('environment')\n    \"\"\"\n    See environment\n    \"\"\"\n\n    appAppleId: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of an app in the App Store.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/appappleid\n    \"\"\"\n\n    bundleId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The bundle identifier of an app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/bundleid\n    \"\"\"\n\n    bundleVersion: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The version of the build that identifies an iteration of the bundle.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/bundleversion\n    \"\"\"\n\n    signedTransactionInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    Transaction information signed by the App Store, in JSON Web Signature (JWS) format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n    \"\"\"\n\n    signedRenewalInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo\n    \"\"\"\n        \n    status: Optional[Status] = Status.create_main_attr('rawStatus')\n    \"\"\"\n    The status of an auto-renewable subscription as of the signedDate in the responseBodyV2DecodedPayload.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/status\n    \"\"\"\n\n    rawStatus: Optional[int] = Status.create_raw_attr('status')\n    \"\"\"\n    See status\n    \"\"\"\n\n    consumptionRequestReason: Optional[ConsumptionRequestReason] = ConsumptionRequestReason.create_main_attr('rawConsumptionRequestReason')\n    \"\"\"\n    The reason the customer requested the refund.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason\n    \"\"\"\n\n    rawConsumptionRequestReason: Optional[str] = ConsumptionRequestReason.create_raw_attr('consumptionRequestReason')\n    \"\"\"\n    See consumptionRequestReason\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/DecodedRealtimeRequestBody.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\nfrom .Environment import Environment\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass DecodedRealtimeRequestBody(AttrsRawValueAware):\n    \"\"\"\n    The decoded request body the App Store sends to your server to request a real-time retention message.\n\n    https://developer.apple.com/documentation/retentionmessaging/decodedrealtimerequestbody\n    \"\"\"\n\n    originalTransactionId: str = attr.ib()\n    \"\"\"\n    The original transaction identifier of the customer's subscription.\n\n    https://developer.apple.com/documentation/retentionmessaging/originaltransactionid\n    \"\"\"\n\n    appAppleId: int = attr.ib()\n    \"\"\"\n    The unique identifier of the app in the App Store.\n\n    https://developer.apple.com/documentation/retentionmessaging/appappleid\n    \"\"\"\n\n    productId: str = attr.ib()\n    \"\"\"\n    The unique identifier of the auto-renewable subscription.\n\n    https://developer.apple.com/documentation/retentionmessaging/productid\n    \"\"\"\n\n    userLocale: str = attr.ib()\n    \"\"\"\n    The device's locale.\n\n    https://developer.apple.com/documentation/retentionmessaging/locale\n    \"\"\"\n\n    requestIdentifier: UUID = attr.ib()\n    \"\"\"\n    A UUID the App Store server creates to uniquely identify each request.\n\n    https://developer.apple.com/documentation/retentionmessaging/requestidentifier\n    \"\"\"\n\n    signedDate: int = attr.ib()\n    \"\"\"\n    The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature (JWS) data.\n\n    https://developer.apple.com/documentation/retentionmessaging/signeddate\n    \"\"\"\n\n    environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment', raw_required=True)\n    \"\"\"\n    The server environment, either sandbox or production.\n\n    https://developer.apple.com/documentation/retentionmessaging/environment\n    \"\"\"\n\n    rawEnvironment: str = Environment.create_raw_attr('environment', required=True)\n    \"\"\"\n    See environment\n    \"\"\"\n\n"
  },
  {
    "path": "appstoreserverlibrary/models/DefaultConfigurationRequest.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\n@define\nclass DefaultConfigurationRequest:\n    \"\"\"\n    The request body that contains the default configuration information.\n\n    https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationrequest\n    \"\"\"\n\n    messageIdentifier: Optional[UUID] = attr.ib(default=None)\n    \"\"\"\n    The message identifier of the message to configure as a default message.\n\n    Note: In a future version, this field will become required.\n\n    https://developer.apple.com/documentation/retentionmessaging/messageidentifier\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/DefaultConfigurationResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\n@define\nclass DefaultConfigurationResponse:\n    \"\"\"\n    The response body that contains the default configuration information.\n\n    https://developer.apple.com/documentation/retentionmessaging/defaultconfigurationresponse\n    \"\"\"\n\n    messageIdentifier: UUID = attr.ib()\n    \"\"\"\n    The message identifier of the retention message you configured as a default.\n\n    https://developer.apple.com/documentation/retentionmessaging/messageidentifier\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/DeliveryStatus.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass DeliveryStatus(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates whether the app successfully delivered an in-app purchase that works properly.\n\n    https://developer.apple.com/documentation/appstoreserverapi/deliverystatus\n    \"\"\"\n    DELIVERED = \"DELIVERED\"\n    UNDELIVERED_QUALITY_ISSUE = \"UNDELIVERED_QUALITY_ISSUE\"\n    UNDELIVERED_WRONG_ITEM = \"UNDELIVERED_WRONG_ITEM\"\n    UNDELIVERED_SERVER_OUTAGE = \"UNDELIVERED_SERVER_OUTAGE\"\n    UNDELIVERED_OTHER = \"UNDELIVERED_OTHER\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/DeliveryStatusV1.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass DeliveryStatusV1(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates whether the app successfully delivered an in-app purchase that works properly.\n\n    .. deprecated::\n        Use :class:`DeliveryStatus` instead.\n\n    https://developer.apple.com/documentation/appstoreserverapi/deliverystatusv1\n    \"\"\"\n    DELIVERED_AND_WORKING_PROPERLY = 0\n    DID_NOT_DELIVER_DUE_TO_QUALITY_ISSUE = 1\n    DELIVERED_WRONG_ITEM = 2\n    DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE = 3\n    DID_NOT_DELIVER_DUE_TO_IN_GAME_CURRENCY_CHANGE = 4\n    DID_NOT_DELIVER_FOR_OTHER_REASON = 5\n"
  },
  {
    "path": "appstoreserverlibrary/models/Environment.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass Environment(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The server environment, either sandbox or production.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/environment\n    \"\"\"\n    SANDBOX = \"Sandbox\"\n    PRODUCTION = \"Production\"\n    XCODE = \"Xcode\"\n    LOCAL_TESTING = \"LocalTesting\" # Used for unit testing\n"
  },
  {
    "path": "appstoreserverlibrary/models/ExpirationIntent.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass ExpirationIntent(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The reason an auto-renewable subscription expired.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/expirationintent\n    \"\"\"\n    CUSTOMER_CANCELLED = 1\n    BILLING_ERROR = 2\n    CUSTOMER_DID_NOT_CONSENT_TO_PRICE_INCREASE = 3\n    PRODUCT_NOT_AVAILABLE = 4\n    OTHER = 5\n"
  },
  {
    "path": "appstoreserverlibrary/models/ExtendReasonCode.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass ExtendReasonCode(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The code that represents the reason for the subscription-renewal-date extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode\n    \"\"\"\n    UNDECLARED = 0\n    CUSTOMER_SATISFACTION = 1\n    OTHER = 2\n    SERVICE_ISSUE_OR_OUTAGE = 3\n"
  },
  {
    "path": "appstoreserverlibrary/models/ExtendRenewalDateRequest.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .ExtendReasonCode import ExtendReasonCode\n\n@define\nclass ExtendRenewalDateRequest: \n    \"\"\"\n    The request body that contains subscription-renewal-extension data for an individual subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldaterequest\n    \"\"\"\n\n    extendByDays: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The number of days to extend the subscription renewal date.\n\n    https://developer.apple.com/documentation/appstoreserverapi/extendbydays\n    maximum: 90\n    \"\"\"\n\n    extendReasonCode: Optional[ExtendReasonCode] = attr.ib(default=None)\n    \"\"\"\n    The reason code for the subscription date extension\n    \n    https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode\n    \"\"\"\n\n    requestIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A string that contains a unique identifier you provide to track each subscription-renewal-date extension request.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/requestidentifier\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/ExtendRenewalDateResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass ExtendRenewalDateResponse: \n    \"\"\"\n    A response that indicates whether an individual renewal-date extension succeeded, and related details.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldateresponse\n    \"\"\"\n\n    originalTransactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The original transaction identifier of a purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid\n    \"\"\"\n\n    webOrderLineItemId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of subscription-purchase events across devices, including renewals.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid\n    \"\"\"\n\n    success: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    A Boolean value that indicates whether the subscription-renewal-date extension succeeded.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/success\n    \"\"\"\n\n    effectiveDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The new subscription expiration date for a subscription-renewal extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/effectivedate\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/ExternalPurchaseToken.py",
    "content": "# Copyright (c) 2024 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass ExternalPurchaseToken(AttrsRawValueAware):\n    \"\"\"\n    The payload data that contains an external purchase token.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken\n    \"\"\"\n\n    externalPurchaseId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The field of an external purchase token that uniquely identifies the token.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/externalpurchaseid\n    \"\"\"\n\n    tokenCreationDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The field of an external purchase token that contains the UNIX date, in milliseconds, when the system created the token.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/tokencreationdate\n    \"\"\"\n\n    appAppleId: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of an app in the App Store.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/appappleid\n    \"\"\"\n\n    bundleId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The bundle identifier of an app.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/bundleid\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/FirstSendAttemptResult.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum, unique\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\n@unique\nclass FirstSendAttemptResult(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    An error or result that the App Store server receives when attempting to send an App Store server notification to your server.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/firstsendattemptresult\n    \"\"\"\n    SUCCESS = \"SUCCESS\"\n    TIMED_OUT = \"TIMED_OUT\"\n    TLS_ISSUE = \"TLS_ISSUE\"\n    CIRCULAR_REDIRECT = \"CIRCULAR_REDIRECT\"\n    NO_RESPONSE = \"NO_RESPONSE\"\n    SOCKET_ISSUE = \"SOCKET_ISSUE\"\n    UNSUPPORTED_CHARSET = \"UNSUPPORTED_CHARSET\"\n    INVALID_RESPONSE = \"INVALID_RESPONSE\"\n    PREMATURE_CLOSE = \"PREMATURE_CLOSE\"\n    UNSUCCESSFUL_HTTP_RESPONSE_CODE = \"UNSUCCESSFUL_HTTP_RESPONSE_CODE\"\n    OTHER = \"OTHER\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/GetImageListResponse.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, List\n\nfrom attr import define\nimport attr\n\nfrom .GetImageListResponseItem import GetImageListResponseItem\n\n@define\nclass GetImageListResponse:\n    \"\"\"\n    A response that contains status information for all images.\n\n    https://developer.apple.com/documentation/retentionmessaging/getimagelistresponse\n    \"\"\"\n\n    imageIdentifiers: Optional[List[GetImageListResponseItem]] = attr.ib(default=None)\n    \"\"\"\n    An array of all image identifiers and their image state.\n\n    https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/GetImageListResponseItem.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\nfrom .ImageSize import ImageSize\nfrom .ImageState import ImageState\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass GetImageListResponseItem(AttrsRawValueAware):\n    \"\"\"\n    An image identifier and state information for an image.\n\n    https://developer.apple.com/documentation/retentionmessaging/getimagelistresponseitem\n    \"\"\"\n\n    imageIdentifier: Optional[UUID] = attr.ib(default=None)\n    \"\"\"\n    The identifier of the image.\n\n    https://developer.apple.com/documentation/retentionmessaging/imageidentifier\n    \"\"\"\n\n    imageState: Optional[ImageState] = ImageState.create_main_attr('rawImageState')\n    \"\"\"\n    The current state of the image.\n\n    https://developer.apple.com/documentation/retentionmessaging/imagestate\n    \"\"\"\n\n    rawImageState: Optional[str] = ImageState.create_raw_attr('imageState')\n    \"\"\"\n    See imageState\n    \"\"\"\n\n    imageSize: Optional[ImageSize] = ImageSize.create_main_attr('rawImageSize')\n    \"\"\"\n    The size of the image.\n\n    https://developer.apple.com/documentation/retentionmessaging/imagesize\n    \"\"\"\n\n    rawImageSize: Optional[str] = ImageSize.create_raw_attr('imageSize')\n    \"\"\"\n    See imageSize\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/GetMessageListResponse.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, List\n\nfrom attr import define\nimport attr\n\nfrom .GetMessageListResponseItem import GetMessageListResponseItem\n\n@define\nclass GetMessageListResponse:\n    \"\"\"\n    A response that contains status information for all messages.\n\n    https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponse\n    \"\"\"\n\n    messageIdentifiers: Optional[List[GetMessageListResponseItem]] = attr.ib(default=None)\n    \"\"\"\n    An array of all message identifiers and their message state.\n\n    https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/GetMessageListResponseItem.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\nfrom .MessageState import MessageState\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass GetMessageListResponseItem(AttrsRawValueAware):\n    \"\"\"\n    A message identifier and status information for a message.\n\n    https://developer.apple.com/documentation/retentionmessaging/getmessagelistresponseitem\n    \"\"\"\n\n    messageIdentifier: Optional[UUID] = attr.ib(default=None)\n    \"\"\"\n    The identifier of the message.\n\n    https://developer.apple.com/documentation/retentionmessaging/messageidentifier\n    \"\"\"\n\n    messageState: Optional[MessageState] = MessageState.create_main_attr('rawMessageState')\n    \"\"\"\n    The current state of the message.\n\n    https://developer.apple.com/documentation/retentionmessaging/messageState\n    \"\"\"\n\n    rawMessageState: Optional[str] = MessageState.create_raw_attr('messageState')\n    \"\"\"\n    See messageState\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/HeaderPosition.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass HeaderPosition(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The position where the header text appears in a message.\n\n    https://developer.apple.com/documentation/retentionmessaging/headerposition\n    \"\"\"\n    ABOVE_BODY = \"ABOVE_BODY\"\n    ABOVE_IMAGE = \"ABOVE_IMAGE\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/HistoryResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom typing import List, Optional\nimport attr\n\nfrom .Environment import Environment\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass HistoryResponse(AttrsRawValueAware):\n    \"\"\"\n    A response that contains the customer's transaction history for an app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/historyresponse\n    \"\"\"\n\n    revision: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A token you use in a query to request the next set of transactions for the customer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/revision\n    \"\"\"\n\n    hasMore: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    A Boolean value indicating whether the App Store has more transaction data.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/hasmore\n    \"\"\"\n\n    bundleId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The bundle identifier of an app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/bundleid\n    \"\"\"\n\n    appAppleId: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of an app in the App Store.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/appappleid\n    \"\"\"\n\n    environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment')\n    \"\"\"\n    The server environment in which you're making the request, whether sandbox or production.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/environment\n    \"\"\"\n\n    rawEnvironment: Optional[str] = Environment.create_raw_attr('environment')\n    \"\"\"\n    See environment\n    \"\"\"\n\n    signedTransactions: Optional[List[str]] = attr.ib(default=None)\n    \"\"\"\n    An array of in-app purchase transactions for the customer, signed by Apple, in JSON Web Signature format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/ImageSize.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass ImageSize(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The size of an image.\n\n    https://developer.apple.com/documentation/retentionmessaging/imagesize\n    \"\"\"\n    FULL_SIZE = \"FULL_SIZE\"\n    BULLET_POINT = \"BULLET_POINT\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/ImageState.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass ImageState(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The approval state of an image.\n\n    https://developer.apple.com/documentation/retentionmessaging/imagestate\n    \"\"\"\n    PENDING = \"PENDING\"\n    APPROVED = \"APPROVED\"\n    REJECTED = \"REJECTED\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/InAppOwnershipType.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass InAppOwnershipType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The relationship of the user with the family-shared purchase to which they have access.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype\n    \"\"\"\n    FAMILY_SHARED = \"FAMILY_SHARED\"\n    PURCHASED = \"PURCHASED\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/JWSRenewalInfoDecodedPayload.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import List, Optional\n\nfrom attr import define\nimport attr\nfrom .AutoRenewStatus import AutoRenewStatus\nfrom .Environment import Environment\n\nfrom .ExpirationIntent import ExpirationIntent\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .OfferType import OfferType\nfrom .PriceIncreaseStatus import PriceIncreaseStatus\nfrom .OfferDiscountType import OfferDiscountType\n\n@define\nclass JWSRenewalInfoDecodedPayload(AttrsRawValueAware):\n    \"\"\"\n    A decoded payload containing subscription renewal information for an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload\n    \"\"\"\n\n    expirationIntent: Optional[ExpirationIntent] = ExpirationIntent.create_main_attr('rawExpirationIntent')\n    \"\"\"\n    The reason the subscription expired.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/expirationintent\n    \"\"\"\n\n    rawExpirationIntent: Optional[int] = ExpirationIntent.create_raw_attr('expirationIntent')\n    \"\"\"\n    See expirationIntent\n    \"\"\"\n\n    originalTransactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The original transaction identifier of a purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid\n    \"\"\"\n\n    autoRenewProductId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The product identifier of the product that will renew at the next billing period.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/autorenewproductid\n    \"\"\"\n\n    productId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier for the product, that you create in App Store Connect.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/productid\n    \"\"\"\n\n    autoRenewStatus: Optional[AutoRenewStatus] = AutoRenewStatus.create_main_attr('rawAutoRenewStatus')\n    \"\"\"\n    The renewal status of the auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/autorenewstatus\n    \"\"\"\n\n    rawAutoRenewStatus: Optional[int] = AutoRenewStatus.create_raw_attr('autoRenewStatus')\n    \"\"\"\n    See autoRenewStatus\n    \"\"\"\n\n    isInBillingRetryPeriod: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    A Boolean value that indicates whether the App Store is attempting to automatically renew an expired subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/isinbillingretryperiod\n    \"\"\"\n\n    priceIncreaseStatus: Optional[PriceIncreaseStatus] = PriceIncreaseStatus.create_main_attr('rawPriceIncreaseStatus')\n    \"\"\"\n    The status that indicates whether the auto-renewable subscription is subject to a price increase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus\n    \"\"\"\n\n    rawPriceIncreaseStatus: Optional[int] = PriceIncreaseStatus.create_raw_attr('priceIncreaseStatus')\n    \"\"\"\n    See priceIncreaseStatus\n    \"\"\"\n\n    gracePeriodExpiresDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The time when the billing grace period for subscription renewals expires.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/graceperiodexpiresdate\n    \"\"\"\n\n    offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType')\n    \"\"\"\n    The type of subscription offer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offertype\n    \"\"\"\n\n    rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType')\n    \"\"\"\n    See offerType\n    \"\"\"\n\n    offerIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The offer code or the promotional offer identifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offeridentifier\n    \"\"\"\n\n    signedDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/signeddate\n    \"\"\"\n\n    environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment')\n    \"\"\"\n    The server environment, either sandbox or production.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/environment\n    \"\"\"\n\n    rawEnvironment: Optional[str] = Environment.create_raw_attr('environment')\n    \"\"\"\n    See environment\n    \"\"\"\n\n    recentSubscriptionStartDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/recentsubscriptionstartdate\n    \"\"\"\n\n    renewalDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The UNIX time, in milliseconds, that the most recent auto-renewable subscription purchase expires.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/renewaldate\n    \"\"\"\n\n    currency: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The currency code for the renewalPrice of the subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/currency\n    \"\"\"\n\n    renewalPrice: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/renewalprice\n    \"\"\"\n\n    offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType')\n    \"\"\"\n    The payment mode you configure for the offer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype\n    \"\"\"\n\n    rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType')\n    \"\"\"\n    See offerDiscountType\n    \"\"\"\n\n    eligibleWinBackOfferIds: Optional[List[str]] = attr.ib(default=None)\n    \"\"\"\n    An array of win-back offer identifiers that a customer is eligible to redeem, which sorts the identifiers to present the better offers first.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/eligiblewinbackofferids\n    \"\"\"\n\n    appAccountToken: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken\n    \"\"\"\n\n    appTransactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of the app download transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/appTransactionId\n    \"\"\"\n\n    offerPeriod: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The duration of the offer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offerPeriod\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/JWSTransactionDecodedPayload.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .OfferDiscountType import OfferDiscountType\nfrom .Environment import Environment\nfrom .InAppOwnershipType import InAppOwnershipType\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .OfferType import OfferType\nfrom .RevocationReason import RevocationReason\nfrom .RevocationType import RevocationType\nfrom .TransactionReason import TransactionReason\n\nfrom .Type import Type\n\n@define\nclass JWSTransactionDecodedPayload(AttrsRawValueAware):\n    \"\"\"\n    A decoded payload containing transaction information.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload\n    \"\"\"\n\n    originalTransactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The original transaction identifier of a purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid\n    \"\"\"\n\n    transactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier for a transaction such as an in-app purchase, restored in-app purchase, or subscription renewal.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/transactionid\n    \"\"\"\n\n    webOrderLineItemId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of subscription-purchase events across devices, including renewals.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/weborderlineitemid\n    \"\"\"\n\n    bundleId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The bundle identifier of an app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/bundleid\n    \"\"\"\n\n    productId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier for the product, that you create in App Store Connect.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/productid\n    \"\"\"    \n\n    subscriptionGroupIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The identifier of the subscription group that the subscription belongs to.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier\n    \"\"\"\n\n    purchaseDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/purchasedate\n    \"\"\"\n\n    originalPurchaseDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The purchase date of the transaction associated with the original transaction identifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originalpurchasedate\n    \"\"\"\n\n    expiresDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The UNIX time, in milliseconds, an auto-renewable subscription expires or renews.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/expiresdate\n    \"\"\"\n\n    quantity: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The number of consumable products purchased.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/quantity\n    \"\"\"\n\n    type: Optional[Type] = Type.create_main_attr('rawType')\n    \"\"\"\n    The type of the in-app purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/type\n    \"\"\"\n\n    rawType: Optional[str] = Type.create_raw_attr('type')\n    \"\"\"\n    See type\n    \"\"\"\n\n    appAccountToken: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken\n    \"\"\"\n\n    inAppOwnershipType: Optional[InAppOwnershipType] = InAppOwnershipType.create_main_attr('rawInAppOwnershipType')\n    \"\"\"\n    A string that describes whether the transaction was purchased by the user, or is available to them through Family Sharing.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype\n    \"\"\"\n\n    rawInAppOwnershipType: Optional[str] = InAppOwnershipType.create_raw_attr('inAppOwnershipType')\n    \"\"\"\n    See inAppOwnershipType\n    \"\"\"\n\n    signedDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/signeddate\n    \"\"\"\n\n    revocationReason: Optional[RevocationReason] = RevocationReason.create_main_attr('rawRevocationReason')\n    \"\"\"\n    The reason that the App Store refunded the transaction or revoked it from Family Sharing.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/revocationreason\n    \"\"\"\n\n    rawRevocationReason: Optional[int] = RevocationReason.create_raw_attr('revocationReason')\n    \"\"\"\n    See revocationReason\n    \"\"\"\n\n    revocationDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The UNIX time, in milliseconds, that Apple Support refunded a transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/revocationdate\n    \"\"\"    \n\n    isUpgraded: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    The Boolean value that indicates whether the user upgraded to another subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/isupgraded\n    \"\"\"\n\n    offerType: Optional[OfferType] = OfferType.create_main_attr('rawOfferType')\n    \"\"\"\n    A value that represents the promotional offer type.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offertype\n    \"\"\"\n\n    rawOfferType: Optional[int] = OfferType.create_raw_attr('offerType')\n    \"\"\"\n    See offerType\n    \"\"\"\n\n    offerIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The identifier that contains the offer code or the promotional offer identifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offeridentifier\n    \"\"\"\n\n    environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment')\n    \"\"\"\n    The server environment, either sandbox or production.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/environment\n    \"\"\"\n\n    rawEnvironment: Optional[str] = Environment.create_raw_attr('environment')\n    \"\"\"\n    See environment\n    \"\"\"\n\n    storefront: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The three-letter code that represents the country or region associated with the App Store storefront for the purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/storefront\n    \"\"\"\n\n    storefrontId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    An Apple-defined value that uniquely identifies the App Store storefront associated with the purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/storefrontid\n    \"\"\"\n\n    transactionReason: Optional[TransactionReason] = TransactionReason.create_main_attr('rawTransactionReason')\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/transactionreason\n    \"\"\"\n\n    rawTransactionReason: Optional[str] = TransactionReason.create_raw_attr('transactionReason')\n    \"\"\"\n    See transactionReason\n    \"\"\"\n\n    currency: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The three-letter ISO 4217 currency code for the price of the product.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/currency\n    \"\"\"\n\n    price: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The price, in milliunits, of the in-app purchase or subscription offer that you configured in App Store Connect.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/price\n    \"\"\"\n\n    offerDiscountType: Optional[OfferDiscountType] = OfferDiscountType.create_main_attr('rawOfferDiscountType')\n    \"\"\"\n    The payment mode you configure for the offer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype\n    \"\"\"\n\n    rawOfferDiscountType: Optional[str] = OfferDiscountType.create_raw_attr('offerDiscountType')\n    \"\"\"\n    See offerDiscountType\n    \"\"\"\n\n    appTransactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of the app download transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/appTransactionId\n    \"\"\"\n\n    offerPeriod: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The duration of the offer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offerPeriod\n    \"\"\"\n    revocationType: Optional[RevocationType] = RevocationType.create_main_attr('rawRevocationType')\n    \"\"\"\n    The type of the refund or revocation that applies to the transaction.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/revocationtype\n    \"\"\"\n\n    rawRevocationType: Optional[str] = RevocationType.create_raw_attr('revocationType')\n    \"\"\"\n    See revocationType\n    \"\"\"\n\n    revocationPercentage: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The percentage, in milliunits, of the transaction that the App Store has refunded or revoked.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/revocationpercentage\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/LastTransactionsItem.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .Status import Status\n\n@define\nclass LastTransactionsItem(AttrsRawValueAware):\n    \"\"\"\n    The most recent App Store-signed transaction information and App Store-signed renewal information for an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem\n    \"\"\"\n\n    status: Optional[Status] = Status.create_main_attr('rawStatus')\n    \"\"\"\n    The status of the auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/status\n    \"\"\"\n\n    rawStatus: Optional[int] = Status.create_raw_attr('status')\n    \"\"\"\n    See status\n    \"\"\"\n\n    originalTransactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The original transaction identifier of a purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid\n    \"\"\"\n\n    signedTransactionInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    Transaction information signed by the App Store, in JSON Web Signature (JWS) format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n    \"\"\"\n\n    signedRenewalInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    Subscription renewal information, signed by the App Store, in JSON Web Signature (JWS) format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/LibraryUtility.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import EnumMeta\nfrom functools import lru_cache\nfrom typing import Any, List, Type, TypeVar\nfrom uuid import UUID\n\nfrom attr import Attribute, has, ib, fields\nfrom cattr import override\nfrom cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override\nimport cattrs\n\nT = TypeVar('T')\n\nmetadata_key = 'correspondingFieldName'\nmetadata_type_key = 'typeOfField'\nmetadata_required_key = 'requiredField'\n\nclass AppStoreServerLibraryEnumMeta(EnumMeta):\n    def __contains__(c, val):\n        try:\n            c(val)\n        except ValueError:\n            return False\n        return True  \n    \n    def create_main_attr(c, raw_field_name: str, raw_required: bool = False) -> Any:\n        def value_set(self, _: Attribute, value: c):\n            newValue = value.value if value is not None else None\n            if raw_required and newValue is None:\n                raise ValueError(f\"{raw_field_name} cannot be set to None when field is required\")\n            if newValue != getattr(self, raw_field_name):\n                object.__setattr__(self, raw_field_name, newValue)\n            return value\n        return ib(default=None, on_setattr=value_set, metadata={metadata_key: raw_field_name, metadata_type_key: 'main'})\n\n    def create_raw_attr(c, field_name: str, required: bool = False) -> Any:\n        def value_set(self, _: Attribute, value: str):\n            if required and value is None:\n                raise ValueError(f\"raw{field_name[0].upper() + field_name[1:]} cannot be None\")\n            newValue = c(value) if value in c else None\n            if newValue != getattr(self, field_name):\n                object.__setattr__(self, field_name, newValue)\n            return value\n\n        def validate_not_none(instance, attribute, value):\n            if value is None:\n                raise ValueError(f\"{attribute.name} cannot be None\")\n\n        if required:\n            from attr import Factory\n            def factory(instance):\n                main_value = getattr(instance, field_name)\n                if main_value is not None:\n                    return main_value.value\n                raise ValueError(f\"Either {field_name} or raw{field_name[0].upper() + field_name[1:]} must be provided\")\n            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})\n        else:\n            return ib(default=None, kw_only=True, on_setattr=value_set, metadata={metadata_key: field_name, metadata_type_key: 'raw', metadata_required_key: False})\n    \nclass AttrsRawValueAware:\n    def __attrs_post_init__(self):\n        attr_fields: List[Attribute] = fields(type(self))\n        for attribute in attr_fields:\n            if metadata_type_key not in attribute.metadata or attribute.metadata[metadata_type_key] != 'raw':\n                continue\n            field: str = attribute.metadata.get(metadata_key)\n            rawField = 'raw' + field[0].upper() + field[1:]\n            rawValue = getattr(self, rawField)\n            value = getattr(self, field)\n            if rawValue is not None:\n                setattr(self, rawField, rawValue)\n            elif value is not None:\n                setattr(self, field, value)\n\n\n@lru_cache(maxsize=None)\ndef _get_cattrs_converter(destination_class: Type[T]) -> cattrs.Converter:\n    c = cattrs.Converter()\n\n    # Register UUID hooks to ensure lowercase serialization\n    c.register_unstructure_hook(UUID, lambda uuid: str(uuid).lower() if uuid is not None else None)\n    c.register_structure_hook(UUID, lambda d, _: UUID(d) if d is not None else None)\n\n    # Need a function here because it must be a lambda based on cl, which is not always destination_class\n    def make_overrides(cl):\n        attributes: List[Attribute] = fields(cl)\n        cattrs_overrides = {}\n        # Use omit_if_default to prevent null fields from being serialized to JSON\n        for attribute in attributes:\n            if metadata_type_key in attribute.metadata:\n                matching_name: str = attribute.metadata[metadata_key]\n                if attribute.metadata[metadata_type_key] == 'raw':\n                    cattrs_overrides[matching_name] = override(omit=True)\n                    raw_field = 'raw' + matching_name[0].upper() + matching_name[1:]\n                    if attribute.metadata.get(metadata_required_key, False):\n                        cattrs_overrides[raw_field] = override(rename=matching_name)\n                    else:\n                        cattrs_overrides[raw_field] = override(rename=matching_name, omit_if_default=True)\n            elif attribute.default is None and attribute.name not in cattrs_overrides:\n                cattrs_overrides[attribute.name] = override(omit_if_default=True)\n        return cattrs_overrides\n\n    c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, **make_overrides(cl)))\n    c.register_unstructure_hook_factory(has, lambda cl: make_dict_unstructure_fn(cl, c, **make_overrides(cl)))\n    return c\n"
  },
  {
    "path": "appstoreserverlibrary/models/LifetimeDollarsPurchased.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass LifetimeDollarsPurchased(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased\n    \"\"\"\n    UNDECLARED = 0\n    ZERO_DOLLARS = 1\n    ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2\n    FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3\n    ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4\n    FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5\n    ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6\n    TWO_THOUSAND_DOLLARS_OR_GREATER = 7\n"
  },
  {
    "path": "appstoreserverlibrary/models/LifetimeDollarsRefunded.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass LifetimeDollarsRefunded(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates the dollar amount of refunds the customer has received in your app, since purchasing the app, across all platforms.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded\n    \"\"\"\n    UNDECLARED = 0\n    ZERO_DOLLARS = 1\n    ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 2\n    FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 3\n    ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 4\n    FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 5\n    ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS = 6\n    TWO_THOUSAND_DOLLARS_OR_GREATER = 7\n"
  },
  {
    "path": "appstoreserverlibrary/models/MassExtendRenewalDateRequest.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom typing import List, Optional\nimport attr\n\nfrom .ExtendReasonCode import ExtendReasonCode\n\n@define\nclass MassExtendRenewalDateRequest: \n    \"\"\"\n    The request body that contains subscription-renewal-extension data to apply for all eligible active subscribers.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldaterequest\n    \"\"\"\n\n    extendByDays: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The number of days to extend the subscription renewal date.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/extendbydays\n    maximum: 90\n    \"\"\"\n\n    extendReasonCode: Optional[ExtendReasonCode] = attr.ib(default=None)\n    \"\"\"\n    The reason code for the subscription-renewal-date extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/extendreasoncode\n    \"\"\"    \n\n    requestIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A string that contains a unique identifier you provide to track each subscription-renewal-date extension request.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/requestidentifier\n    \"\"\"\n\n    storefrontCountryCodes: Optional[List[str]] = attr.ib(default=None)\n    \"\"\"\n    A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes\n    \"\"\"\n\n    productId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier for the product, that you create in App Store Connect.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/productid\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/MassExtendRenewalDateResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass MassExtendRenewalDateResponse: \n    \"\"\"\n    A response that indicates the server successfully received the subscription-renewal-date extension request.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldateresponse\n    \"\"\"\n\n    requestIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A string that contains a unique identifier you provide to track each subscription-renewal-date extension request.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/requestidentifier\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/MassExtendRenewalDateStatusResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass MassExtendRenewalDateStatusResponse: \n    \"\"\"\n    A response that indicates the current status of a request to extend the subscription renewal date to all eligible subscribers.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/massextendrenewaldatestatusresponse\n    \"\"\"\n\n    requestIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A string that contains a unique identifier you provide to track each subscription-renewal-date extension request.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/requestidentifier\n    \"\"\"\n\n    complete: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    A Boolean value that indicates whether the App Store completed the request to extend a subscription renewal date to active subscribers.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/complete\n    \"\"\"\n\n    completeDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The UNIX time, in milliseconds, that the App Store completes a request to extend a subscription renewal date for eligible subscribers.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/completedate\n    \"\"\"\n\n    succeededCount: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The count of subscriptions that successfully receive a subscription-renewal-date extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/succeededcount\n    \"\"\"\n\n    failedCount: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The count of subscriptions that fail to receive a subscription-renewal-date extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/failedcount\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/Message.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\n@define\nclass Message:\n    \"\"\"\n    A message identifier you provide in a real-time response to your Get Retention Message endpoint.\n\n    https://developer.apple.com/documentation/retentionmessaging/message\n    \"\"\"\n\n    messageIdentifier: Optional[UUID] = attr.ib(default=None)\n    \"\"\"\n    The identifier of the message to display to the customer.\n\n    https://developer.apple.com/documentation/retentionmessaging/messageidentifier\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/MessageState.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass MessageState(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The approval state of the message.\n\n    https://developer.apple.com/documentation/retentionmessaging/messagestate\n    \"\"\"\n    PENDING = \"PENDING\"\n    APPROVED = \"APPROVED\"\n    REJECTED = \"REJECTED\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/NotificationHistoryRequest.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nfrom attr import define\nimport attr\n\nfrom .NotificationTypeV2 import NotificationTypeV2\nfrom .Subtype import Subtype\n\n@define\nclass NotificationHistoryRequest: \n    \"\"\"\n    The request body for notification history.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryrequest\n    \"\"\"\n\n    startDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/startdate\n    \"\"\"\n\n    endDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/enddate\n    \"\"\"\n\n    notificationType: Optional[NotificationTypeV2] = attr.ib(default=None)\n    \"\"\"\n    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.\n    Include either the transactionId or the notificationType in your query, but not both.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/notificationtype\n    \"\"\"\n\n    notificationSubtype: Optional[Subtype] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/notificationsubtype\n    \"\"\"\n\n    transactionId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    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.\n    Include either the transactionId or the notificationType in your query, but not both.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/transactionid\n    \"\"\"\n\n    onlyFailures: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/onlyfailures\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/NotificationHistoryResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional, List\n\nfrom attr import define\nimport attr\n\nfrom .NotificationHistoryResponseItem import NotificationHistoryResponseItem\n\n@define\nclass NotificationHistoryResponse: \n    \"\"\"\n    A response that contains the App Store Server Notifications history for your app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponse\n    \"\"\"\n\n    paginationToken: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A pagination token that you return to the endpoint on a subsequent call to receive the next set of results.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/paginationtoken\n    \"\"\"\n\n    hasMore: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    A Boolean value indicating whether the App Store has more transaction data.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/hasmore\n    \"\"\"\n\n    notificationHistory: Optional[List[NotificationHistoryResponseItem]] = attr.ib(default=None)\n    \"\"\"\n    An array of App Store server notification history records.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/NotificationHistoryResponseItem.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional, List\n\nfrom attr import define\nimport attr\n\nfrom .SendAttemptItem import SendAttemptItem\n\n@define\nclass NotificationHistoryResponseItem: \n    \"\"\"\n    The App Store server notification history record, including the signed notification payload and the result of the server's first send attempt.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/notificationhistoryresponseitem\n    \"\"\"\n\n    signedPayload: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/signedpayload\n    \"\"\"\n\n    sendAttempts: Optional[List[SendAttemptItem]] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/NotificationTypeV2.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass NotificationTypeV2(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The type that describes the in-app purchase or external purchase event for which the App Store sends the version 2 notification.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/notificationtype\n    \"\"\"\n    SUBSCRIBED = \"SUBSCRIBED\"\n    DID_CHANGE_RENEWAL_PREF = \"DID_CHANGE_RENEWAL_PREF\"\n    DID_CHANGE_RENEWAL_STATUS = \"DID_CHANGE_RENEWAL_STATUS\"\n    OFFER_REDEEMED = \"OFFER_REDEEMED\"\n    DID_RENEW = \"DID_RENEW\"\n    EXPIRED = \"EXPIRED\"\n    DID_FAIL_TO_RENEW = \"DID_FAIL_TO_RENEW\"\n    GRACE_PERIOD_EXPIRED = \"GRACE_PERIOD_EXPIRED\"\n    PRICE_INCREASE = \"PRICE_INCREASE\"\n    REFUND = \"REFUND\"\n    REFUND_DECLINED = \"REFUND_DECLINED\"\n    CONSUMPTION_REQUEST = \"CONSUMPTION_REQUEST\"\n    RENEWAL_EXTENDED = \"RENEWAL_EXTENDED\"\n    REVOKE = \"REVOKE\"\n    TEST = \"TEST\"\n    RENEWAL_EXTENSION = \"RENEWAL_EXTENSION\"\n    REFUND_REVERSED = \"REFUND_REVERSED\"\n    EXTERNAL_PURCHASE_TOKEN = \"EXTERNAL_PURCHASE_TOKEN\"\n    ONE_TIME_CHARGE = \"ONE_TIME_CHARGE\"\n    RESCIND_CONSENT = \"RESCIND_CONSENT\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/OfferDiscountType.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass OfferDiscountType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The payment mode for a discount offer on an In-App Purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype\n    \"\"\"\n    FREE_TRIAL = \"FREE_TRIAL\"\n    PAY_AS_YOU_GO = \"PAY_AS_YOU_GO\"\n    PAY_UP_FRONT = \"PAY_UP_FRONT\"\n    ONE_TIME = \"ONE_TIME\""
  },
  {
    "path": "appstoreserverlibrary/models/OfferType.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass OfferType(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The type of offer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/offertype\n    \"\"\"\n    INTRODUCTORY_OFFER = 1\n    PROMOTIONAL_OFFER = 2\n    OFFER_CODE = 3\n    WIN_BACK_OFFER = 4\n"
  },
  {
    "path": "appstoreserverlibrary/models/OrderLookupResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom typing import List, Optional\nimport attr\n\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .OrderLookupStatus import OrderLookupStatus\n\n@define\nclass OrderLookupResponse(AttrsRawValueAware):\n    \"\"\"\n    A response that includes the order lookup status and an array of signed transactions for the in-app purchases in the order.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/orderlookupresponse\n    \"\"\"\n\n    status: Optional[OrderLookupStatus] = OrderLookupStatus.create_main_attr('rawStatus')\n    \"\"\"\n    The status that indicates whether the order ID is valid.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus\n    \"\"\"\n\n    rawStatus: Optional[int] = OrderLookupStatus.create_raw_attr('status')\n    \"\"\"\n    See status\n    \"\"\"\n\n    signedTransactions: Optional[List[str]] = attr.ib(default=None)\n    \"\"\"\n    An array of in-app purchase transactions that are part of order, signed by Apple, in JSON Web Signature format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/OrderLookupStatus.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass OrderLookupStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates whether the order ID in the request is valid for your app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/orderlookupstatus\n    \"\"\"\n    VALID = 0\n    INVALID = 1\n"
  },
  {
    "path": "appstoreserverlibrary/models/PerformanceTestConfig.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass PerformanceTestConfig:\n    \"\"\"\n    An object that enumerates the test configuration parameters.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestconfig\n    \"\"\"\n\n    maxConcurrentRequests: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The maximum number of concurrent requests the API allows.\n\n    https://developer.apple.com/documentation/retentionmessaging/maxconcurrentrequests\n    \"\"\"\n\n    totalRequests: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The total number of requests to make during the test.\n\n    https://developer.apple.com/documentation/retentionmessaging/totalrequests\n    \"\"\"\n\n    totalDuration: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The total duration of the test in milliseconds.\n\n    https://developer.apple.com/documentation/retentionmessaging/totalduration\n    \"\"\"\n\n    responseTimeThreshold: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The maximum time your server has to respond when the system calls your Get Retention Message endpoint in the sandbox environment.\n\n    https://developer.apple.com/documentation/retentionmessaging/responsetimethreshold\n    \"\"\"\n\n    successRateThreshold: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The success rate threshold percentage.\n\n    https://developer.apple.com/documentation/retentionmessaging/successratethreshold\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/PerformanceTestRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nimport attr\n\n@define\nclass PerformanceTestRequest:\n    \"\"\"\n    The request object you provide for a performance test that contains an original transaction identifier.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestrequest\n    \"\"\"\n\n    originalTransactionId: str = attr.ib()\n    \"\"\"\n    The original transaction identifier of an In-App Purchase you initiate in the sandbox environment, to use as the purchase for this test.\n\n    https://developer.apple.com/documentation/retentionmessaging/originaltransactionid\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/PerformanceTestResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .PerformanceTestConfig import PerformanceTestConfig\n\n@define\nclass PerformanceTestResponse:\n    \"\"\"\n    The performance test response object.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestresponse\n    \"\"\"\n\n    config: Optional[PerformanceTestConfig] = attr.ib(default=None)\n    \"\"\"\n    The performance test configuration object.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestconfig\n    \"\"\"\n\n    requestId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The performance test request identifier.\n\n    https://developer.apple.com/documentation/retentionmessaging/requestid\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/PerformanceTestResponseTimes.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass PerformanceTestResponseTimes:\n    \"\"\"\n    An object that describes test response times.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes\n    \"\"\"\n\n    average: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    Average response time in milliseconds.\n\n    https://developer.apple.com/documentation/retentionmessaging/average\n    \"\"\"\n\n    p50: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The 50th percentile response time in milliseconds.\n\n    https://developer.apple.com/documentation/retentionmessaging/p50\n    \"\"\"\n\n    p90: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The 90th percentile response time in milliseconds.\n\n    https://developer.apple.com/documentation/retentionmessaging/p90\n    \"\"\"\n\n    p95: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The 95th percentile response time in milliseconds.\n\n    https://developer.apple.com/documentation/retentionmessaging/p95\n    \"\"\"\n\n    p99: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The 99th percentile response time in milliseconds.\n\n    https://developer.apple.com/documentation/retentionmessaging/p99\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/PerformanceTestResultResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Dict, Optional\n\nfrom attr import define, Attribute\nimport attr\n\nfrom .LibraryUtility import AttrsRawValueAware, metadata_key, metadata_type_key\nfrom .PerformanceTestConfig import PerformanceTestConfig\nfrom .PerformanceTestResponseTimes import PerformanceTestResponseTimes\nfrom .PerformanceTestStatus import PerformanceTestStatus\nfrom .SendAttemptResult import SendAttemptResult\n\ndef _failures_value_set(self, _: Attribute, value: Optional[Dict[SendAttemptResult, int]]):\n    new_raw = {k.value: v for k, v in value.items()} if value is not None else None\n    if new_raw != getattr(self, 'rawFailures'):\n        object.__setattr__(self, 'rawFailures', new_raw)\n    return value\n\ndef _raw_failures_value_set(self, _: Attribute, value: Optional[Dict[str, int]]):\n    new_typed = {}\n    if value is not None:\n        for k, v in value.items():\n            if k in SendAttemptResult:\n                new_typed[SendAttemptResult(k)] = v\n    new_typed = new_typed if new_typed else None\n    if new_typed != getattr(self, 'failures'):\n        object.__setattr__(self, 'failures', new_typed)\n    return value\n\n@define\nclass PerformanceTestResultResponse(AttrsRawValueAware):\n    \"\"\"\n    An object the API returns that describes the performance test results.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestresultresponse\n    \"\"\"\n\n    config: Optional[PerformanceTestConfig] = attr.ib(default=None)\n    \"\"\"\n    A PerformanceTestConfig object that enumerates the test parameters.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestconfig\n    \"\"\"\n\n    target: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The target URL for the performance test.\n\n    https://developer.apple.com/documentation/retentionmessaging/target\n    \"\"\"\n\n    result: Optional[PerformanceTestStatus] = PerformanceTestStatus.create_main_attr('rawResult')\n    \"\"\"\n    A PerformanceTestStatus object that describes the overall result of the test.\n\n    https://developer.apple.com/documentation/retentionmessaging/performanceteststatus\n    \"\"\"\n\n    rawResult: Optional[str] = PerformanceTestStatus.create_raw_attr('result')\n    \"\"\"\n    See result\n    \"\"\"\n\n    successRate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    An integer that describes he success rate percentage of the performance test.\n\n    https://developer.apple.com/documentation/retentionmessaging/successrate\n    \"\"\"\n\n    numPending: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    An integer that describes the number of pending requests in the performance test.\n\n    https://developer.apple.com/documentation/retentionmessaging/numpending\n    \"\"\"\n\n    responseTimes: Optional[PerformanceTestResponseTimes] = attr.ib(default=None)\n    \"\"\"\n    A PerformanceTestResponseTimes object that enumerates the response times measured during the test.\n\n    https://developer.apple.com/documentation/retentionmessaging/performancetestresponsetimes\n    \"\"\"\n\n    failures: Optional[Dict[SendAttemptResult, int]] = attr.ib(default=None, on_setattr=_failures_value_set, metadata={metadata_key: 'rawFailures', metadata_type_key: 'main'})\n    \"\"\"\n    A map of server-to-server notification failure reasons and counts that represent the number of failures encountered during the performance test.\n\n    https://developer.apple.com/documentation/retentionmessaging/failures\n    \"\"\"\n\n    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'})\n    \"\"\"\n    See failures\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/PerformanceTestStatus.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass PerformanceTestStatus(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The status of the performance test.\n\n    https://developer.apple.com/documentation/retentionmessaging/performanceteststatus\n    \"\"\"\n    PENDING = \"PENDING\"\n    PASS = \"PASS\"\n    FAIL = \"FAIL\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/Platform.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass Platform(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The platform on which the customer consumed the in-app purchase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/platform\n    \"\"\"\n    UNDECLARED = 0\n    APPLE = 1\n    NON_APPLE = 2\n"
  },
  {
    "path": "appstoreserverlibrary/models/PlayTime.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass PlayTime(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates the amount of time that the customer used the app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/playtime\n    \"\"\"\n    UNDECLARED = 0\n    ZERO_TO_FIVE_MINUTES = 1\n    FIVE_TO_SIXTY_MINUTES = 2\n    ONE_TO_SIX_HOURS = 3\n    SIX_HOURS_TO_TWENTY_FOUR_HOURS = 4\n    ONE_DAY_TO_FOUR_DAYS = 5\n    FOUR_DAYS_TO_SIXTEEN_DAYS = 6\n    OVER_SIXTEEN_DAYS = 7\n"
  },
  {
    "path": "appstoreserverlibrary/models/PriceIncreaseStatus.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass PriceIncreaseStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The status that indicates whether an auto-renewable subscription is subject to a price increase.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/priceincreasestatus\n    \"\"\"\n    CUSTOMER_HAS_NOT_RESPONDED = 0\n    CUSTOMER_CONSENTED_OR_WAS_NOTIFIED_WITHOUT_NEEDING_CONSENT = 1\n"
  },
  {
    "path": "appstoreserverlibrary/models/PromotionalOffer.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\nfrom .PromotionalOfferSignatureV1 import PromotionalOfferSignatureV1\n\n@define\nclass PromotionalOffer:\n    \"\"\"\n    A promotional offer and message you provide in a real-time response to your Get Retention Message endpoint.\n\n    https://developer.apple.com/documentation/retentionmessaging/promotionaloffer\n    \"\"\"\n\n    messageIdentifier: Optional[UUID] = attr.ib(default=None)\n    \"\"\"\n    The identifier of the message to display to the customer, along with the promotional offer.\n\n    https://developer.apple.com/documentation/retentionmessaging/messageidentifier\n    \"\"\"\n\n    promotionalOfferSignatureV2: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The promotional offer signature in V2 format.\n\n    https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev2\n    \"\"\"\n\n    promotionalOfferSignatureV1: Optional[PromotionalOfferSignatureV1] = attr.ib(default=None)\n    \"\"\"\n    The promotional offer signature in V1 format.\n\n    https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/PromotionalOfferSignatureV1.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\n@define\nclass PromotionalOfferSignatureV1:\n    \"\"\"\n    The promotional offer signature you generate using an earlier signature version.\n\n    https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1\n    \"\"\"\n\n    encodedSignature: str = attr.ib()\n    \"\"\"\n    The Base64-encoded cryptographic signature you generate using the offer parameters.\n    \"\"\"\n\n    productId: str = attr.ib()\n    \"\"\"\n    The subscription's product identifier.\n\n    https://developer.apple.com/documentation/retentionmessaging/productid\n    \"\"\"\n\n    nonce: UUID = attr.ib()\n    \"\"\"\n    A one-time-use UUID antireplay value you generate.\n    \"\"\"\n\n    timestamp: int = attr.ib()\n    \"\"\"\n    The UNIX time, in milliseconds, when you generate the signature.\n    \"\"\"\n\n    keyId: str = attr.ib()\n    \"\"\"\n    A string that identifies the private key you use to generate the signature.\n    \"\"\"\n\n    offerIdentifier: str = attr.ib()\n    \"\"\"\n    The subscription offer identifier that you set up in App Store Connect.\n    \"\"\"\n\n    appAccountToken: Optional[UUID] = attr.ib(default=None)\n    \"\"\"\n    A UUID that you provide to associate with the transaction if the customer accepts the promotional offer.\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/PurchasePlatform.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass PurchasePlatform(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    Values that represent Apple platforms.\n    \n    https://developer.apple.com/documentation/storekit/appstore/platform\n    \"\"\"\n    IOS = \"iOS\"\n    MAC_OS = \"macOS\"\n    TV_OS = \"tvOS\"\n    VISION_OS = \"visionOS\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/RealtimeRequestBody.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional, Dict\n\nfrom attr import define\nimport attr\n\n@define\nclass RealtimeRequestBody:\n    \"\"\"\n    The request body the App Store server sends to your Get Retention Message endpoint.\n\n    https://developer.apple.com/documentation/retentionmessaging/realtimerequestbody\n    \"\"\"\n\n    signedPayload: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The payload in JSON Web Signature (JWS) format, signed by the App Store.\n\n    https://developer.apple.com/documentation/retentionmessaging/signedpayload\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/RealtimeResponseBody.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .Message import Message\nfrom .AlternateProduct import AlternateProduct\nfrom .PromotionalOffer import PromotionalOffer\n\n@define\nclass RealtimeResponseBody:\n    \"\"\"\n    A response you provide to choose, in real time, a retention message the system displays to the customer.\n\n    https://developer.apple.com/documentation/retentionmessaging/realtimeresponsebody\n    \"\"\"\n\n    message: Optional[Message] = attr.ib(default=None)\n    \"\"\"\n    A retention message that's text-based and can include an optional image.\n\n    https://developer.apple.com/documentation/retentionmessaging/message\n    \"\"\"\n\n    alternateProduct: Optional[AlternateProduct] = attr.ib(default=None)\n    \"\"\"\n    A retention message with a switch-plan option.\n\n    https://developer.apple.com/documentation/retentionmessaging/alternateproduct\n    \"\"\"\n\n    promotionalOffer: Optional[PromotionalOffer] = attr.ib(default=None)\n    \"\"\"\n    A retention message that includes a promotional offer.\n\n    https://developer.apple.com/documentation/retentionmessaging/promotionaloffer\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/RealtimeUrlRequest.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nimport attr\n\n@define\nclass RealtimeUrlRequest:\n    \"\"\"\n    The request body for configuring the URL of your Get Retention Message endpoint.\n\n    https://developer.apple.com/documentation/retentionmessaging/realtimeurlrequest\n    \"\"\"\n\n    realtimeURL: str = attr.ib(validator=attr.validators.max_len(256))\n    \"\"\"\n    A string that contains the URL of your Get Retention Message endpoint for configuration.\n\n    https://developer.apple.com/documentation/retentionmessaging/realtimeurl\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/RealtimeUrlResponse.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass RealtimeUrlResponse:\n    \"\"\"\n    The response body that contains the URL for your Get Retention Message endpoint.\n\n    https://developer.apple.com/documentation/retentionmessaging/realtimeurlresponse\n    \"\"\"\n\n    realtimeURL: Optional[str] = attr.ib()\n    \"\"\"\n    A string that contains the URL you provided for your Get Retention Message endpoint.\n\n    https://developer.apple.com/documentation/retentionmessaging/realtimeurl\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/RefundHistoryResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom typing import List, Optional\nimport attr\n\n@define\nclass RefundHistoryResponse: \n    \"\"\"\n    A response that contains an array of signed JSON Web Signature (JWS) refunded transactions, and paging information.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/refundhistoryresponse\n    \"\"\"\n\n    signedTransactions: Optional[List[str]] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n    \"\"\"\n\n    revision: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A token you use in a query to request the next set of transactions for the customer.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/revision\n    \"\"\"\n\n    hasMore: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    A Boolean value indicating whether the App Store has more transaction data.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/hasmore\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/RefundPreference.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass RefundPreference(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund.\n\n    https://developer.apple.com/documentation/appstoreserverapi/refundpreference\n    \"\"\"\n    DECLINE = \"DECLINE\"\n    GRANT_FULL = \"GRANT_FULL\"\n    GRANT_PRORATED = \"GRANT_PRORATED\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/RefundPreferenceV1.py",
    "content": "# Copyright (c) 2024 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass RefundPreferenceV1(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A value that indicates your preferred outcome for the refund request.\n\n    .. deprecated::\n        Use :class:`RefundPreference` instead.\n\n    https://developer.apple.com/documentation/appstoreserverapi/refundpreferencev1\n    \"\"\"\n    UNDECLARED = 0\n    PREFER_GRANT = 1\n    PREFER_DECLINE = 2\n    NO_PREFERENCE = 3\n"
  },
  {
    "path": "appstoreserverlibrary/models/ResponseBodyV2.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass ResponseBodyV2:\n    \"\"\"\n    The response body the App Store sends in a version 2 server notification.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2\n    \"\"\"\n     \n    signedPayload: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A cryptographically signed payload, in JSON Web Signature (JWS) format, containing the response body for a version 2 notification.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/signedpayload\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/ResponseBodyV2DecodedPayload.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .AppData import AppData\nfrom .Data import Data\nfrom .ExternalPurchaseToken import ExternalPurchaseToken\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .NotificationTypeV2 import NotificationTypeV2\nfrom .Subtype import Subtype\nfrom .Summary import Summary\n\n\n@define\nclass ResponseBodyV2DecodedPayload(AttrsRawValueAware):\n    \"\"\"\n    A decoded payload containing the version 2 notification data.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload\n    \"\"\"\n\n    notificationType: Optional[NotificationTypeV2] = NotificationTypeV2.create_main_attr('rawNotificationType')\n    \"\"\"\n    The in-app purchase event for which the App Store sends this version 2 notification.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/notificationtype\n    \"\"\"\n\n    rawNotificationType: Optional[str] = NotificationTypeV2.create_raw_attr('notificationType')\n    \"\"\"\n    See notificationType\n    \"\"\"\n\n    subtype: Optional[Subtype] = Subtype.create_main_attr('rawSubtype')\n    \"\"\"\n    Additional information that identifies the notification event. The subtype field is present only for specific version 2 notifications.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/subtype\n    \"\"\"\n    \n    rawSubtype: Optional[str] = Subtype.create_raw_attr('subtype')\n    \"\"\"\n    See subtype\n    \"\"\"\n\n    notificationUUID: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A unique identifier for the notification.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/notificationuuid\n    \"\"\"\n\n    data: Optional[Data] = attr.ib(default=None)\n    \"\"\"\n    The object that contains the app metadata and signed renewal and transaction information.\n    The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/data\n    \"\"\"\n\n    version: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A string that indicates the notification's App Store Server Notifications version number.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/version\n    \"\"\"\n\n    signedDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The UNIX time, in milliseconds, that the App Store signed the JSON Web Signature data.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/signeddate\n    \"\"\"\n\n    summary: Optional[Summary] = attr.ib(default=None)\n    \"\"\"\n    The summary data that appears when the App Store server completes your request to extend a subscription renewal date for eligible subscribers.\n    The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/summary\n    \"\"\"\n\n    externalPurchaseToken: Optional[ExternalPurchaseToken] = attr.ib(default=None)\n    \"\"\"\n    This field appears when the notificationType is EXTERNAL_PURCHASE_TOKEN.\n    The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken\n    \"\"\"\n    appData: Optional[AppData] = attr.ib(default=None)\n    \"\"\"\n    The object that contains the app metadata and signed app transaction information. This field appears when the notificationType is RESCIND_CONSENT.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/appdata\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/RevocationReason.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass RevocationReason(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The reason for a refunded transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/revocationreason\n    \"\"\"\n    REFUNDED_DUE_TO_ISSUE = 1\n    REFUNDED_FOR_OTHER_REASON = 0\n"
  },
  {
    "path": "appstoreserverlibrary/models/RevocationType.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass RevocationType(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The type of the refund or revocation that applies to the transaction.\n\n    https://developer.apple.com/documentation/appstoreservernotifications/revocationtype\n    \"\"\"\n    REFUND_FULL = \"REFUND_FULL\"\n    REFUND_PRORATED = \"REFUND_PRORATED\"\n    FAMILY_REVOKE = \"FAMILY_REVOKE\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/SendAttemptItem.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .SendAttemptResult import SendAttemptResult\n\n@define\nclass SendAttemptItem(AttrsRawValueAware):\n    \"\"\"\n    The success or error information and the date the App Store server records when it attempts to send a server notification to your server.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/sendattemptitem\n    \"\"\"\n\n    attemptDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The date the App Store server attempts to send a notification.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/attemptdate\n    \"\"\"\n\n    sendAttemptResult: Optional[SendAttemptResult] = SendAttemptResult.create_main_attr('rawSendAttemptResult')\n    \"\"\"\n    The success or error information the App Store server records when it attempts to send an App Store server notification to your server.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult\n    \"\"\"\n        \n    rawSendAttemptResult: Optional[str] = SendAttemptResult.create_raw_attr('sendAttemptResult')\n    \"\"\"\n    See sendAttemptResult\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/SendAttemptResult.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass SendAttemptResult(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The success or error information the App Store server records when it attempts to send an App Store server notification to your server.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/sendattemptresult\n    \"\"\"\n    SUCCESS = \"SUCCESS\"\n    TIMED_OUT = \"TIMED_OUT\"\n    TLS_ISSUE = \"TLS_ISSUE\"\n    CIRCULAR_REDIRECT = \"CIRCULAR_REDIRECT\"\n    NO_RESPONSE = \"NO_RESPONSE\"\n    SOCKET_ISSUE = \"SOCKET_ISSUE\"\n    UNSUPPORTED_CHARSET = \"UNSUPPORTED_CHARSET\"\n    INVALID_RESPONSE = \"INVALID_RESPONSE\"\n    PREMATURE_CLOSE = \"PREMATURE_CLOSE\"\n    UNSUCCESSFUL_HTTP_RESPONSE_CODE = \"UNSUCCESSFUL_HTTP_RESPONSE_CODE\"\n    OTHER = \"OTHER\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/SendTestNotificationResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass SendTestNotificationResponse: \n    \"\"\"\n    A response that contains the test notification token.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/sendtestnotificationresponse\n    \"\"\"\n\n    testNotificationToken: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A unique identifier for a notification test that the App Store server sends to your server.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/testnotificationtoken\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/Status.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass Status(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The status of an auto-renewable subscription.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/status\n    \"\"\"\n    ACTIVE = 1\n    EXPIRED = 2\n    BILLING_RETRY = 3\n    BILLING_GRACE_PERIOD = 4\n    REVOKED = 5\n"
  },
  {
    "path": "appstoreserverlibrary/models/StatusResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional, List\n\nfrom attr import define\nimport attr\n\nfrom .Environment import Environment\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem\n\n@define\nclass StatusResponse(AttrsRawValueAware):\n    \"\"\"\n    A response that contains status information for all of a customer's auto-renewable subscriptions in your app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/statusresponse\n    \"\"\"\n\n    environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment')\n    \"\"\"\n    The server environment, sandbox or production, in which the App Store generated the response.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/environment\n    \"\"\"\n            \n    rawEnvironment: Optional[str] = Environment.create_raw_attr('environment')\n    \"\"\"\n    See environment\n    \"\"\"\n\n    bundleId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The bundle identifier of an app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/bundleid\n    \"\"\"\n\n    appAppleId: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of an app in the App Store.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/appappleid\n    \"\"\"\n\n    data: Optional[List[SubscriptionGroupIdentifierItem]] = attr.ib(default=None)\n    \"\"\"\n    An array of information for auto-renewable subscriptions, including App Store-signed transaction information and App Store-signed renewal information.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/SubscriptionGroupIdentifierItem.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional, List\n\nfrom attr import define\nimport attr\n\nfrom .LastTransactionsItem import LastTransactionsItem\n\n@define\nclass SubscriptionGroupIdentifierItem: \n    \"\"\"\n    Information for auto-renewable subscriptions, including signed transaction information and signed renewal information, for one subscription group.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifieritem\n    \"\"\"\n\n    subscriptionGroupIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The identifier of the subscription group that the subscription belongs to.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier\n    \"\"\"    \n\n    lastTransactions: Optional[List[LastTransactionsItem]] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/lasttransactionsitem\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/Subtype.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass Subtype(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    A string that provides details about select notification types in version 2.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/subtype\n    \"\"\"\n    INITIAL_BUY = \"INITIAL_BUY\"\n    RESUBSCRIBE = \"RESUBSCRIBE\"\n    DOWNGRADE = \"DOWNGRADE\"\n    UPGRADE = \"UPGRADE\"\n    AUTO_RENEW_ENABLED = \"AUTO_RENEW_ENABLED\"\n    AUTO_RENEW_DISABLED = \"AUTO_RENEW_DISABLED\"\n    VOLUNTARY = \"VOLUNTARY\"\n    BILLING_RETRY = \"BILLING_RETRY\"\n    PRICE_INCREASE = \"PRICE_INCREASE\"\n    GRACE_PERIOD = \"GRACE_PERIOD\"\n    PENDING = \"PENDING\"\n    ACCEPTED = \"ACCEPTED\"\n    BILLING_RECOVERY = \"BILLING_RECOVERY\"\n    PRODUCT_NOT_FOR_SALE = \"PRODUCT_NOT_FOR_SALE\"\n    SUMMARY = \"SUMMARY\"\n    FAILURE = \"FAILURE\"\n    UNREPORTED = \"UNREPORTED\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/Summary.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nfrom typing import List, Optional\nimport attr\n\nfrom .Environment import Environment\nfrom .LibraryUtility import AttrsRawValueAware\n\n@define\nclass Summary(AttrsRawValueAware):\n    \"\"\"\n    The payload data for a subscription-renewal-date extension notification.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/summary\n    \"\"\"\n\n    environment: Optional[Environment] = Environment.create_main_attr('rawEnvironment')\n    \"\"\"\n    The server environment that the notification applies to, either sandbox or production.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/environment\n    \"\"\"\n           \n    rawEnvironment: Optional[str] = Environment.create_raw_attr('environment')\n    \"\"\"\n    See environment\n    \"\"\"\n\n    appAppleId: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier of an app in the App Store.\n    \n    https://developer.apple.com/documentation/appstoreservernotifications/appappleid\n    \"\"\"\n\n    bundleId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The bundle identifier of an app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/bundleid\n    \"\"\"\n\n    productId: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    The unique identifier for the product, that you create in App Store Connect.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/productid\n    \"\"\"    \n\n    requestIdentifier: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A string that contains a unique identifier you provide to track each subscription-renewal-date extension request.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/requestidentifier\n    \"\"\"\n\n    storefrontCountryCodes: Optional[List[str]] = attr.ib(default=None)\n    \"\"\"\n    A list of storefront country codes you provide to limit the storefronts for a subscription-renewal-date extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/storefrontcountrycodes\n    \"\"\"\n\n    succeededCount: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The count of subscriptions that successfully receive a subscription-renewal-date extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/succeededcount\n    \"\"\"\n\n    failedCount: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    The count of subscriptions that fail to receive a subscription-renewal-date extension.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/failedcount\n    \"\"\""
  },
  {
    "path": "appstoreserverlibrary/models/TransactionHistoryRequest.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\nfrom typing import List, Optional\nimport attr\n\n\nfrom .InAppOwnershipType import InAppOwnershipType\n\nclass ProductType(str, Enum):\n    AUTO_RENEWABLE = \"AUTO_RENEWABLE\"\n    NON_RENEWABLE = \"NON_RENEWABLE\"\n    CONSUMABLE = \"CONSUMABLE\"\n    NON_CONSUMABLE = \"NON_CONSUMABLE\"\n\nclass Order(str, Enum):\n    ASCENDING = \"ASCENDING\"\n    DESCENDING = \"DESCENDING\"\n\n@attr.define\nclass TransactionHistoryRequest:\n    startDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/startdate\n    \"\"\"\n    \n    endDate: Optional[int] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/enddate\n    \"\"\"\n\n    productIds: Optional[List[str]] = attr.ib(default=None)\n    \"\"\"\n    An optional filter that indicates the product identifier to include in the transaction history. Your query may specify more than one productID.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/productid\n    \"\"\"    \n\n    productTypes: Optional[List[ProductType]] = attr.ib(default=None)\n    \"\"\"\n    An optional filter that indicates the product type to include in the transaction history. Your query may specify more than one productType.\n    \"\"\"\n    \n    sort: Optional[Order] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \"\"\"\n\n    subscriptionGroupIdentifiers: Optional[List[str]] = attr.ib(default=None)\n    \"\"\"\n    An optional filter that indicates the subscription group identifier to include in the transaction history. Your query may specify more than one subscriptionGroupIdentifier.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/subscriptiongroupidentifier\n    \"\"\"\n\n    inAppOwnershipType: Optional[InAppOwnershipType] = attr.ib(default=None)\n    \"\"\"\n    An optional filter that limits the transaction history by the in-app ownership type.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/inappownershiptype\n    \"\"\"\n   \n    revoked: Optional[bool] = attr.ib(default=None)\n    \"\"\"\n    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.\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/TransactionInfoResponse.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\nfrom typing import Optional\n\nfrom attr import define\nimport attr\n\n@define\nclass TransactionInfoResponse:\n    \"\"\"\n    A response that contains signed transaction information for a single transaction.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/transactioninforesponse\n    \"\"\"\n    \n    signedTransactionInfo: Optional[str] = attr.ib(default=None)\n    \"\"\"\n    A customer’s in-app purchase transaction, signed by Apple, in JSON Web Signature (JWS) format.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/TransactionReason.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass TransactionReason(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    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.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/transactionreason\n    \"\"\"\n    PURCHASE = \"PURCHASE\"\n    RENEWAL = \"RENEWAL\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/Type.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import Enum\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass Type(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The type of in-app purchase products you can offer in your app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/type\n    \"\"\"\n    AUTO_RENEWABLE_SUBSCRIPTION = \"Auto-Renewable Subscription\"\n    NON_CONSUMABLE = \"Non-Consumable\"\n    CONSUMABLE = \"Consumable\"\n    NON_RENEWING_SUBSCRIPTION =\"Non-Renewing Subscription\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/UpdateAppAccountTokenRequest.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\nfrom attr import define\nimport attr\n\n@define\nclass UpdateAppAccountTokenRequest:\n    \"\"\"\n    The request body that contains an app account token value.\n\n    https://developer.apple.com/documentation/appstoreserverapi/updateappaccounttokenrequest\n    \"\"\"\n\n    appAccountToken: str = attr.ib()\n    \"\"\"\n    The UUID that an app optionally generates to map a customer's in-app purchase with its resulting App Store transaction.\n\n    https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/UploadMessageImage.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom uuid import UUID\n\nfrom attr import define\nimport attr\n\n@define\nclass UploadMessageImage:\n    \"\"\"\n    The definition of an image with its alternative text.\n\n    https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage\n    \"\"\"\n\n    imageIdentifier: UUID = attr.ib()\n    \"\"\"\n    The unique identifier of an image.\n\n    https://developer.apple.com/documentation/retentionmessaging/imageidentifier\n    \"\"\"\n\n    altText: str = attr.ib(validator=attr.validators.max_len(150))\n    \"\"\"\n    The alternative text you provide for the corresponding image.\n\n    https://developer.apple.com/documentation/retentionmessaging/alttext\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/UploadMessageRequestBody.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom typing import List, Optional\n\nfrom attr import define\nimport attr\n\nfrom .BulletPoint import BulletPoint\nfrom .HeaderPosition import HeaderPosition\nfrom .LibraryUtility import AttrsRawValueAware\nfrom .UploadMessageImage import UploadMessageImage\n\n@define\nclass UploadMessageRequestBody(AttrsRawValueAware):\n    \"\"\"\n    The request body for uploading a message, which includes the message text and an optional image reference.\n\n    https://developer.apple.com/documentation/retentionmessaging/uploadmessagerequestbody\n    \"\"\"\n\n    header: str = attr.ib(validator=attr.validators.max_len(66))\n    \"\"\"\n    The header text of the retention message that the system displays to customers.\n\n    https://developer.apple.com/documentation/retentionmessaging/header\n    \"\"\"\n\n    body: str = attr.ib(validator=attr.validators.max_len(144))\n    \"\"\"\n    The body text of the retention message that the system displays to customers.\n\n    https://developer.apple.com/documentation/retentionmessaging/body\n    \"\"\"\n\n    image: Optional[UploadMessageImage] = attr.ib(default=None)\n    \"\"\"\n    The optional image identifier and its alternative text to appear as part of a text-based message with an image.\n\n    https://developer.apple.com/documentation/retentionmessaging/uploadmessageimage\n    \"\"\"\n\n    headerPosition: Optional[HeaderPosition] = HeaderPosition.create_main_attr('rawHeaderPosition')\n    \"\"\"\n    The position of header text, which defaults to placing header text above the body.\n\n    https://developer.apple.com/documentation/retentionmessaging/headerposition\n    \"\"\"\n\n    rawHeaderPosition: Optional[str] = HeaderPosition.create_raw_attr('headerPosition')\n    \"\"\"\n    See headerPosition\n    \"\"\"\n\n    bulletPoints: Optional[List[BulletPoint]] = attr.ib(default=None, validator=attr.validators.optional(attr.validators.max_len(5)))\n    \"\"\"\n    An optional array of bullet points.\n\n    https://developer.apple.com/documentation/retentionmessaging/bulletpoint\n    \"\"\"\n"
  },
  {
    "path": "appstoreserverlibrary/models/UserStatus.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom enum import IntEnum\n\nfrom .LibraryUtility import AppStoreServerLibraryEnumMeta\n\nclass UserStatus(IntEnum, metaclass=AppStoreServerLibraryEnumMeta):\n    \"\"\"\n    The status of a customer's account within your app.\n    \n    https://developer.apple.com/documentation/appstoreserverapi/userstatus\n    \"\"\"\n    UNDECLARED = 0\n    ACTIVE = 1\n    SUSPENDED = 2\n    TERMINATED = 3\n    LIMITED_ACCESS = 4\n"
  },
  {
    "path": "appstoreserverlibrary/models/__init__.py",
    "content": ""
  },
  {
    "path": "appstoreserverlibrary/promotional_offer.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.hashes import SHA256\nfrom cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurvePrivateKey\n\nimport uuid\nimport base64\n\nclass PromotionalOfferSignatureCreator:\n    _signing_key: EllipticCurvePrivateKey\n    _key_id: str\n    _bundle_id: str\n    def __init__(self, signing_key: bytes, key_id: str, bundle_id: str):\n       self._signing_key = serialization.load_pem_private_key(signing_key, password=None, backend=default_backend())\n       self._key_id = key_id\n       self._bundle_id = bundle_id\n    def create_signature(self, product_identifier: str, subscription_offer_id: str, application_username: str, nonce: uuid.UUID, timestamp: int):\n        \"\"\"\n        Return the Base64 encoded signature\n\n        https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers\n\n        :param product_identifier: The subscription product identifier\n        :param subscription_offer_id: The subscription discount identifier\n        :param application_username: An optional string value that you define; may be an empty string\n        :param nonce: A one-time UUID value that your server generates. Generate a new nonce for every signature.\n        :param timestamp: A timestamp your server generates in UNIX time format, in milliseconds. The timestamp keeps the offer active for 24 hours.\n        :return: The Base64 encoded signature\n        \"\"\"\n        payload = self._bundle_id + '\\u2063' + \\\n            self._key_id + '\\u2063' + \\\n            product_identifier + '\\u2063' + \\\n            subscription_offer_id + '\\u2063' + \\\n            application_username.lower()  + '\\u2063'+ \\\n            str(nonce).lower() + '\\u2063' + \\\n            str(timestamp)\n\n        return base64.b64encode(self._signing_key.sign(\n            payload.encode('utf-8'), ECDSA(SHA256())\n        )).decode('utf-8')\n"
  },
  {
    "path": "appstoreserverlibrary/py.typed",
    "content": ""
  },
  {
    "path": "appstoreserverlibrary/receipt_utility.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom base64 import b64decode\nfrom typing import Optional\n\nimport asn1\nimport base64\nimport re\n\nPKCS7_OID = \"1.2.840.113549.1.7.2\"\nIN_APP_ARRAY = 17\nTRANSACTION_IDENTIFIER = 1703\nORIGINAL_TRANSACTION_IDENTIFIER = 1705\n\nclass ReceiptUtility:\n    def _decode_octet_string(self, octet_string: bytes):\n        decoder = asn1.Decoder()\n        decoder.start(octet_string)\n        _, value = decoder.read()\n        return value\n\n    def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[str]:\n        \"\"\"\n        Extracts a transaction id from an encoded App Receipt. Throws if the receipt does not match the expected format.\n        *NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API.\n\n        :param appReceipt: The unmodified app receipt\n        :return: A transaction id from the array of in-app purchases, null if the receipt contains no in-app purchases\n        \"\"\"\n        try:\n            val = self._decode_octet_string(b64decode(app_receipt, validate=True))\n            found_oid = val[0]\n            if found_oid != PKCS7_OID:\n                raise ValueError()\n            inner_value = val[1][0][2][1][0]\n            # Xcode uses nested OctetStrings, we extract the inner string in this case\n            value = self._decode_octet_string(inner_value)\n            # We are in the top-level sequence, work our way to the array of in-apps\n            for inner_value in value:\n                if inner_value[0] == IN_APP_ARRAY:\n                    array_values = self._decode_octet_string(inner_value[2])\n                    # In-app array\n                    for array_value in array_values:\n                        if array_value[0] == TRANSACTION_IDENTIFIER or array_value[0] == ORIGINAL_TRANSACTION_IDENTIFIER:\n                            return self._decode_octet_string(array_value[2])\n            return None\n        except Exception as e:\n            raise ValueError(e)\n    \n    def extract_transaction_id_from_transaction_receipt(self, transaction_receipt: str) -> Optional[str]:\n        \"\"\"\n        Extracts a transaction id from an encoded transactional receipt. Throws if the receipt does not match the expected format.\n        *NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API.\n        :param transactionReceipt: The unmodified transactionReceipt\n        :return: A transaction id, or null if no transactionId is found in the receipt\n        \"\"\"\n        decoded_top_level = base64.b64decode(transaction_receipt).decode('utf-8')\n        matching_result = re.search(r'\"purchase-info\"\\s+=\\s+\"([a-zA-Z0-9+/=]+)\";', decoded_top_level)\n        if matching_result:\n            decoded_inner_level = base64.b64decode(matching_result.group(1)).decode('utf-8')\n            inner_matching_result = re.search(r'\"transaction-id\"\\s+=\\s+\"([a-zA-Z0-9+/=]+)\";', decoded_inner_level)\n            if inner_matching_result:\n                return inner_matching_result.group(1)\n        return None\n"
  },
  {
    "path": "appstoreserverlibrary/signed_data_verifier.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom typing import List, Optional, Dict\nfrom base64 import b64decode\nfrom enum import IntEnum\nimport time\nimport datetime\n\nimport asn1\nimport jwt\nimport requests\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import hashes, serialization\nfrom cryptography.hazmat.primitives.asymmetric.ec import ECDSA\nfrom cryptography.hazmat.primitives.hashes import SHA1, SHA256\nfrom cryptography.x509 import ocsp, oid\nfrom OpenSSL import crypto\n\nfrom appstoreserverlibrary.models.AppTransaction import AppTransaction\nfrom appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter\n\nfrom .models.DecodedRealtimeRequestBody import DecodedRealtimeRequestBody\nfrom .models.Environment import Environment\nfrom .models.ResponseBodyV2DecodedPayload import ResponseBodyV2DecodedPayload\nfrom .models.JWSTransactionDecodedPayload import JWSTransactionDecodedPayload\nfrom .models.JWSRenewalInfoDecodedPayload import JWSRenewalInfoDecodedPayload\n\nclass SignedDataVerifier:\n    \"\"\"\n    A class providing utility methods for verifying and decoding App Store signed data.\n    \"\"\"\n    def __init__(\n        self,\n        root_certificates: List[bytes],\n        enable_online_checks: bool,\n        environment: Environment,\n        bundle_id: str,\n        app_apple_id: Optional[int] = None,\n    ):\n        self._chain_verifier = _ChainVerifier(root_certificates)\n        self._environment = environment\n        self._bundle_id = bundle_id\n        self._app_apple_id = app_apple_id\n        self._enable_online_checks = enable_online_checks\n        if environment == Environment.PRODUCTION and app_apple_id is None:\n            raise ValueError(\"appAppleId is required when the environment is Production\")\n\n    def verify_and_decode_renewal_info(self, signed_renewal_info: str) -> JWSRenewalInfoDecodedPayload:\n        \"\"\"\n        Verifies and decodes a signedRenewalInfo obtained from the App Store Server API, an App Store Server Notification, or from a device\n        See https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo\n\n        :param signed_renewal_info: The signedRenewalInfo field\n        :return: The decoded renewal info after verification\n        :throws VerificationException: Thrown if the data could not be verified\n        \"\"\"\n        \n        decoded_renewal_info = _get_cattrs_converter(JWSRenewalInfoDecodedPayload).structure(self._decode_signed_object(signed_renewal_info), JWSRenewalInfoDecodedPayload)\n        if decoded_renewal_info.environment != self._environment:\n            raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT)\n        return decoded_renewal_info\n\n    def verify_and_decode_signed_transaction(self, signed_transaction: str) -> JWSTransactionDecodedPayload:\n        \"\"\"\n        Verifies and decodes a signedTransaction obtained from the App Store Server API, an App Store Server Notification, or from a device\n        See https://developer.apple.com/documentation/appstoreserverapi/jwstransaction\n\n        :param signed_transaction: The signedTransaction field\n        :return: The decoded transaction info after verification\n        :throws VerificationException: Thrown if the data could not be verified\n        \"\"\"\n        decoded_transaction_info = _get_cattrs_converter(JWSTransactionDecodedPayload).structure(self._decode_signed_object(signed_transaction), JWSTransactionDecodedPayload)\n        if decoded_transaction_info.bundleId != self._bundle_id:\n            raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)\n        if decoded_transaction_info.environment != self._environment:\n            raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT)\n        return decoded_transaction_info\n\n    def verify_and_decode_notification(self, signed_payload: str) -> ResponseBodyV2DecodedPayload:\n        \"\"\"\n        Verifies and decodes an App Store Server Notification signedPayload\n        See https://developer.apple.com/documentation/appstoreservernotifications/signedpayload\n\n        :param signedPayload: The payload received by your server\n        :return: The decoded payload after verification\n        :throws VerificationException: Thrown if the data could not be verified\n        \"\"\"\n        decoded_dict = self._decode_signed_object(signed_payload)\n        decoded_signed_notification = _get_cattrs_converter(ResponseBodyV2DecodedPayload).structure(decoded_dict, ResponseBodyV2DecodedPayload)\n        bundle_id = None\n        app_apple_id = None\n        environment = None\n        if decoded_signed_notification.data:\n            bundle_id = decoded_signed_notification.data.bundleId\n            app_apple_id = decoded_signed_notification.data.appAppleId\n            environment = decoded_signed_notification.data.environment\n        elif decoded_signed_notification.summary:\n            bundle_id = decoded_signed_notification.summary.bundleId\n            app_apple_id = decoded_signed_notification.summary.appAppleId\n            environment = decoded_signed_notification.summary.environment\n        elif decoded_signed_notification.externalPurchaseToken:\n            bundle_id = decoded_signed_notification.externalPurchaseToken.bundleId\n            app_apple_id = decoded_signed_notification.externalPurchaseToken.appAppleId\n            if decoded_signed_notification.externalPurchaseToken.externalPurchaseId and decoded_signed_notification.externalPurchaseToken.externalPurchaseId.startswith(\"SANDBOX\"):\n                environment = Environment.SANDBOX\n            else:\n                environment = Environment.PRODUCTION\n        elif decoded_signed_notification.appData:\n            bundle_id = decoded_signed_notification.appData.bundleId\n            app_apple_id = decoded_signed_notification.appData.appAppleId\n            environment = decoded_signed_notification.appData.environment\n        self._verify_notification(bundle_id, app_apple_id, environment)\n        return decoded_signed_notification\n\n    def _verify_notification(self, bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]):\n        if bundle_id != self._bundle_id or (self._environment == Environment.PRODUCTION and app_apple_id != self._app_apple_id):\n            raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)\n        if environment != self._environment:\n            raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT)\n\n    def verify_and_decode_app_transaction(self, signed_app_transaction: str) -> AppTransaction:\n        \"\"\"\n        Verifies and decodes a signed AppTransaction\n        See https://developer.apple.com/documentation/storekit/apptransaction\n\n        :param signed_app_transaction: The signed AppTransaction\n        :return: The decoded AppTransaction after validation\n        :throws VerificationException: Thrown if the data could not be verified\n        \"\"\"\n        decoded_dict = self._decode_signed_object(signed_app_transaction)\n        decoded_app_transaction = _get_cattrs_converter(AppTransaction).structure(decoded_dict, AppTransaction)\n        environment = decoded_app_transaction.receiptType\n        if decoded_app_transaction.bundleId != self._bundle_id or (self._environment == Environment.PRODUCTION and decoded_app_transaction.appAppleId != self._app_apple_id):\n            raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)\n        if environment != self._environment:\n            raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT)\n        return decoded_app_transaction\n\n    def verify_and_decode_realtime_request(self, signed_payload: str) -> DecodedRealtimeRequestBody:\n        \"\"\"\n        Verifies and decodes a Retention Messaging API signedPayload\n        See https://developer.apple.com/documentation/retentionmessaging/signedpayload\n\n        :param signedPayload: The payload received by your server\n        :return: The decoded payload after verification\n        :throws VerificationException: Thrown if the data could not be verified\n        \"\"\"\n        decoded_dict = self._decode_signed_object(signed_payload)\n        decoded_realtime_request = _get_cattrs_converter(DecodedRealtimeRequestBody).structure(decoded_dict, DecodedRealtimeRequestBody)\n        if self._environment == Environment.PRODUCTION and decoded_realtime_request.appAppleId != self._app_apple_id:\n            raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)\n        if decoded_realtime_request.environment != self._environment:\n            raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT)\n        return decoded_realtime_request\n\n    def _decode_signed_object(self, signed_obj: str) -> dict:\n        try:\n            decoded_jwt = jwt.decode(signed_obj, options={\"verify_signature\": False})\n            if self._environment == Environment.XCODE or self._environment == Environment.LOCAL_TESTING:\n                # Data is not signed by the App Store, and verification should be skipped\n                # The environment MUST be checked in the public method calling this\n                return decoded_jwt\n            unverified_headers: dict = jwt.get_unverified_header(signed_obj)\n            x5c_header: List[str] = unverified_headers.get(\"x5c\")\n            if x5c_header is None or len(x5c_header) == 0:\n                raise Exception(\"x5c claim was empty\")\n            algorithm_header: str = unverified_headers.get(\"alg\")\n            if algorithm_header is None or \"ES256\" != algorithm_header:\n                raise Exception(\"Algorithm was not ES256\")\n            signed_date = decoded_jwt.get('signedDate') if decoded_jwt.get('signedDate') is not None else decoded_jwt.get('receiptCreationDate')\n            effective_date = time.time() if self._enable_online_checks or signed_date is None else int(signed_date) // 1000\n            signing_key = self._chain_verifier.verify_chain(x5c_header, self._enable_online_checks, effective_date)\n            return jwt.decode(signed_obj, signing_key, algorithms=[\"ES256\"])\n        except VerificationException as e:\n            raise e\n        except Exception as e:\n            raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e\n\nclass _ChainVerifier:\n    MAXIMUM_CACHE_SIZE = 32 # There are unlikely to be more than a couple keys at once\n    CACHE_TIME_LIMIT = 15 * 60 # 15 minutes\n\n    def __init__(self, root_certificates: List[bytes], enable_strict_checks=True):\n        self.enable_strict_checks = enable_strict_checks\n        self.root_certificates = root_certificates\n        self.verified_certificates_cache: Dict[tuple[str, ...], (str, int)] = {}\n\n    def verify_chain(self, certificates: List[str], perform_online_checks: bool, effective_date: int) -> str:\n        if perform_online_checks and len(certificates) > 0:\n            cached_public_key = self.get_cached_public_key(certificates)\n            if cached_public_key is not None:\n                return cached_public_key\n        verified_public_key = self._verify_chain_without_caching(certificates=certificates, perform_online_checks=perform_online_checks, effective_date=effective_date)\n        if perform_online_checks:\n            self.put_verified_public_key(certificates, verified_public_key)\n        return verified_public_key\n    \n    def _verify_chain_without_caching(self, certificates: List[str], perform_online_checks: bool, effective_date: int) -> str:\n        if len(self.root_certificates) == 0:\n            raise VerificationException(VerificationStatus.INVALID_CERTIFICATE)\n        if len(certificates) != 3:\n            raise VerificationException(VerificationStatus.INVALID_CHAIN_LENGTH)\n        trusted_store = crypto.X509Store()\n        try:\n            for trusted_cert_bytes in self.root_certificates:\n                trusted_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, trusted_cert_bytes)\n                trusted_store.add_cert(trusted_cert)\n            if self.enable_strict_checks:\n                trusted_store.set_flags(crypto.X509StoreFlags.X509_STRICT)\n            leaf_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, b64decode(certificates[0], validate=True))\n            intermediate_cert = crypto.load_certificate(crypto.FILETYPE_ASN1, b64decode(certificates[1], validate=True))\n            verification_context = crypto.X509StoreContext(trusted_store, leaf_cert, [intermediate_cert])\n        except Exception as e:\n            raise VerificationException(VerificationStatus.INVALID_CERTIFICATE) from e\n\n        trusted_store.set_time(datetime.datetime.fromtimestamp(effective_date, tz=datetime.timezone.utc))\n        try:\n            verification_context.verify_certificate()\n            trusted_chain = verification_context.get_verified_chain()\n        except Exception as e:\n            raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e\n        self.check_oid(trusted_chain[0].to_cryptography(), \"1.2.840.113635.100.6.11.1\")\n        self.check_oid(trusted_chain[1].to_cryptography(), \"1.2.840.113635.100.6.2.1\")\n        if perform_online_checks:\n            self.check_ocsp_status(trusted_chain[1], trusted_chain[2], trusted_chain[2])\n            self.check_ocsp_status(trusted_chain[0], trusted_chain[1], trusted_chain[2])\n        return (\n            leaf_cert.to_cryptography()\n            .public_key()\n            .public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)\n            .decode()\n        )\n\n    def check_oid(self, cert: x509.Certificate, oid: str):\n        try:\n            cert.extensions.get_extension_for_oid(x509.ObjectIdentifier(oid))\n        except Exception as e:\n            raise VerificationException(VerificationStatus.VERIFICATION_FAILURE) from e\n\n    def check_ocsp_status(self, cert: crypto.X509, issuer: crypto.X509, root: crypto.X509):\n        builder = ocsp.OCSPRequestBuilder()\n        builder = builder.add_certificate(cert.to_cryptography(), issuer.to_cryptography(), SHA256())\n        req = builder.build()\n        authority_values = (\n            cert.to_cryptography()\n            .extensions.get_extension_for_oid(x509.oid.ExtensionOID.AUTHORITY_INFORMATION_ACCESS)\n            .value\n        )\n        ocsps = [val for val in authority_values if val.access_method == x509.oid.AuthorityInformationAccessOID.OCSP]\n        for o in ocsps:\n            try:\n                r = requests.post(\n                    o.access_location.value,\n                    headers={\"Content-Type\": \"application/ocsp-request\"},\n                    data=req.public_bytes(serialization.Encoding.DER),\n                    timeout=30,\n                )\n            except (requests.exceptions.RequestException, OSError) as e:\n                raise VerificationException(VerificationStatus.RETRYABLE_VERIFICATION_FAILURE) from e\n            if r.status_code != 200:\n                raise VerificationException(VerificationStatus.RETRYABLE_VERIFICATION_FAILURE)\n            else:\n                ocsp_resp = ocsp.load_der_ocsp_response(r.content)\n                if ocsp_resp.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL:\n                    certs = [issuer]\n                    for ocsp_cert in ocsp_resp.certificates:\n                        certs.append(crypto.X509.from_cryptography(ocsp_cert))\n                    # Find signing cert\n                    signing_cert = None\n                    for potential_signing_cert in certs:\n                        if ocsp_resp.responder_key_hash:\n                            subject_public_key_info = (\n                                potential_signing_cert.get_pubkey()\n                                .to_cryptography_key()\n                                .public_bytes(\n                                    encoding=serialization.Encoding.DER,\n                                    format=serialization.PublicFormat.SubjectPublicKeyInfo,\n                                )\n                            )\n                            decoder = asn1.Decoder()\n                            decoder.start(subject_public_key_info)\n                            decoder.enter()\n                            decoder.read()\n                            _, value = decoder.read()\n                            digest = hashes.Hash(SHA1())\n                            digest.update(value)\n                            if digest.finalize() == ocsp_resp.responder_key_hash:\n                                signing_cert = potential_signing_cert\n                                break\n\n                        elif ocsp_resp.responder_name:\n                            if ocsp_resp.responder_name == potential_signing_cert.subject.rfc4514_string():\n                                signing_cert = potential_signing_cert\n                                break\n                    if signing_cert is None:\n                        raise VerificationException(VerificationStatus.VERIFICATION_FAILURE)\n\n                    if signing_cert.to_cryptography().public_bytes(\n                        encoding=serialization.Encoding.DER\n                    ) == issuer.to_cryptography().public_bytes(encoding=serialization.Encoding.DER):\n                        # We trust this because it is the issuer\n                        pass\n                    else:\n                        trusted_store = crypto.X509Store()\n                        trusted_store.add_cert(issuer)\n                        trusted_store.add_cert(root)  # Apparently a full chain is always needed\n                        verification_context = crypto.X509StoreContext(trusted_store, signing_cert, [])\n                        verification_context.verify_certificate()\n                        if (\n                            oid.ExtendedKeyUsageOID.OCSP_SIGNING\n                            not in signing_cert.to_cryptography()\n                            .extensions.get_extension_for_class(x509.ExtendedKeyUsage)\n                            .value._usages\n                        ):\n                            raise VerificationException(VerificationStatus.VERIFICATION_FAILURE)\n\n                    # Confirm response is signed by signing_certificate\n                    signing_cert.to_cryptography().public_key().verify(\n                        ocsp_resp.signature, ocsp_resp.tbs_response_bytes, ECDSA(ocsp_resp.signature_hash_algorithm)\n                    )\n\n                    # Get the CertId\n                    for single_response in ocsp_resp.responses:\n                        # Get the cert ID with the provided hashing algorithm (using the request builder wrapper)\n                        builder = ocsp.OCSPRequestBuilder()\n                        builder = builder.add_certificate(\n                            cert.to_cryptography(), issuer.to_cryptography(), single_response.hash_algorithm\n                        )\n                        req = builder.build()\n                        if (\n                            single_response.certificate_status == ocsp.OCSPCertStatus.GOOD\n                            and single_response.serial_number == req.serial_number\n                            and single_response.issuer_key_hash == req.issuer_key_hash\n                            and single_response.issuer_name_hash == req.issuer_name_hash\n                        ):\n                            # Success\n                            return\n\n        raise VerificationException(VerificationStatus.VERIFICATION_FAILURE)\n    \n    def get_cached_public_key(self, certificates: List[str]) -> Optional[str]:\n        verified_public_key = self.verified_certificates_cache.get(tuple(certificates))\n        if verified_public_key is None:\n            return None\n        if verified_public_key[1] <= time.time():\n            return None\n        return verified_public_key[0]\n\n    def put_verified_public_key(self, certificates: List[str], verified_public_key: str):\n        cache_expiration = time.time() + _ChainVerifier.CACHE_TIME_LIMIT\n        self.verified_certificates_cache[tuple(certificates)] = (verified_public_key, cache_expiration)\n        if len(self.verified_certificates_cache) > _ChainVerifier.MAXIMUM_CACHE_SIZE:\n            for k, v in list(self.verified_certificates_cache.items()):\n                if v[1] <= time.time():\n                    del self.verified_certificates_cache[k]\n\nclass VerificationStatus(IntEnum):\n    OK = 0\n    VERIFICATION_FAILURE = 1\n    INVALID_APP_IDENTIFIER = 2\n    INVALID_CERTIFICATE = 3\n    INVALID_CHAIN_LENGTH = 4\n    INVALID_CHAIN = 5\n    INVALID_ENVIRONMENT = 6\n    RETRYABLE_VERIFICATION_FAILURE = 7\n\n\nclass VerificationException(Exception):\n    def __init__(self, status: VerificationStatus):\n        super().__init__(\"Verification failed with status \" + status.name)\n        self.status = status\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "sphinx == 9.1.0"
  },
  {
    "path": "pyproject.toml",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\n[build-system]\nrequires = [\"setuptools>=61.0\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"app-store-server-library\"\nversion = \"3.0.0\"\ndescription = \"The App Store Server Library\"\nreadme = {file = \"README.md\", content-type = \"text/markdown\"}\nlicense = {text = \"MIT\"}\nrequires-python = \">=3.7, <4\"\nclassifiers = [\n    \"License :: OSI Approved :: MIT License\"\n]\ndependencies = [\n    \"attrs>=21.3.0\",\n    \"PyJWT>=2.6.0,<3\",\n    \"requests>=2.28.0,<3\",\n    \"cryptography>=40.0.0\",\n    \"pyOpenSSL>=23.1.1\",\n    \"asn1==3.2.0\",\n    \"cattrs>=23.1.2\",\n]\n\n[project.optional-dependencies]\nasync = [\"httpx\"]\n\n[tool.setuptools]\npackages = {find = {exclude = [\"tests\"]}}\n\n[tool.setuptools.package-data]\nappstoreserverlibrary = [\"py.typed\"]\n"
  },
  {
    "path": "requirements.txt",
    "content": "attrs >= 21.3.0\nPyJWT >= 2.6.0, < 3\nrequests >= 2.28.0, < 3\ncryptography >= 40.0.0\npyOpenSSL >= 23.1.1\nasn1==3.2.0\ncattrs >= 23.1.2\nhttpx==0.28.1\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/resources/certs/testSigningKey.p8",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgSpP55ELdXswj9JRZ\nAPRwtTfS4CNRqpKIs+28rNHiPAqhRANCAASs8nLES7b+goKslppNVOurf0MonZdw\n3pb6TxS8Z/5j+UNY1sWK1ChxpuwNS9I3R50cfdQo/lA9PPhw6XIg8ytd\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/resources/mock_signed_data/legacyTransaction",
    "content": "ewoicHVyY2hhc2UtaW5mbyIgPSAiZXdvaWRISmhibk5oWTNScGIyNHRhV1FpSUQwZ0lqTXpPVGt6TXprNUlqc0tmUW89IjsKfQo="
  },
  {
    "path": "tests/resources/mock_signed_data/missingX5CHeaderClaim",
    "content": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1Y3dyb25nIjpbIk1JSUJvRENDQVVhZ0F3SUJBZ0lCRERBS0JnZ3Foa2pPUFFRREF6QkZNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1CNFhEVEl6TURFd05USXhNekV6TkZvWERUTXpNREV3TVRJeE16RXpORm93UFRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eERUQUxCZ05WQkFvTUJFeGxZV1l3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVRpdFlIRWFZVnVjOGc5QWpUT3dFck12R3lQeWtQYStwdXZUSThoSlRIWlpETEdhczJxWDErRXJ4Z1FUSmdWWHY3Nm5tTGhoUkpIK2oyNUFpQUk4aUdzb3k4d0xUQUpCZ05WSFJNRUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVFCZ29xaGtpRzkyTmtCZ3NCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05JQURCRkFpQlg0YytUMEZwNW5KNVFSQ2xSZnU1UFNCeVJ2TlB0dWFUc2swdlBCM1dBSUFJaEFOZ2FhdUFqL1lQOXMwQWtFaHlKaHhRTy82UTJ6b3VaK0gxQ0lPZWhuTXpRIiwiTUlJQm56Q0NBVVdnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQXpBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1CNFhEVEl6TURFd05USXhNekV3TlZvWERUTXpNREV3TVRJeE16RXdOVm93UlRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eEZUQVRCZ05WQkFvTURFbHVkR1Z5YldWa2FXRjBaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCQlVONVY5cktqZlJpTUFJb2pFQTBBdjVNcDBvRitPMGNMNGd6clRGMTc4aW5VSHVnajdFdDQ2TnJrUTdoS2dNVm5qb2dxNDVRMXJNcytjTUhWTklMV3FqTlRBek1BOEdBMVVkRXdRSU1BWUJBZjhDQVFBd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJBR0NpcUdTSWIzWTJRR0FnRUVBZ1VBTUFvR0NDcUdTTTQ5QkFNREEwZ0FNRVVDSVFDbXNJS1lzNDF1bGxzc0hYNHJWdmVVVDBaN0lzNS9oTEsxbEZQVHR1bjNoQUlnYzIrMlJHNStnTmNGVmNzK1hKZUVsNEdaK29qbDNST09tbGwreWU3ZHluUT0iLCJNSUlCZ2pDQ0FTbWdBd0lCQWdJSkFMVWM1QUxpSDVwYk1Bb0dDQ3FHU000OUJBTURNRFl4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh3SGhjTk1qTXdNVEExTWpFek1ESXlXaGNOTXpNd01UQXlNakV6TURJeVdqQTJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVjKy9CbCtnb3NwbzZ0ZjlaN2lvNXRkS2RybE4xWWRWbnFFaEVEWERTaHpkQUpQUWlqYW1YSU1IZjh4V1dUYTF6Z29ZVHhPS3BidUp0RHBsejFYcmlUYU1nTUI0d0RBWURWUjBUQkFVd0F3RUIvekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdDZ1lJS29aSXpqMEVBd01EUndBd1JBSWdlbVdRWG5NQWRUYWQySkRKV25nOVU0dUJCTDVtQTdXSTA1SDdvSDdjNmlRQ0lIaVJxTWpOZnpVQXlpdTloNnJPVS9LK2lUUjBJLzNZL05TV3NYSFgrYWNjIl19.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.1TFhjDR4WwQJNgizVGYXz3WE3ajxTdH1wKLQQ71MtrkadSxxOo3yPo_6L9Z03unIU7YK-NRNzSIb5bh5WqTprQ"
  },
  {
    "path": "tests/resources/mock_signed_data/renewalInfo",
    "content": "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJzaWduZWREYXRlIjoxNjcyOTU2MTU0MDAwfQ.FbK2OL-t6l4892W7fzWyus_g9mIl2CzWLbVt7Kgcnt6zzVulF8bzovgpe0v_y490blROGixy8KDoe2dSU53-Xw"
  },
  {
    "path": "tests/resources/mock_signed_data/testNotification",
    "content": "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQsImVudmlyb25tZW50IjoiU2FuZGJveCIsImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsInNpZ25lZERhdGUiOjE2ODEzMTQzMjQwMDAsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.VVXYwuNm2Y3XsOUva-BozqatRCsDuykA7xIe_CCRw6aIAAxJ1nb2sw871jfZ6dcgNhUuhoZ93hfbc1v_5zB7Og"
  },
  {
    "path": "tests/resources/mock_signed_data/transactionInfo",
    "content": "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlIiwic2lnbmVkRGF0ZSI6MTY3Mjk1NjE1NDAwMH0.PnHWpeIJZ8f2Q218NSGLo_aR0IBEJvC6PxmxKXh-qfYTrZccx2suGl223OSNAX78e4Ylf2yJCG2N-FfU-NIhZQ"
  },
  {
    "path": "tests/resources/mock_signed_data/wrongBundleId",
    "content": "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUud3JvbmcifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.WWE31hTB_mcv2O_lf-xI-MNY3d8txc0MzpqFx4QnYDfFIxB95Lo2Fm3r46YSjLLdL7xCWdEJrJP5bHgRCejAGg"
  },
  {
    "path": "tests/resources/models/advancedCommerceDescriptors.json",
    "content": "{\n  \"description\": \"description\",\n  \"displayName\": \"display name\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceOffer.json",
    "content": "{\n  \"period\": \"P1W\",\n  \"periodCount\": 3,\n  \"price\": 5000,\n  \"reason\": \"WIN_BACK\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceOneTimeChargeCreateRequest.json",
    "content": "{\n  \"currency\": \"USD\",\n  \"item\": {\n    \"description\": \"description\",\n    \"displayName\": \"display name\",\n    \"SKU\": \"sku\",\n    \"price\": 10000\n  },\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440000\"\n  },\n  \"taxCode\": \"taxCode\",\n  \"storefront\": \"USA\",\n  \"operation\": \"CREATE\",\n  \"version\": \"1.0\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceOneTimeChargeItem.json",
    "content": "{\n  \"description\": \"description\",\n  \"displayName\": \"display name\",\n  \"SKU\": \"sku\",\n  \"price\": 15000\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceRequestInfo.json",
    "content": "{\n  \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440010\",\n  \"appAccountToken\": \"660e8400-e29b-41d4-a716-446655440011\",\n  \"consistencyToken\": \"consistency_token_value\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceRequestRefundItem.json",
    "content": "{\n  \"SKU\": \"sku\",\n  \"refundReason\": \"LEGAL\",\n  \"refundType\": \"FULL\",\n  \"revoke\": true,\n  \"refundAmount\": 5000\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceRequestRefundRequest.json",
    "content": "{\n  \"items\": [\n    {\n      \"SKU\": \"sku\",\n      \"refundReason\": \"LEGAL\",\n      \"refundType\": \"FULL\",\n      \"revoke\": true\n    },\n    {\n      \"SKU\": \"sku\",\n      \"refundReason\": \"OTHER\",\n      \"refundType\": \"PRORATED\",\n      \"revoke\": false\n    }\n  ],\n  \"refundRiskingPreference\": true,\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440002\"\n  },\n  \"currency\": \"USD\",\n  \"storefront\": \"USA\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceRequestRefundResponse.json",
    "content": "{\n  \"signedTransactionInfo\": \"signed_transaction_info_value\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionCancelRequest.json",
    "content": "{\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440003\"\n  },\n  \"storefront\": \"USA\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionCancelResponse.json",
    "content": "{\n  \"signedRenewalInfo\": \"signed_renewal_info\",\n  \"signedTransactionInfo\": \"signed_transaction_info\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionChangeMetadataDescriptors.json",
    "content": "{\n  \"effective\": \"IMMEDIATELY\",\n  \"description\": \"description\",\n  \"displayName\": \"displayName\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionChangeMetadataItem.json",
    "content": "{\n  \"currentSKU\": \"currentSku\",\n  \"effective\": \"NEXT_BILL_CYCLE\",\n  \"description\": \"description\",\n  \"displayName\": \"displayName\",\n  \"SKU\": \"sku\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionChangeMetadataRequest.json",
    "content": "{\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440009\"\n  },\n  \"items\": [\n    {\n      \"currentSKU\": \"currentSKU\",\n      \"effective\": \"IMMEDIATELY\",\n      \"description\": \"description\",\n      \"displayName\": \"display name\",\n      \"SKU\": \"sku\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionChangeMetadataResponse.json",
    "content": "{\n  \"signedRenewalInfo\": \"signed_renewal_info\",\n  \"signedTransactionInfo\": \"signed_transaction_info\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionCreateItem.json",
    "content": "{\n  \"description\": \"description\",\n  \"displayName\": \"display name\",\n  \"SKU\": \"sku\",\n  \"price\": 20000\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionCreateRequest.json",
    "content": "{\n  \"currency\": \"USD\",\n  \"descriptors\": {\n    \"description\": \"description\",\n    \"displayName\": \"display name\"\n  },\n  \"items\": [\n    {\n      \"SKU\": \"sku\",\n      \"description\": \"description\",\n      \"displayName\": \"display name\",\n      \"price\": 20000\n    },\n    {\n      \"SKU\": \"sku\",\n      \"description\": \"description\",\n      \"displayName\": \"display name\",\n      \"price\": 30000\n    }\n  ],\n  \"period\": \"P1M\",\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440001\"\n  },\n  \"taxCode\": \"taxCode\",\n  \"storefront\": \"USA\",\n  \"previousTransactionId\": \"transactionId\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionMigrateDescriptors.json",
    "content": "{\n  \"description\": \"description\",\n  \"displayName\": \"displayName\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionMigrateItem.json",
    "content": "{\n  \"SKU\": \"sku\",\n  \"description\": \"description\",\n  \"displayName\": \"displayName\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionMigrateRenewalItem.json",
    "content": "{\n  \"SKU\": \"sku\",\n  \"description\": \"description\",\n  \"displayName\": \"displayName\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionMigrateRequest.json",
    "content": "{\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440006\"\n  },\n  \"descriptors\": {\n    \"description\": \"description\",\n    \"displayName\": \"display name\"\n  },\n  \"items\": [\n    {\n      \"SKU\": \"sku\",\n      \"description\": \"description\",\n      \"displayName\": \"display name\"\n    }\n  ],\n  \"targetProductId\": \"targetProductId\",\n  \"taxCode\": \"taxCode\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionMigrateResponse.json",
    "content": "{\n  \"signedRenewalInfo\": \"signed_renewal_info_value\",\n  \"signedTransactionInfo\": \"signed_transaction_info_value\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionModifyAddItem.json",
    "content": "{\n  \"SKU\": \"sku\",\n  \"description\": \"description\",\n  \"displayName\": \"displayName\",\n  \"price\": 12000\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionModifyChangeItem.json",
    "content": "{\n  \"currentSKU\": \"currentSku\",\n  \"description\": \"description\",\n  \"displayName\": \"displayName\",\n  \"effective\": \"IMMEDIATELY\",\n  \"price\": 13000,\n  \"reason\": \"UPGRADE\",\n  \"SKU\": \"sku\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionModifyDescriptors.json",
    "content": "{\n  \"effective\": \"IMMEDIATELY\",\n  \"description\": \"description\",\n  \"displayName\": \"displayName\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionModifyInAppRequest.json",
    "content": "{\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440007\"\n  },\n  \"transactionId\": \"transactionId\",\n  \"retainBillingCycle\": true,\n  \"descriptors\": {\n    \"effective\": \"IMMEDIATELY\",\n    \"description\": \"description\",\n    \"displayName\": \"display name\"\n  },\n  \"taxCode\": \"taxCode\",\n  \"currency\": \"USD\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionModifyPeriodChange.json",
    "content": "{\n  \"effective\": \"IMMEDIATELY\",\n  \"period\": \"P3M\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionModifyRemoveItem.json",
    "content": "{\n  \"SKU\": \"sku\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionPriceChangeItem.json",
    "content": "{\n  \"SKU\": \"sku\",\n  \"price\": 16000,\n  \"dependentSKUs\": [\"dependentSKU\"]\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionPriceChangeRequest.json",
    "content": "{\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440005\"\n  },\n  \"items\": [\n    {\n      \"SKU\": \"sku123\",\n      \"price\": 15000\n    }\n  ],\n  \"currency\": \"USD\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionPriceChangeResponse.json",
    "content": "{\n  \"signedRenewalInfo\": \"signed_renewal_info\",\n  \"signedTransactionInfo\": \"signed_transaction_info\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionReactivateInAppRequest.json",
    "content": "{\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440008\"\n  },\n  \"transactionId\": \"transactionId\",\n  \"items\": [\n    {\n      \"SKU\": \"sku\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionReactivateItem.json",
    "content": "{\n  \"SKU\": \"sku\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionRevokeRequest.json",
    "content": "{\n  \"requestInfo\": {\n    \"requestReferenceId\": \"550e8400-e29b-41d4-a716-446655440004\"\n  },\n  \"refundRiskingPreference\": true,\n  \"refundReason\": \"LEGAL\",\n  \"refundType\": \"FULL\",\n  \"storefront\": \"USA\"\n}\n"
  },
  {
    "path": "tests/resources/models/advancedCommerceSubscriptionRevokeResponse.json",
    "content": "{\n  \"signedRenewalInfo\": \"signed_renewal_info\",\n  \"signedTransactionInfo\": \"signed_transaction_info\"\n}\n"
  },
  {
    "path": "tests/resources/models/apiException.json",
    "content": "{\n  \"errorCode\": 5000000,\n  \"errorMessage\": \"An unknown error occurred.\"\n}"
  },
  {
    "path": "tests/resources/models/apiTooManyRequestsException.json",
    "content": "{\n  \"errorCode\": 4290000,\n  \"errorMessage\": \"Rate limit exceeded.\"\n}"
  },
  {
    "path": "tests/resources/models/apiUnknownError.json",
    "content": "{\n  \"errorCode\": 9990000,\n  \"errorMessage\": \"Testing error.\"\n}"
  },
  {
    "path": "tests/resources/models/appData.json",
    "content": "{\n  \"appAppleId\": 987654321,\n  \"bundleId\": \"com.example\",\n  \"environment\": \"Sandbox\",\n  \"signedAppTransactionInfo\": \"signed-app-transaction-info\"\n}"
  },
  {
    "path": "tests/resources/models/appTransaction.json",
    "content": "{\n  \"receiptType\": \"LocalTesting\",\n  \"appAppleId\": 531412,\n  \"bundleId\": \"com.example\",\n  \"applicationVersion\": \"1.2.3\",\n  \"versionExternalIdentifier\": 512,\n  \"receiptCreationDate\": 1698148900000,\n  \"originalPurchaseDate\": 1698148800000,\n  \"originalApplicationVersion\": \"1.1.2\",\n  \"deviceVerification\": \"device_verification_value\",\n  \"deviceVerificationNonce\": \"48ccfa42-7431-4f22-9908-7e88983e105a\",\n  \"preorderDate\": 1698148700000,\n  \"appTransactionId\": \"71134\",\n  \"originalPlatform\": \"iOS\"\n}"
  },
  {
    "path": "tests/resources/models/appTransactionDoesNotExistError.json",
    "content": "{\n   \"errorCode\": 4040019,\n   \"errorMessage\": \"No AppTransaction exists for the customer.\"\n}"
  },
  {
    "path": "tests/resources/models/appTransactionInfoResponse.json",
    "content": "{\n    \"signedAppTransactionInfo\": \"signed_app_transaction_info_value\"\n}"
  },
  {
    "path": "tests/resources/models/decodedRealtimeRequest.json",
    "content": "{\n  \"originalTransactionId\": \"99371282\",\n  \"appAppleId\": 531412,\n  \"productId\": \"com.example.product\",\n  \"userLocale\": \"en-US\",\n  \"requestIdentifier\": \"3db5c98d-8acf-4e29-831e-8e1f82f9f6e9\",\n  \"environment\": \"LocalTesting\",\n  \"signedDate\": 1698148900000\n}\n"
  },
  {
    "path": "tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json",
    "content": "{\n  \"requestIdentifier\": \"758883e8-151b-47b7-abd0-60c4d804c2f5\"\n}"
  },
  {
    "path": "tests/resources/models/extendSubscriptionRenewalDateResponse.json",
    "content": "{\n  \"originalTransactionId\": \"2312412\",\n  \"webOrderLineItemId\": \"9993\",\n  \"success\": true,\n  \"effectiveDate\": 1698148900000\n}"
  },
  {
    "path": "tests/resources/models/familyTransactionNotSupportedError.json",
    "content": "{\n  \"errorCode\": 4000185,\n  \"errorMessage\": \"Invalid request. Family Sharing transactions aren't supported by this endpoint.\"\n}"
  },
  {
    "path": "tests/resources/models/getAllSubscriptionStatusesResponse.json",
    "content": "{\n  \"environment\": \"LocalTesting\",\n  \"bundleId\": \"com.example\",\n  \"appAppleId\": 5454545,\n  \"data\": [\n    {\n      \"subscriptionGroupIdentifier\": \"sub_group_one\",\n      \"lastTransactions\": [\n        {\n          \"status\": 1,\n          \"originalTransactionId\": \"3749183\",\n          \"signedTransactionInfo\": \"signed_transaction_one\",\n          \"signedRenewalInfo\": \"signed_renewal_one\"\n        },\n        {\n          \"status\": 5,\n          \"originalTransactionId\": \"5314314134\",\n          \"signedTransactionInfo\": \"signed_transaction_two\",\n          \"signedRenewalInfo\": \"signed_renewal_two\"\n        }\n      ]\n    },\n    {\n      \"subscriptionGroupIdentifier\": \"sub_group_two\",\n      \"lastTransactions\": [\n        {\n          \"status\": 2,\n          \"originalTransactionId\": \"3413453\",\n          \"signedTransactionInfo\": \"signed_transaction_three\",\n          \"signedRenewalInfo\": \"signed_renewal_three\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "tests/resources/models/getDefaultMessageResponse.json",
    "content": "{\n  \"messageIdentifier\": \"a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890\"\n}\n"
  },
  {
    "path": "tests/resources/models/getImageListResponse.json",
    "content": "{\n  \"imageIdentifiers\": [\n    {\n      \"imageIdentifier\": \"a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890\",\n      \"imageState\": \"APPROVED\",\n      \"imageSize\": \"FULL_SIZE\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/resources/models/getMessageListResponse.json",
    "content": "{\n  \"messageIdentifiers\": [\n    {\n      \"messageIdentifier\": \"a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890\",\n      \"messageState\": \"APPROVED\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/resources/models/getNotificationHistoryResponse.json",
    "content": "{\n  \"paginationToken\": \"57715481-805a-4283-8499-1c19b5d6b20a\",\n  \"hasMore\": true,\n  \"notificationHistory\": [\n    {\n      \"sendAttempts\": [\n        {\n          \"attemptDate\": 1698148900000,\n          \"sendAttemptResult\": \"NO_RESPONSE\"\n        }, {\n          \"attemptDate\": 1698148950000,\n          \"sendAttemptResult\": \"SUCCESS\"\n        }\n      ],\n      \"signedPayload\": \"signed_payload_one\"\n    },\n    {\n      \"sendAttempts\": [\n        {\n          \"attemptDate\": 1698148800000,\n          \"sendAttemptResult\": \"CIRCULAR_REDIRECT\"\n        }\n      ],\n      \"signedPayload\": \"signed_payload_two\"\n    }\n  ]\n}"
  },
  {
    "path": "tests/resources/models/getRealtimeUrlResponse.json",
    "content": "{\n  \"realtimeURL\": \"https://example.com/realtime\"\n}\n"
  },
  {
    "path": "tests/resources/models/getRefundHistoryResponse.json",
    "content": "{\n  \"signedTransactions\": [\n    \"signed_transaction_one\",\n    \"signed_transaction_two\"\n  ],\n  \"revision\": \"revision_output\",\n  \"hasMore\": true\n}"
  },
  {
    "path": "tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json",
    "content": "{\n  \"requestIdentifier\": \"20fba8a0-2b80-4a7d-a17f-85c1854727f8\",\n  \"complete\": true,\n  \"completeDate\": 1698148900000,\n  \"succeededCount\": 30,\n  \"failedCount\": 2\n}"
  },
  {
    "path": "tests/resources/models/getTestNotificationStatusResponse.json",
    "content": "{\n  \"signedPayload\": \"signed_payload\",\n  \"sendAttempts\": [\n    {\n      \"attemptDate\": 1698148900000,\n      \"sendAttemptResult\": \"NO_RESPONSE\"\n    }, {\n      \"attemptDate\": 1698148950000,\n      \"sendAttemptResult\": \"SUCCESS\"\n    }\n  ]\n}"
  },
  {
    "path": "tests/resources/models/invalidAppAccountTokenUUIDError.json",
    "content": "{\n  \"errorCode\": 4000183,\n  \"errorMessage\": \"Invalid request. The app account token field must be a valid UUID.\"\n}"
  },
  {
    "path": "tests/resources/models/invalidTransactionIdError.json",
    "content": "{\n   \"errorCode\": 4000006,\n   \"errorMessage\": \"Invalid transaction id.\"\n}"
  },
  {
    "path": "tests/resources/models/lookupOrderIdResponse.json",
    "content": "{\n  \"status\": 1,\n  \"signedTransactions\": [\n    \"signed_transaction_one\",\n    \"signed_transaction_two\"\n  ]\n}"
  },
  {
    "path": "tests/resources/models/performanceTestResponse.json",
    "content": "{\n  \"config\": {\n    \"maxConcurrentRequests\": 10,\n    \"totalRequests\": 100,\n    \"totalDuration\": 60000,\n    \"responseTimeThreshold\": 500,\n    \"successRateThreshold\": 95\n  },\n  \"requestId\": \"c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d\"\n}\n"
  },
  {
    "path": "tests/resources/models/performanceTestResultResponse.json",
    "content": "{\n  \"config\": {\n    \"maxConcurrentRequests\": 10,\n    \"totalRequests\": 100,\n    \"totalDuration\": 60000,\n    \"responseTimeThreshold\": 500,\n    \"successRateThreshold\": 95\n  },\n  \"target\": \"https://example.com/retention\",\n  \"result\": \"PASS\",\n  \"successRate\": 98,\n  \"numPending\": 0,\n  \"responseTimes\": {\n    \"average\": 120,\n    \"p50\": 100,\n    \"p90\": 200,\n    \"p95\": 250,\n    \"p99\": 400\n  },\n  \"failures\": {\n    \"TIMED_OUT\": 1,\n    \"NO_RESPONSE\": 1\n  }\n}\n"
  },
  {
    "path": "tests/resources/models/requestTestNotificationResponse.json",
    "content": "{\n  \"testNotificationToken\": \"ce3af791-365e-4c60-841b-1674b43c1609\"\n}"
  },
  {
    "path": "tests/resources/models/signedConsumptionRequestNotification.json",
    "content": "{\n    \"notificationType\": \"CONSUMPTION_REQUEST\",\n    \"notificationUUID\": \"002e14d5-51f5-4503-b5a8-c3a1af68eb20\",\n    \"data\": {\n      \"environment\": \"LocalTesting\",\n      \"appAppleId\": 41234,\n      \"bundleId\": \"com.example\",\n      \"bundleVersion\": \"1.2.3\",\n      \"signedTransactionInfo\": \"signed_transaction_info_value\",\n      \"signedRenewalInfo\": \"signed_renewal_info_value\",\n      \"status\": 1,\n      \"consumptionRequestReason\": \"UNINTENDED_PURCHASE\"\n    },\n    \"version\": \"2.0\",\n    \"signedDate\": 1698148900000\n}"
  },
  {
    "path": "tests/resources/models/signedExternalPurchaseTokenNotification.json",
    "content": "{\n    \"notificationType\": \"EXTERNAL_PURCHASE_TOKEN\",\n    \"subtype\": \"UNREPORTED\",\n    \"notificationUUID\": \"002e14d5-51f5-4503-b5a8-c3a1af68eb20\",\n    \"version\": \"2.0\",\n    \"signedDate\": 1698148900000,\n    \"externalPurchaseToken\": {\n      \"externalPurchaseId\": \"b2158121-7af9-49d4-9561-1f588205523e\",\n      \"tokenCreationDate\": 1698148950000,\n      \"appAppleId\": 55555,\n      \"bundleId\": \"com.example\"\n    }\n  }"
  },
  {
    "path": "tests/resources/models/signedExternalPurchaseTokenSandboxNotification.json",
    "content": "{\n    \"notificationType\": \"EXTERNAL_PURCHASE_TOKEN\",\n    \"subtype\": \"UNREPORTED\",\n    \"notificationUUID\": \"002e14d5-51f5-4503-b5a8-c3a1af68eb20\",\n    \"version\": \"2.0\",\n    \"signedDate\": 1698148900000,\n    \"externalPurchaseToken\": {\n      \"externalPurchaseId\": \"SANDBOX_b2158121-7af9-49d4-9561-1f588205523e\",\n      \"tokenCreationDate\": 1698148950000,\n      \"appAppleId\": 55555,\n      \"bundleId\": \"com.example\"\n    }\n  }"
  },
  {
    "path": "tests/resources/models/signedNotification.json",
    "content": "{\n  \"notificationType\": \"SUBSCRIBED\",\n  \"subtype\": \"INITIAL_BUY\",\n  \"notificationUUID\": \"002e14d5-51f5-4503-b5a8-c3a1af68eb20\",\n  \"data\": {\n    \"environment\": \"LocalTesting\",\n    \"appAppleId\": 41234,\n    \"bundleId\": \"com.example\",\n    \"bundleVersion\": \"1.2.3\",\n    \"signedTransactionInfo\": \"signed_transaction_info_value\",\n    \"signedRenewalInfo\": \"signed_renewal_info_value\",\n    \"status\": 1\n  },\n  \"version\": \"2.0\",\n  \"signedDate\": 1698148900000\n}"
  },
  {
    "path": "tests/resources/models/signedRenewalInfo.json",
    "content": "{\n  \"expirationIntent\": 1,\n  \"originalTransactionId\": \"12345\",\n  \"autoRenewProductId\": \"com.example.product.2\",\n  \"productId\": \"com.example.product\",\n  \"autoRenewStatus\": 1,\n  \"isInBillingRetryPeriod\": true,\n  \"priceIncreaseStatus\": 0,\n  \"gracePeriodExpiresDate\": 1698148900000,\n  \"offerType\": 2,\n  \"offerIdentifier\": \"abc.123\",\n  \"signedDate\": 1698148800000,\n  \"environment\": \"LocalTesting\",\n  \"recentSubscriptionStartDate\": 1698148800000,\n  \"renewalDate\": 1698148850000,\n  \"renewalPrice\": 9990,\n  \"currency\": \"USD\",\n  \"offerDiscountType\": \"PAY_AS_YOU_GO\",\n  \"eligibleWinBackOfferIds\": [\n    \"eligible1\",\n    \"eligible2\"\n  ],\n  \"appTransactionId\": \"71134\",\n  \"offerPeriod\": \"P1Y\",\n  \"appAccountToken\": \"7e3fb20b-4cdb-47cc-936d-99d65f608138\"\n}\n"
  },
  {
    "path": "tests/resources/models/signedRescindConsentNotification.json",
    "content": "{\n  \"notificationType\": \"RESCIND_CONSENT\",\n  \"notificationUUID\": \"002e14d5-51f5-4503-b5a8-c3a1af68eb20\",\n  \"appData\": {\n    \"appAppleId\": 41234,\n    \"bundleId\": \"com.example\",\n    \"environment\": \"LocalTesting\",\n    \"signedAppTransactionInfo\": \"signed_app_transaction_info_value\"\n  },\n  \"version\": \"2.0\",\n  \"signedDate\": 1698148900000\n}"
  },
  {
    "path": "tests/resources/models/signedSummaryNotification.json",
    "content": "{\n  \"notificationType\": \"RENEWAL_EXTENSION\",\n  \"subtype\": \"SUMMARY\",\n  \"notificationUUID\": \"002e14d5-51f5-4503-b5a8-c3a1af68eb20\",\n  \"version\": \"2.0\",\n  \"signedDate\": 1698148900000,\n  \"summary\": {\n    \"environment\": \"LocalTesting\",\n    \"appAppleId\": 41234,\n    \"bundleId\": \"com.example\",\n    \"productId\": \"com.example.product\",\n    \"requestIdentifier\": \"efb27071-45a4-4aca-9854-2a1e9146f265\",\n    \"storefrontCountryCodes\": [\n      \"CAN\",\n      \"USA\",\n      \"MEX\"\n    ],\n    \"succeededCount\": 5,\n    \"failedCount\": 2\n  }\n}"
  },
  {
    "path": "tests/resources/models/signedTransaction.json",
    "content": "{\n  \"transactionId\":\"23456\",\n  \"originalTransactionId\":\"12345\",\n  \"webOrderLineItemId\":\"34343\",\n  \"bundleId\":\"com.example\",\n  \"productId\":\"com.example.product\",\n  \"subscriptionGroupIdentifier\":\"55555\",\n  \"purchaseDate\":1698148900000,\n  \"originalPurchaseDate\":1698148800000,\n  \"expiresDate\":1698149000000,\n  \"quantity\":1,\n  \"type\":\"Auto-Renewable Subscription\",\n  \"appAccountToken\": \"7e3fb20b-4cdb-47cc-936d-99d65f608138\",\n  \"inAppOwnershipType\":\"PURCHASED\",\n  \"signedDate\":1698148900000,\n  \"revocationReason\": 1,\n  \"revocationDate\": 1698148950000,\n  \"isUpgraded\": true,\n  \"offerType\":1,\n  \"offerIdentifier\": \"abc.123\",\n  \"environment\":\"LocalTesting\",\n  \"transactionReason\":\"PURCHASE\",\n  \"storefront\":\"USA\",\n  \"storefrontId\":\"143441\",\n  \"price\": 10990,\n  \"currency\": \"USD\",\n  \"offerDiscountType\": \"PAY_AS_YOU_GO\",\n  \"appTransactionId\": \"71134\",\n  \"offerPeriod\": \"P1Y\"\n}"
  },
  {
    "path": "tests/resources/models/signedTransactionWithRevocation.json",
    "content": "{\n  \"originalTransactionId\": \"12345\",\n  \"transactionId\": \"23456\",\n  \"webOrderLineItemId\": \"34343\",\n  \"bundleId\": \"com.example\",\n  \"productId\": \"com.example.product\",\n  \"subscriptionGroupIdentifier\": \"55555\",\n  \"purchaseDate\": 1698148900000,\n  \"originalPurchaseDate\": 1698148800000,\n  \"expiresDate\": 1698149000000,\n  \"quantity\": 1,\n  \"type\": \"Auto-Renewable Subscription\",\n  \"appAccountToken\": \"7e3fb20b-4cdb-47cc-936d-99d65f608138\",\n  \"inAppOwnershipType\": \"PURCHASED\",\n  \"signedDate\": 1698148900000,\n  \"revocationReason\": 1,\n  \"revocationDate\": 1698148950000,\n  \"isUpgraded\": true,\n  \"offerType\": 1,\n  \"offerIdentifier\": \"abc.123\",\n  \"environment\": \"LocalTesting\",\n  \"storefront\": \"USA\",\n  \"storefrontId\": \"143441\",\n  \"transactionReason\": \"PURCHASE\",\n  \"price\": 10990,\n  \"currency\": \"USD\",\n  \"offerDiscountType\": \"PAY_AS_YOU_GO\",\n  \"appTransactionId\": \"71134\",\n  \"offerPeriod\": \"P1Y\",\n  \"revocationType\": \"REFUND_PRORATED\",\n  \"revocationPercentage\": 50000\n}"
  },
  {
    "path": "tests/resources/models/transactionHistoryResponse.json",
    "content": "{\n  \"revision\": \"revision_output\",\n  \"hasMore\": true,\n  \"bundleId\": \"com.example\",\n  \"appAppleId\": 323232,\n  \"environment\": \"LocalTesting\",\n  \"signedTransactions\": [\n    \"signed_transaction_value\",\n    \"signed_transaction_value2\"\n  ]\n}"
  },
  {
    "path": "tests/resources/models/transactionHistoryResponseWithMalformedAppAppleId.json",
    "content": "{\n    \"revision\": \"revision_output\",\n    \"hasMore\": 1,\n    \"bundleId\": \"com.example\",\n    \"appAppleId\": \"hi\",\n    \"environment\": \"LocalTesting\",\n    \"signedTransactions\": [\n      \"signed_transaction_value\",\n      \"signed_transaction_value2\"\n    ]\n  }"
  },
  {
    "path": "tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json",
    "content": "{\n    \"revision\": \"revision_output\",\n    \"hasMore\": true,\n    \"bundleId\": \"com.example\",\n    \"appAppleId\": 323232,\n    \"environment\": \"LocalTestingxxx\",\n    \"signedTransactions\": [\n      \"signed_transaction_value\",\n      \"signed_transaction_value2\"\n    ]\n  }"
  },
  {
    "path": "tests/resources/models/transactionIdNotFoundError.json",
    "content": "{\n   \"errorCode\": 4040010,\n   \"errorMessage\": \"Transaction id not found.\"\n}"
  },
  {
    "path": "tests/resources/models/transactionIdNotOriginalTransactionId.json",
    "content": "{\n  \"errorCode\": 4000187,\n  \"errorMessage\": \"Invalid request. The transaction ID provided is not an original transaction ID.\"\n}"
  },
  {
    "path": "tests/resources/models/transactionInfoResponse.json",
    "content": "{\n  \"signedTransactionInfo\": \"signed_transaction_info_value\"\n}"
  },
  {
    "path": "tests/resources/xcode/xcode-app-receipt-empty",
    "content": "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="
  },
  {
    "path": "tests/resources/xcode/xcode-app-receipt-with-transaction",
    "content": "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="
  },
  {
    "path": "tests/resources/xcode/xcode-signed-app-transaction",
    "content": "eyJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFwcGxlX1hjb2RlX0tleSJ9.eyJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJhcHBsaWNhdGlvblZlcnNpb24iOiIxIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiI0OGM4YjkyZC1jZTBkLTQyMjktYmVkZi1lNjFiNGY5Y2ZjOTIiLCJyZWNlaXB0VHlwZSI6Ilhjb2RlIiwicmVjZWlwdENyZWF0aW9uRGF0ZSI6MTY5NzY4MDEyMjI1Ny40NDcsImRldmljZVZlcmlmaWNhdGlvbiI6ImNZVXNYYzUzRWJZYzBwT2VYRzVkNlwvMzFMR0hlVkdmODRzcVNOME9ySmk1dVwvajJIODlXV0tnUzhOMGhNc01sZiIsInJlcXVlc3REYXRlIjoxNjk3NjgwMTIyMjU3LjQ0Nywib3JpZ2luYWxBcHBsaWNhdGlvblZlcnNpb24iOiIxIiwib3JpZ2luYWxQdXJjaGFzZURhdGUiOi02MjEzNTc2OTYwMDAwMH0.Dpdk_VsO2MUCevwyS407alJpPc1Nq_UIP9EiDHaQBxlyi35NFnsKUVNuFNcGWrGRCCImnb4QGBKHfQC2i4sPCg"
  },
  {
    "path": "tests/resources/xcode/xcode-signed-renewal-info",
    "content": "eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiJ9.eyJkZXZpY2VWZXJpZmljYXRpb24iOiJ1K1cxb1FUcXZGSE9RK1pCZTRRMHhQTUMyOGtxRUZ2YmJzRVBwTEtEVlJGdjFHSkdlZ21yTkhWb09ZTU9QdmIyIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiIzNDM5OTE5ZS04N2M5LTQ3YjYtYWVlZS0yODIzZjdhOWQzYzMiLCJyZW5ld2FsRGF0ZSI6MTcwMDM1ODMzNjA0OS43Mjk3LCJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiIwIiwicmVjZW50U3Vic2NyaXB0aW9uU3RhcnREYXRlIjoxNjk3Njc5OTM2MDQ5LjcyOTcsImF1dG9SZW5ld1N0YXR1cyI6MSwic2lnbmVkRGF0ZSI6MTY5NzY3OTkzNjcxMS4wNzQ3LCJlbnZpcm9ubWVudCI6Ilhjb2RlIiwiYXV0b1JlbmV3UHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIn0.WnT3aB9Lwjbr0ICUGn_5CdglzedVd7eOkrqirhcWFvwJZzN1FajuMV6gFEbgD82aL0Ix6HGZcwkNDlVNLvYOEQ"
  },
  {
    "path": "tests/resources/xcode/xcode-signed-transaction",
    "content": "eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlCekRDQ0FYR2dBd0lCQWdJQkFUQUtCZ2dxaGtqT1BRUURBakJJTVNJd0lBWURWUVFERXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTVNJd0lBWURWUVFLRXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTUI0WERUSXpNVEF4T1RBeE5EVXpObG9YRFRJME1UQXhPREF4TkRVek5sb3dTREVpTUNBR0ExVUVBeE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEVpTUNBR0ExVUVDaE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCS1hFUWdZakNvdVB0VHN0R3JLcFk4STUzbkg3cmJESG5jSUxHbm9nU0F1bElJM3NcL3VmTTBnOURjM0JjcjQ5N0FVZ3pHVHZXcGl3SnhwZUIzNzFOZ1YralREQktNQklHQTFVZEV3RUJcL3dRSU1BWUJBZjhDQVFBd0pBWURWUjBSQkIwd0c0RVpVM1J2Y21WTGFYUWdWR1Z6ZEdsdVp5QnBiaUJZWTI5a1pUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0NnWUlLb1pJemowRUF3SURTUUF3UmdJaEFNWnZWWUo2NENGK2gyZm1zbXd2ekFjZVBySUQxM3JwSUpHQkVXK1dncHB0QWlFQXhXaXk0K1QxenQzN1ZzdTB2YjZZdW0wK05MdERxSGxLNnJwTWN2NkpmbkE9Il19.eyJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJwdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5Nywic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiNkYzQTkzQUIiLCJzaWduZWREYXRlIjoxNjk3Njc5OTM2MDU2LjQ4NSwib3JpZ2luYWxQdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5NywiaXNVcGdyYWRlZCI6ZmFsc2UsImRldmljZVZlcmlmaWNhdGlvbiI6InNHRG5wZytvemI4dXdEU3VDRFoyb1ZabzFDS3JiQjh1alI4VnhDeGh5a1J3eUJJSzZ4NlhDeUVSbTh5V3J6RTgiLCJvZmZlclR5cGUiOjEsInF1YW50aXR5IjoxLCJ0cmFuc2FjdGlvbklkIjoiMCIsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJ0cmFuc2FjdGlvblJlYXNvbiI6IlBVUkNIQVNFIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZXhwaXJlc0RhdGUiOjE3MDAzNTgzMzYwNDkuNzI5NywiZW52aXJvbm1lbnQiOiJYY29kZSIsInN0b3JlZnJvbnRJZCI6IjE0MzQ0MSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjAiLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJkZXZpY2VWZXJpZmljYXRpb25Ob25jZSI6IjdlZGVhODdkLTk4ZjAtNDJkMC05NjgyLTQ5Y2E4MTAyMmY3MyIsIndlYk9yZGVyTGluZUl0ZW1JZCI6IjAiLCJzdG9yZWZyb250IjoiVVNBIn0.rkJYnvujStteRkMHhoIR2ThmNFnyKcx5XxIakXYdh-1oKtEVEU5zQAiONaLDpBDO5JhLLrTbfp7LS5tMiqmgHw"
  },
  {
    "path": "tests/test_advanced_commerce_models.py",
    "content": "# Copyright (c) 2026 Apple Inc. Licensed under MIT License.\n\nimport json\nimport unittest\n\nfrom appstoreserverlibrary.models.AdvancedCommerceDescriptors import AdvancedCommerceDescriptors\nfrom appstoreserverlibrary.models.AdvancedCommerceEffective import AdvancedCommerceEffective\nfrom appstoreserverlibrary.models.AdvancedCommerceOffer import AdvancedCommerceOffer\nfrom appstoreserverlibrary.models.AdvancedCommerceOfferPeriod import AdvancedCommerceOfferPeriod\nfrom appstoreserverlibrary.models.AdvancedCommerceOfferReason import AdvancedCommerceOfferReason\nfrom appstoreserverlibrary.models.AdvancedCommerceOneTimeChargeCreateRequest import AdvancedCommerceOneTimeChargeCreateRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceOneTimeChargeItem import AdvancedCommerceOneTimeChargeItem\nfrom appstoreserverlibrary.models.AdvancedCommercePeriod import AdvancedCommercePeriod\nfrom appstoreserverlibrary.models.AdvancedCommerceReason import AdvancedCommerceReason\nfrom appstoreserverlibrary.models.AdvancedCommerceRefundReason import AdvancedCommerceRefundReason\nfrom appstoreserverlibrary.models.AdvancedCommerceRefundType import AdvancedCommerceRefundType\nfrom appstoreserverlibrary.models.AdvancedCommerceRequestInfo import AdvancedCommerceRequestInfo\nfrom appstoreserverlibrary.models.AdvancedCommerceRequestRefundItem import AdvancedCommerceRequestRefundItem\nfrom appstoreserverlibrary.models.AdvancedCommerceRequestRefundRequest import AdvancedCommerceRequestRefundRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceRequestRefundResponse import AdvancedCommerceRequestRefundResponse\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionCancelRequest import AdvancedCommerceSubscriptionCancelRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionCancelResponse import AdvancedCommerceSubscriptionCancelResponse\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionChangeMetadataDescriptors import AdvancedCommerceSubscriptionChangeMetadataDescriptors\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionChangeMetadataItem import AdvancedCommerceSubscriptionChangeMetadataItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionChangeMetadataRequest import AdvancedCommerceSubscriptionChangeMetadataRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionChangeMetadataResponse import AdvancedCommerceSubscriptionChangeMetadataResponse\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionCreateItem import AdvancedCommerceSubscriptionCreateItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionCreateRequest import AdvancedCommerceSubscriptionCreateRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateDescriptors import AdvancedCommerceSubscriptionMigrateDescriptors\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateItem import AdvancedCommerceSubscriptionMigrateItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateRenewalItem import AdvancedCommerceSubscriptionMigrateRenewalItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateRequest import AdvancedCommerceSubscriptionMigrateRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionMigrateResponse import AdvancedCommerceSubscriptionMigrateResponse\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyAddItem import AdvancedCommerceSubscriptionModifyAddItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyChangeItem import AdvancedCommerceSubscriptionModifyChangeItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyDescriptors import AdvancedCommerceSubscriptionModifyDescriptors\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyInAppRequest import AdvancedCommerceSubscriptionModifyInAppRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyPeriodChange import AdvancedCommerceSubscriptionModifyPeriodChange\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionModifyRemoveItem import AdvancedCommerceSubscriptionModifyRemoveItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionPriceChangeItem import AdvancedCommerceSubscriptionPriceChangeItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionPriceChangeRequest import AdvancedCommerceSubscriptionPriceChangeRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionPriceChangeResponse import AdvancedCommerceSubscriptionPriceChangeResponse\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionReactivateInAppRequest import AdvancedCommerceSubscriptionReactivateInAppRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionReactivateItem import AdvancedCommerceSubscriptionReactivateItem\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionRevokeRequest import AdvancedCommerceSubscriptionRevokeRequest\nfrom appstoreserverlibrary.models.AdvancedCommerceSubscriptionRevokeResponse import AdvancedCommerceSubscriptionRevokeResponse\nfrom appstoreserverlibrary.models.AdvancedCommerceValidationUtils import AdvancedCommerceValidationUtils\nfrom appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter\nfrom tests.util import read_data_from_file\n\n\nclass AdvancedCommerceModelsTest(unittest.TestCase):\n    def test_advanced_commerce_period(self):\n        self.assertEqual(\"P1W\", AdvancedCommercePeriod.P1W.value)\n        self.assertEqual(\"P1M\", AdvancedCommercePeriod.P1M.value)\n        self.assertEqual(\"P2M\", AdvancedCommercePeriod.P2M.value)\n        self.assertEqual(\"P3M\", AdvancedCommercePeriod.P3M.value)\n        self.assertEqual(\"P6M\", AdvancedCommercePeriod.P6M.value)\n        self.assertEqual(\"P1Y\", AdvancedCommercePeriod.P1Y.value)\n\n        self.assertEqual(AdvancedCommercePeriod.P1W, AdvancedCommercePeriod(\"P1W\"))\n        self.assertEqual(AdvancedCommercePeriod.P1M, AdvancedCommercePeriod(\"P1M\"))\n        self.assertEqual(AdvancedCommercePeriod.P1Y, AdvancedCommercePeriod(\"P1Y\"))\n        self.assertFalse(\"INVALID\" in AdvancedCommercePeriod)\n\n        self.assertEqual(\"P1W\", AdvancedCommercePeriod.P1W.value)\n\n    def test_advanced_commerce_reason(self):\n        self.assertEqual(\"UPGRADE\", AdvancedCommerceReason.UPGRADE.value)\n        self.assertEqual(\"DOWNGRADE\", AdvancedCommerceReason.DOWNGRADE.value)\n        self.assertEqual(\"APPLY_OFFER\", AdvancedCommerceReason.APPLY_OFFER.value)\n\n        self.assertEqual(AdvancedCommerceReason.UPGRADE, AdvancedCommerceReason(\"UPGRADE\"))\n        self.assertEqual(AdvancedCommerceReason.DOWNGRADE, AdvancedCommerceReason(\"DOWNGRADE\"))\n        self.assertEqual(AdvancedCommerceReason.APPLY_OFFER, AdvancedCommerceReason(\"APPLY_OFFER\"))\n        self.assertFalse(\"INVALID\" in AdvancedCommerceReason)\n\n        self.assertEqual(\"UPGRADE\", AdvancedCommerceReason.UPGRADE.value)\n\n    def test_advanced_commerce_refund_reason(self):\n        self.assertEqual(\"UNINTENDED_PURCHASE\", AdvancedCommerceRefundReason.UNINTENDED_PURCHASE.value)\n        self.assertEqual(\"FULFILLMENT_ISSUE\", AdvancedCommerceRefundReason.FULFILLMENT_ISSUE.value)\n        self.assertEqual(\"UNSATISFIED_WITH_PURCHASE\", AdvancedCommerceRefundReason.UNSATISFIED_WITH_PURCHASE.value)\n        self.assertEqual(\"LEGAL\", AdvancedCommerceRefundReason.LEGAL.value)\n        self.assertEqual(\"OTHER\", AdvancedCommerceRefundReason.OTHER.value)\n        self.assertEqual(\"MODIFY_ITEMS_REFUND\", AdvancedCommerceRefundReason.MODIFY_ITEMS_REFUND.value)\n        self.assertEqual(\"SIMULATE_REFUND_DECLINE\", AdvancedCommerceRefundReason.SIMULATE_REFUND_DECLINE.value)\n\n        self.assertEqual(AdvancedCommerceRefundReason.LEGAL, AdvancedCommerceRefundReason(\"LEGAL\"))\n        self.assertEqual(AdvancedCommerceRefundReason.OTHER, AdvancedCommerceRefundReason(\"OTHER\"))\n        self.assertFalse(\"INVALID\" in AdvancedCommerceRefundReason)\n\n        self.assertEqual(\"LEGAL\", AdvancedCommerceRefundReason.LEGAL.value)\n\n    def test_advanced_commerce_refund_type(self):\n        self.assertEqual(\"FULL\", AdvancedCommerceRefundType.FULL.value)\n        self.assertEqual(\"PRORATED\", AdvancedCommerceRefundType.PRORATED.value)\n        self.assertEqual(\"CUSTOM\", AdvancedCommerceRefundType.CUSTOM.value)\n\n        self.assertEqual(AdvancedCommerceRefundType.FULL, AdvancedCommerceRefundType(\"FULL\"))\n        self.assertEqual(AdvancedCommerceRefundType.PRORATED, AdvancedCommerceRefundType(\"PRORATED\"))\n        self.assertEqual(AdvancedCommerceRefundType.CUSTOM, AdvancedCommerceRefundType(\"CUSTOM\"))\n        self.assertFalse(\"INVALID\" in AdvancedCommerceRefundType)\n\n        self.assertEqual(\"FULL\", AdvancedCommerceRefundType.FULL.value)\n\n    def test_advanced_commerce_offer_period(self):\n        self.assertEqual(\"P3D\", AdvancedCommerceOfferPeriod.P3D.value)\n        self.assertEqual(\"P1W\", AdvancedCommerceOfferPeriod.P1W.value)\n        self.assertEqual(\"P2W\", AdvancedCommerceOfferPeriod.P2W.value)\n        self.assertEqual(\"P1M\", AdvancedCommerceOfferPeriod.P1M.value)\n        self.assertEqual(\"P2M\", AdvancedCommerceOfferPeriod.P2M.value)\n        self.assertEqual(\"P3M\", AdvancedCommerceOfferPeriod.P3M.value)\n\n        self.assertEqual(AdvancedCommerceOfferPeriod.P1W, AdvancedCommerceOfferPeriod(\"P1W\"))\n        self.assertEqual(AdvancedCommerceOfferPeriod.P1M, AdvancedCommerceOfferPeriod(\"P1M\"))\n        self.assertEqual(AdvancedCommerceOfferPeriod.P3D, AdvancedCommerceOfferPeriod(\"P3D\"))\n        self.assertFalse(\"INVALID\" in AdvancedCommerceOfferPeriod)\n\n        self.assertEqual(\"P1W\", AdvancedCommerceOfferPeriod.P1W.value)\n\n    def test_advanced_commerce_offer_reason(self):\n        self.assertEqual(\"ACQUISITION\", AdvancedCommerceOfferReason.ACQUISITION.value)\n        self.assertEqual(\"WIN_BACK\", AdvancedCommerceOfferReason.WIN_BACK.value)\n        self.assertEqual(\"RETENTION\", AdvancedCommerceOfferReason.RETENTION.value)\n\n        self.assertEqual(AdvancedCommerceOfferReason.ACQUISITION, AdvancedCommerceOfferReason(\"ACQUISITION\"))\n        self.assertEqual(AdvancedCommerceOfferReason.WIN_BACK, AdvancedCommerceOfferReason(\"WIN_BACK\"))\n        self.assertEqual(AdvancedCommerceOfferReason.RETENTION, AdvancedCommerceOfferReason(\"RETENTION\"))\n        self.assertFalse(\"INVALID\" in AdvancedCommerceOfferReason)\n\n        self.assertEqual(\"WIN_BACK\", AdvancedCommerceOfferReason.WIN_BACK.value)\n\n    def test_advanced_commerce_effective(self):\n        self.assertEqual(\"IMMEDIATELY\", AdvancedCommerceEffective.IMMEDIATELY.value)\n        self.assertEqual(\"NEXT_BILL_CYCLE\", AdvancedCommerceEffective.NEXT_BILL_CYCLE.value)\n\n        self.assertEqual(AdvancedCommerceEffective.IMMEDIATELY, AdvancedCommerceEffective(\"IMMEDIATELY\"))\n        self.assertEqual(AdvancedCommerceEffective.NEXT_BILL_CYCLE, AdvancedCommerceEffective(\"NEXT_BILL_CYCLE\"))\n        self.assertFalse(\"INVALID\" in AdvancedCommerceEffective)\n\n        self.assertEqual(\"IMMEDIATELY\", AdvancedCommerceEffective.IMMEDIATELY.value)\n\n    def test_validation_utils_description(self):\n        valid_description = \"Valid description\"\n        AdvancedCommerceValidationUtils.description_validator(None, None, valid_description)\n\n        max_length_description = \"A\" * 45\n        AdvancedCommerceValidationUtils.description_validator(None, None, max_length_description)\n\n        too_long_description = \"A\" * 46\n        with self.assertRaises(ValueError):\n            AdvancedCommerceValidationUtils.description_validator(None, None, too_long_description)\n\n    def test_validation_utils_display_name(self):\n        valid_display_name = \"Valid Name\"\n        AdvancedCommerceValidationUtils.display_name_validator(None, None, valid_display_name)\n\n        max_length_display_name = \"A\" * 30\n        AdvancedCommerceValidationUtils.display_name_validator(None, None, max_length_display_name)\n\n        too_long_display_name = \"A\" * 31\n        with self.assertRaises(ValueError):\n            AdvancedCommerceValidationUtils.display_name_validator(None, None, too_long_display_name)\n\n    def test_validation_utils_sku(self):\n        valid_sku = \"valid.sku.123\"\n        AdvancedCommerceValidationUtils.sku_validator(None, None, valid_sku)\n\n        max_length_sku = \"A\" * 128\n        AdvancedCommerceValidationUtils.sku_validator(None, None, max_length_sku)\n\n        too_long_sku = \"A\" * 129\n        with self.assertRaises(ValueError):\n            AdvancedCommerceValidationUtils.sku_validator(None, None, too_long_sku)\n\n    def test_validation_utils_period_count(self):\n        AdvancedCommerceValidationUtils.period_count_validator(None, None, 1)\n        AdvancedCommerceValidationUtils.period_count_validator(None, None, 6)\n        AdvancedCommerceValidationUtils.period_count_validator(None, None, 12)\n\n        with self.assertRaises(ValueError):\n            AdvancedCommerceValidationUtils.period_count_validator(None, None, 0)\n\n        with self.assertRaises(ValueError):\n            AdvancedCommerceValidationUtils.period_count_validator(None, None, 13)\n\n    def test_validation_utils_items(self):\n        valid_list = [\n            AdvancedCommerceOneTimeChargeItem(\n                description=\"desc\",\n                displayName=\"name\",\n                SKU=\"sku1\",\n                price=1000\n            )\n        ]\n        AdvancedCommerceValidationUtils.items_validator(None, None, valid_list)\n\n        with self.assertRaises(ValueError):\n            AdvancedCommerceValidationUtils.items_validator(None, None, None)\n\n        with self.assertRaises(ValueError):\n            AdvancedCommerceValidationUtils.items_validator(None, None, [])\n\n        list_with_none = [None]\n        with self.assertRaises(ValueError):\n            AdvancedCommerceValidationUtils.items_validator(None, None, list_with_none)\n\n    def test_advanced_commerce_descriptors_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceDescriptors.json')\n\n        descriptors_dict = json.loads(json_data)\n        descriptors = _get_cattrs_converter(AdvancedCommerceDescriptors).structure(descriptors_dict, AdvancedCommerceDescriptors)\n\n        self.assertEqual(\"description\", descriptors.description)\n        self.assertEqual(\"display name\", descriptors.displayName)\n    \n    def test_advanced_commerce_one_time_charge_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceOneTimeChargeItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceOneTimeChargeItem).structure(item_dict, AdvancedCommerceOneTimeChargeItem)\n\n        self.assertEqual(\"description\", item.description)\n        self.assertEqual(\"display name\", item.displayName)\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(15000, item.price)\n    \n    def test_advanced_commerce_subscription_create_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCreateItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionCreateItem).structure(item_dict, AdvancedCommerceSubscriptionCreateItem)\n\n        self.assertEqual(\"description\", item.description)\n        self.assertEqual(\"display name\", item.displayName)\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(20000, item.price)\n    \n    def test_advanced_commerce_request_refund_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestRefundItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceRequestRefundItem).structure(item_dict, AdvancedCommerceRequestRefundItem)\n\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(AdvancedCommerceRefundReason.LEGAL, item.refundReason)\n        self.assertEqual(\"LEGAL\", item.rawRefundReason)\n        self.assertEqual(AdvancedCommerceRefundType.FULL, item.refundType)\n        self.assertEqual(\"FULL\", item.rawRefundType)\n        self.assertTrue(item.revoke)\n        self.assertEqual(5000, item.refundAmount)\n\n    def test_advanced_commerce_offer_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceOffer.json')\n\n        offer_dict = json.loads(json_data)\n        offer = _get_cattrs_converter(AdvancedCommerceOffer).structure(offer_dict, AdvancedCommerceOffer)\n\n        self.assertEqual(AdvancedCommerceOfferPeriod.P1W, offer.period)\n        self.assertEqual(\"P1W\", offer.rawPeriod)\n        self.assertEqual(3, offer.periodCount)\n        self.assertEqual(5000, offer.price)\n        self.assertEqual(AdvancedCommerceOfferReason.WIN_BACK, offer.reason)\n        self.assertEqual(\"WIN_BACK\", offer.rawReason)\n\n    def test_advanced_commerce_one_time_charge_create_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceOneTimeChargeCreateRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceOneTimeChargeCreateRequest).structure(request_dict, AdvancedCommerceOneTimeChargeCreateRequest)\n\n        self.assertEqual(\"USD\", request.currency)\n        self.assertIsNotNone(request.item)\n        self.assertEqual(\"description\", request.item.description)\n        self.assertEqual(\"display name\", request.item.displayName)\n        self.assertEqual(\"sku\", request.item.SKU)\n        self.assertEqual(10000, request.item.price)\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440000\", str(request.requestInfo.requestReferenceId))\n        self.assertEqual(\"taxCode\", request.taxCode)\n        self.assertEqual(\"USA\", request.storefront)\n        self.assertEqual(\"CREATE_ONE_TIME_CHARGE\", request.operation)\n        self.assertEqual(\"1\", request.version)\n    \n    def test_advanced_commerce_subscription_create_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCreateRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceSubscriptionCreateRequest).structure(request_dict, AdvancedCommerceSubscriptionCreateRequest)\n\n        self.assertEqual(\"USD\", request.currency)\n        self.assertIsNotNone(request.descriptors)\n        self.assertEqual(\"description\", request.descriptors.description)\n        self.assertEqual(\"display name\", request.descriptors.displayName)\n        self.assertEqual(2, len(request.items))\n        self.assertEqual(\"sku\", request.items[0].SKU)\n        self.assertEqual(20000, request.items[0].price)\n        self.assertEqual(\"sku\", request.items[1].SKU)\n        self.assertEqual(30000, request.items[1].price)\n        self.assertEqual(\"P1M\", request.period)\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440001\", str(request.requestInfo.requestReferenceId))\n        self.assertEqual(\"taxCode\", request.taxCode)\n        self.assertEqual(\"USA\", request.storefront)\n        self.assertEqual(\"transactionId\", request.previousTransactionId)\n\n    def test_advanced_commerce_request_refund_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestRefundRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceRequestRefundRequest).structure(request_dict, AdvancedCommerceRequestRefundRequest)\n\n        self.assertEqual(2, len(request.items))\n        self.assertEqual(\"sku\", request.items[0].SKU)\n        self.assertEqual(AdvancedCommerceRefundReason.LEGAL, request.items[0].refundReason)\n        self.assertEqual(AdvancedCommerceRefundType.FULL, request.items[0].refundType)\n        self.assertTrue(request.items[0].revoke)\n        self.assertEqual(\"sku\", request.items[1].SKU)\n        self.assertEqual(AdvancedCommerceRefundReason.OTHER, request.items[1].refundReason)\n        self.assertEqual(AdvancedCommerceRefundType.PRORATED, request.items[1].refundType)\n        self.assertFalse(request.items[1].revoke)\n        self.assertTrue(request.refundRiskingPreference)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440002\", str(request.requestInfo.requestReferenceId))\n        self.assertEqual(\"USD\", request.currency)\n        self.assertEqual(\"USA\", request.storefront)\n    \n    def test_advanced_commerce_subscription_cancel_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCancelRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceSubscriptionCancelRequest).structure(request_dict, AdvancedCommerceSubscriptionCancelRequest)\n\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440003\", str(request.requestInfo.requestReferenceId))\n        self.assertEqual(\"USA\", request.storefront)\n    \n    def test_advanced_commerce_subscription_revoke_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionRevokeRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceSubscriptionRevokeRequest).structure(request_dict, AdvancedCommerceSubscriptionRevokeRequest)\n\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440004\", str(request.requestInfo.requestReferenceId))\n        self.assertTrue(request.refundRiskingPreference)\n        self.assertEqual(\"USA\", request.storefront)\n    \n    def test_advanced_commerce_subscription_price_change_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionPriceChangeRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceSubscriptionPriceChangeRequest).structure(request_dict, AdvancedCommerceSubscriptionPriceChangeRequest)\n\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440005\", str(request.requestInfo.requestReferenceId))\n        self.assertEqual(1, len(request.items))\n        self.assertEqual(\"sku123\", request.items[0].SKU)\n        self.assertEqual(15000, request.items[0].price)\n        self.assertEqual(\"USD\", request.currency)\n    \n    def test_advanced_commerce_request_refund_response_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestRefundResponse.json')\n\n        response_dict = json.loads(json_data)\n        response = _get_cattrs_converter(AdvancedCommerceRequestRefundResponse).structure(response_dict, AdvancedCommerceRequestRefundResponse)\n\n        self.assertEqual(\"signed_transaction_info_value\", response.signedTransactionInfo)\n\n    def test_advanced_commerce_subscription_cancel_response_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionCancelResponse.json')\n\n        response_dict = json.loads(json_data)\n        response = _get_cattrs_converter(AdvancedCommerceSubscriptionCancelResponse).structure(response_dict, AdvancedCommerceSubscriptionCancelResponse)\n\n        self.assertEqual(\"signed_renewal_info\", response.signedRenewalInfo)\n        self.assertEqual(\"signed_transaction_info\", response.signedTransactionInfo)\n    \n    def test_advanced_commerce_subscription_revoke_response_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionRevokeResponse.json')\n\n        response_dict = json.loads(json_data)\n        response = _get_cattrs_converter(AdvancedCommerceSubscriptionRevokeResponse).structure(response_dict, AdvancedCommerceSubscriptionRevokeResponse)\n\n        self.assertEqual(\"signed_renewal_info\", response.signedRenewalInfo)\n        self.assertEqual(\"signed_transaction_info\", response.signedTransactionInfo)\n\n    def test_advanced_commerce_subscription_price_change_response_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionPriceChangeResponse.json')\n\n        response_dict = json.loads(json_data)\n        response = _get_cattrs_converter(AdvancedCommerceSubscriptionPriceChangeResponse).structure(response_dict, AdvancedCommerceSubscriptionPriceChangeResponse)\n\n        self.assertEqual(\"signed_renewal_info\", response.signedRenewalInfo)\n        self.assertEqual(\"signed_transaction_info\", response.signedTransactionInfo)\n    \n    def test_advanced_commerce_subscription_change_metadata_response_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataResponse.json')\n\n        response_dict = json.loads(json_data)\n        response = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataResponse).structure(response_dict, AdvancedCommerceSubscriptionChangeMetadataResponse)\n\n        self.assertEqual(\"signed_renewal_info\", response.signedRenewalInfo)\n        self.assertEqual(\"signed_transaction_info\", response.signedTransactionInfo)\n\n\n    def test_advanced_commerce_subscription_migrate_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateRequest).structure(request_dict, AdvancedCommerceSubscriptionMigrateRequest)\n\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440006\", str(request.requestInfo.requestReferenceId))\n        self.assertIsNotNone(request.descriptors)\n        self.assertEqual(\"description\", request.descriptors.description)\n        self.assertEqual(\"display name\", request.descriptors.displayName)\n        self.assertEqual(1, len(request.items))\n        self.assertEqual(\"sku\", request.items[0].SKU)\n        self.assertEqual(\"targetProductId\", request.targetProductId)\n        self.assertEqual(\"taxCode\", request.taxCode)\n    \n    def test_advanced_commerce_subscription_modify_in_app_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyInAppRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyInAppRequest).structure(request_dict, AdvancedCommerceSubscriptionModifyInAppRequest)\n\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440007\", str(request.requestInfo.requestReferenceId))\n        self.assertEqual(\"transactionId\", request.transactionId)\n        self.assertTrue(request.retainBillingCycle)\n        self.assertIsNotNone(request.descriptors)\n        self.assertEqual(\"description\", request.descriptors.description)\n        self.assertEqual(\"display name\", request.descriptors.displayName)\n        self.assertEqual(\"taxCode\", request.taxCode)\n        self.assertEqual(\"USD\", request.currency)\n        \n    def test_advanced_commerce_subscription_reactivate_in_app_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionReactivateInAppRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceSubscriptionReactivateInAppRequest).structure(request_dict, AdvancedCommerceSubscriptionReactivateInAppRequest)\n\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440008\", str(request.requestInfo.requestReferenceId))\n        self.assertEqual(\"transactionId\", request.transactionId)\n        self.assertEqual(1, len(request.items))\n        self.assertEqual(\"sku\", request.items[0].SKU)\n    \n    def test_advanced_commerce_subscription_change_metadata_request_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataRequest.json')\n\n        request_dict = json.loads(json_data)\n        request = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataRequest).structure(request_dict, AdvancedCommerceSubscriptionChangeMetadataRequest)\n\n        self.assertIsNotNone(request.requestInfo)\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440009\", str(request.requestInfo.requestReferenceId))\n        self.assertEqual(1, len(request.items))\n        self.assertEqual(\"currentSKU\", request.items[0].currentSKU)\n        self.assertEqual(\"sku\", request.items[0].SKU)\n    \n    def test_advanced_commerce_subscription_migrate_descriptors_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateDescriptors.json')\n\n        descriptors_dict = json.loads(json_data)\n        descriptors = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateDescriptors).structure(descriptors_dict, AdvancedCommerceSubscriptionMigrateDescriptors)\n\n        self.assertEqual(\"description\", descriptors.description)\n        self.assertEqual(\"displayName\", descriptors.displayName)\n    \n    def test_advanced_commerce_subscription_modify_descriptors_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyDescriptors.json')\n\n        descriptors_dict = json.loads(json_data)\n        descriptors = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyDescriptors).structure(descriptors_dict, AdvancedCommerceSubscriptionModifyDescriptors)\n\n        self.assertEqual(\"description\", descriptors.description)\n        self.assertEqual(\"displayName\", descriptors.displayName)\n    \n    def test_advanced_commerce_subscription_change_metadata_descriptors_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataDescriptors.json')\n\n        descriptors_dict = json.loads(json_data)\n        descriptors = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataDescriptors).structure(descriptors_dict, AdvancedCommerceSubscriptionChangeMetadataDescriptors)\n\n        self.assertEqual(\"description\", descriptors.description)\n        self.assertEqual(\"displayName\", descriptors.displayName)\n\n    def test_advanced_commerce_subscription_change_metadata_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionChangeMetadataItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionChangeMetadataItem).structure(item_dict, AdvancedCommerceSubscriptionChangeMetadataItem)\n\n        self.assertEqual(\"currentSku\", item.currentSKU)\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(\"description\", item.description)\n        self.assertEqual(\"displayName\", item.displayName)\n    \n    def test_advanced_commerce_subscription_migrate_renewal_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateRenewalItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateRenewalItem).structure(item_dict, AdvancedCommerceSubscriptionMigrateRenewalItem)\n\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(\"description\", item.description)\n        self.assertEqual(\"displayName\", item.displayName)\n    \n    def test_advanced_commerce_subscription_modify_add_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyAddItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyAddItem).structure(item_dict, AdvancedCommerceSubscriptionModifyAddItem)\n\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(\"description\", item.description)\n        self.assertEqual(\"displayName\", item.displayName)\n        self.assertEqual(12000, item.price)\n    \n    def test_advanced_commerce_subscription_modify_change_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyChangeItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyChangeItem).structure(item_dict, AdvancedCommerceSubscriptionModifyChangeItem)\n\n        self.assertEqual(\"currentSku\", item.currentSKU)\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(\"description\", item.description)\n        self.assertEqual(\"displayName\", item.displayName)\n        self.assertEqual(13000, item.price)\n    \n    def test_advanced_commerce_subscription_modify_remove_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyRemoveItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyRemoveItem).structure(item_dict, AdvancedCommerceSubscriptionModifyRemoveItem)\n\n        self.assertEqual(\"sku\", item.SKU)\n    \n    def test_advanced_commerce_subscription_modify_period_change_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionModifyPeriodChange.json')\n\n        change_dict = json.loads(json_data)\n        change = _get_cattrs_converter(AdvancedCommerceSubscriptionModifyPeriodChange).structure(change_dict, AdvancedCommerceSubscriptionModifyPeriodChange)\n\n        self.assertEqual(\"P3M\", change.period)\n    \n    def test_advanced_commerce_subscription_price_change_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionPriceChangeItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionPriceChangeItem).structure(item_dict, AdvancedCommerceSubscriptionPriceChangeItem)\n\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(16000, item.price)\n        self.assertEqual(\"dependentSKU\", item.dependentSKUs[0])\n\n    def test_advanced_commerce_subscription_price_change_item_dependent_sku_validation(self):\n        valid_sku = \"A\" * 128\n        too_long_sku = \"A\" * 129\n\n        # Valid SKU in dependentSKUs is accepted\n        item = AdvancedCommerceSubscriptionPriceChangeItem(SKU=\"sku\", price=1000, dependentSKUs=[valid_sku])\n        self.assertEqual(valid_sku, item.dependentSKUs[0])\n\n        # Too-long SKU in dependentSKUs raises ValueError\n        with self.assertRaises(ValueError):\n            AdvancedCommerceSubscriptionPriceChangeItem(SKU=\"sku\", price=1000, dependentSKUs=[too_long_sku])\n\n        # None list is allowed (field is optional)\n        item_none = AdvancedCommerceSubscriptionPriceChangeItem(SKU=\"sku\", price=1000, dependentSKUs=None)\n        self.assertIsNone(item_none.dependentSKUs)\n\n    def test_advanced_commerce_subscription_reactivate_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionReactivateItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionReactivateItem).structure(item_dict, AdvancedCommerceSubscriptionReactivateItem)\n\n        self.assertEqual(\"sku\", item.SKU)\n\n    def test_advanced_commerce_request_info_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceRequestInfo.json')\n\n        info_dict = json.loads(json_data)\n        info = _get_cattrs_converter(AdvancedCommerceRequestInfo).structure(info_dict, AdvancedCommerceRequestInfo)\n\n        self.assertEqual(\"550e8400-e29b-41d4-a716-446655440010\", str(info.requestReferenceId))\n        self.assertEqual(\"660e8400-e29b-41d4-a716-446655440011\", str(info.appAccountToken))\n        self.assertEqual(\"consistency_token_value\", info.consistencyToken)\n\n    def test_advanced_commerce_subscription_migrate_item_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateItem.json')\n\n        item_dict = json.loads(json_data)\n        item = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateItem).structure(item_dict, AdvancedCommerceSubscriptionMigrateItem)\n\n        self.assertEqual(\"sku\", item.SKU)\n        self.assertEqual(\"description\", item.description)\n        self.assertEqual(\"displayName\", item.displayName)\n\n    def test_advanced_commerce_subscription_migrate_response_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/advancedCommerceSubscriptionMigrateResponse.json')\n\n        response_dict = json.loads(json_data)\n        response = _get_cattrs_converter(AdvancedCommerceSubscriptionMigrateResponse).structure(response_dict, AdvancedCommerceSubscriptionMigrateResponse)\n\n        self.assertEqual(\"signed_renewal_info_value\", response.signedRenewalInfo)\n        self.assertEqual(\"signed_transaction_info_value\", response.signedTransactionInfo)\n"
  },
  {
    "path": "tests/test_api_client.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom typing import Any, Dict, List, Union\nimport unittest\n\nfrom requests import Response\nfrom appstoreserverlibrary.api_client import APIError, APIException, AppStoreServerAPIClient, GetTransactionHistoryVersion\nfrom appstoreserverlibrary.models.AccountTenure import AccountTenure\nfrom appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus\nfrom appstoreserverlibrary.models.ConsumptionRequest import ConsumptionRequest\nfrom appstoreserverlibrary.models.ConsumptionRequestV1 import ConsumptionRequestV1\nfrom appstoreserverlibrary.models.ConsumptionStatus import ConsumptionStatus\nfrom appstoreserverlibrary.models.DeliveryStatus import DeliveryStatus\nfrom appstoreserverlibrary.models.DeliveryStatusV1 import DeliveryStatusV1\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent\nfrom appstoreserverlibrary.models.ExtendReasonCode import ExtendReasonCode\nfrom appstoreserverlibrary.models.ExtendRenewalDateRequest import ExtendRenewalDateRequest\nfrom appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType\nfrom appstoreserverlibrary.models.LastTransactionsItem import LastTransactionsItem\nfrom appstoreserverlibrary.models.LifetimeDollarsPurchased import LifetimeDollarsPurchased\nfrom appstoreserverlibrary.models.LifetimeDollarsRefunded import LifetimeDollarsRefunded\nfrom appstoreserverlibrary.models.MassExtendRenewalDateRequest import MassExtendRenewalDateRequest\nfrom appstoreserverlibrary.models.NotificationHistoryRequest import NotificationHistoryRequest\nfrom appstoreserverlibrary.models.NotificationHistoryResponseItem import NotificationHistoryResponseItem\nfrom appstoreserverlibrary.models.NotificationTypeV2 import NotificationTypeV2\nfrom appstoreserverlibrary.models.OfferType import OfferType\nfrom appstoreserverlibrary.models.OrderLookupStatus import OrderLookupStatus\nfrom appstoreserverlibrary.models.Platform import Platform\nfrom appstoreserverlibrary.models.PlayTime import PlayTime\nfrom appstoreserverlibrary.models.RefundPreference import RefundPreference\nfrom appstoreserverlibrary.models.RefundPreferenceV1 import RefundPreferenceV1\nfrom appstoreserverlibrary.models.SendAttemptItem import SendAttemptItem\nfrom appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult\nfrom appstoreserverlibrary.models.Status import Status\nfrom appstoreserverlibrary.models.SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem\nfrom appstoreserverlibrary.models.Subtype import Subtype\nfrom appstoreserverlibrary.models.TransactionHistoryRequest import Order, ProductType, TransactionHistoryRequest\nfrom appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest\nfrom appstoreserverlibrary.models.UserStatus import UserStatus\nfrom appstoreserverlibrary.models.DefaultConfigurationRequest import DefaultConfigurationRequest\nfrom appstoreserverlibrary.models.HeaderPosition import HeaderPosition\nfrom appstoreserverlibrary.models.ImageSize import ImageSize\nfrom appstoreserverlibrary.models.ImageState import ImageState\nfrom appstoreserverlibrary.models.MessageState import MessageState\nfrom appstoreserverlibrary.models.BulletPoint import BulletPoint\nfrom appstoreserverlibrary.models.PerformanceTestRequest import PerformanceTestRequest\nfrom appstoreserverlibrary.models.PerformanceTestStatus import PerformanceTestStatus\nfrom appstoreserverlibrary.models.RealtimeUrlRequest import RealtimeUrlRequest\nfrom appstoreserverlibrary.models.UploadMessageImage import UploadMessageImage\nfrom appstoreserverlibrary.models.UploadMessageRequestBody import UploadMessageRequestBody\nfrom uuid import UUID\n\nfrom tests.util import decode_json_from_signed_date, read_data_from_binary_file, read_data_from_file\n\nfrom io import BytesIO\n\nclass DecodedPayloads(unittest.TestCase):\n    def test_extend_renewal_date_for_all_active_subscribers(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json',\n                                           'POST',\n                                           'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass',\n                                           {}, \n                                           {'extendByDays': 45, 'extendReasonCode': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac', 'storefrontCountryCodes': ['USA', 'MEX'], 'productId': 'com.example.productId'})\n        \n        extend_renewal_date_request = MassExtendRenewalDateRequest(\n             extendByDays=45,\n             extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION,\n             requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac',\n             storefrontCountryCodes=['USA', 'MEX'],\n             productId='com.example.productId')\n\n        mass_extend_renewal_date_response = client.extend_renewal_date_for_all_active_subscribers(extend_renewal_date_request)\n\n        self.assertIsNotNone(mass_extend_renewal_date_response)\n        self.assertEqual('758883e8-151b-47b7-abd0-60c4d804c2f5', mass_extend_renewal_date_response.requestIdentifier)\n\n    def test_extend_subscription_renewal_date(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/extendSubscriptionRenewalDateResponse.json',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/subscriptions/extend/4124214',\n                                           {},\n                                           {'extendByDays': 45, 'extendReasonCode': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac'})\n\n        extend_renewal_date_request = ExtendRenewalDateRequest(\n             extendByDays=45,\n             extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION,\n             requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac'\n        )\n\n        extend_renewal_date_response = client.extend_subscription_renewal_date('4124214', extend_renewal_date_request)\n\n        self.assertIsNotNone(extend_renewal_date_response)\n        self.assertEqual('2312412', extend_renewal_date_response.originalTransactionId)\n        self.assertEqual('9993', extend_renewal_date_response.webOrderLineItemId)\n        self.assertTrue(extend_renewal_date_response.success)\n        self.assertEqual(1698148900000, extend_renewal_date_response.effectiveDate)\n\n    def test_get_all_subscription_statuses(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getAllSubscriptionStatusesResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/subscriptions/4321', \n                                           {'status': [2, 1]},\n                                           None)\n\n        status_response = client.get_all_subscription_statuses('4321', [Status.EXPIRED, Status.ACTIVE])\n\n        self.assertIsNotNone(status_response)\n        self.assertEqual(Environment.LOCAL_TESTING, status_response.environment)\n        self.assertEqual('LocalTesting', status_response.rawEnvironment)\n        self.assertEqual('com.example', status_response.bundleId)\n        self.assertEqual(5454545, status_response.appAppleId)\n\n\n        expected_body = [\n             SubscriptionGroupIdentifierItem(\n                subscriptionGroupIdentifier='sub_group_one',\n                lastTransactions=[\n                    LastTransactionsItem(\n                        status=Status.ACTIVE,\n                        originalTransactionId='3749183',\n                        signedTransactionInfo='signed_transaction_one',\n                        signedRenewalInfo='signed_renewal_one'\n                    ),\n                    LastTransactionsItem(\n                        status=Status.REVOKED,\n                        originalTransactionId='5314314134',\n                        signedTransactionInfo='signed_transaction_two',\n                        signedRenewalInfo='signed_renewal_two'\n                    )\n                ]\n            ),\n            SubscriptionGroupIdentifierItem(\n                 subscriptionGroupIdentifier='sub_group_two',\n                 lastTransactions=[\n                      LastTransactionsItem(\n                           status=Status.EXPIRED,\n                           originalTransactionId='3413453',\n                           signedTransactionInfo='signed_transaction_three',\n                           signedRenewalInfo='signed_renewal_three'\n                      )\n                 ]\n            )\n        ]\n        self.assertEqual(expected_body, status_response.data)\n\n    def test_get_refund_history(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getRefundHistoryResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v2/refund/lookup/555555', \n                                           {'revision': ['revision_input']}, \n                                           None)\n\n        refund_history_response = client.get_refund_history('555555', 'revision_input')\n\n        self.assertIsNotNone(refund_history_response)\n        self.assertEqual(['signed_transaction_one', 'signed_transaction_two'], refund_history_response.signedTransactions)\n        self.assertEqual('revision_output', refund_history_response.revision)\n        self.assertTrue(refund_history_response.hasMore)\n\n    def test_get_status_of_subscription_renewal_date_extensions(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass/20fba8a0-2b80-4a7d-a17f-85c1854727f8/com.example.product', \n                                           {},\n                                           None)\n\n        mass_extend_renewal_date_status_response = client.get_status_of_subscription_renewal_date_extensions('com.example.product', '20fba8a0-2b80-4a7d-a17f-85c1854727f8')\n\n        self.assertIsNotNone(mass_extend_renewal_date_status_response)\n        self.assertEqual('20fba8a0-2b80-4a7d-a17f-85c1854727f8', mass_extend_renewal_date_status_response.requestIdentifier)\n        self.assertTrue(mass_extend_renewal_date_status_response.complete)\n        self.assertEqual(1698148900000, mass_extend_renewal_date_status_response.completeDate)\n        self.assertEqual(30, mass_extend_renewal_date_status_response.succeededCount)\n        self.assertEqual(2, mass_extend_renewal_date_status_response.failedCount)\n\n    def test_get_test_notification_status(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getTestNotificationStatusResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/notifications/test/8cd2974c-f905-492a-bf9a-b2f47c791d19',\n                                           {},\n                                           None)\n\n        check_test_notification_response = client.get_test_notification_status('8cd2974c-f905-492a-bf9a-b2f47c791d19')\n\n        self.assertIsNotNone(check_test_notification_response)\n        self.assertEqual('signed_payload', check_test_notification_response.signedPayload)\n        sendAttemptItems = [\n                                SendAttemptItem(attemptDate=1698148900000,sendAttemptResult=SendAttemptResult.NO_RESPONSE),\n                                SendAttemptItem(attemptDate=1698148950000,sendAttemptResult=SendAttemptResult.SUCCESS)\n                            ]\n        self.assertEqual(sendAttemptItems, check_test_notification_response.sendAttempts)\n\n    def test_get_notification_history(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getNotificationHistoryResponse.json',\n                                           'POST',\n                                           'https://local-testing-base-url/inApps/v1/notifications/history', \n                                           {'paginationToken': ['a036bc0e-52b8-4bee-82fc-8c24cb6715d6']},\n                                           {'startDate': 1698148900000, 'endDate': 1698148950000, 'notificationType': 'SUBSCRIBED', 'notificationSubtype': 'INITIAL_BUY', 'transactionId': '999733843', 'onlyFailures': True})\n\n        notification_history_request = NotificationHistoryRequest(\n            startDate=1698148900000,\n            endDate=1698148950000,\n            notificationType=NotificationTypeV2.SUBSCRIBED,\n            notificationSubtype=Subtype.INITIAL_BUY,\n            transactionId='999733843',\n            onlyFailures=True\n        )\n        \n        notification_history_response = client.get_notification_history('a036bc0e-52b8-4bee-82fc-8c24cb6715d6', notification_history_request)\n\n        self.assertIsNotNone(notification_history_response)\n        self.assertEqual('57715481-805a-4283-8499-1c19b5d6b20a', notification_history_response.paginationToken)\n        self.assertTrue(notification_history_response.hasMore)\n        expected_notification_history = [\n             NotificationHistoryResponseItem(sendAttempts=[\n                  SendAttemptItem(\n                       attemptDate=1698148900000,\n                       sendAttemptResult=SendAttemptResult.NO_RESPONSE\n                  ),\n                  SendAttemptItem(\n                       attemptDate=1698148950000,\n                       rawSendAttemptResult='SUCCESS'\n                  )\n             ], signedPayload='signed_payload_one'),\n             NotificationHistoryResponseItem(sendAttempts=[\n                  SendAttemptItem(\n                       attemptDate=1698148800000,\n                        sendAttemptResult=SendAttemptResult.CIRCULAR_REDIRECT\n                    )\n             ], signedPayload='signed_payload_two')\n        ]\n        self.assertEqual(expected_notification_history, notification_history_response.notificationHistory)\n\n    def test_get_transaction_history_v1(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/history/1234', \n                                           {'revision': ['revision_input'],\n                                            'startDate': ['123455'],\n                                            'endDate': ['123456'],\n                                            'productId': ['com.example.1', 'com.example.2'],\n                                            'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],\n                                            'sort': ['ASCENDING'],\n                                            'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],\n                                            'inAppOwnershipType': ['FAMILY_SHARED'],\n                                            'revoked': ['False']},\n                                            None)\n\n        request = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],\n            endDate=123456,\n            startDate=123455,\n            revoked=False,\n            inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,\n            productIds=['com.example.1', 'com.example.2'],\n            subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']\n        )\n\n        history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1)\n\n        self.assertIsNotNone(history_response)\n        self.assertEqual('revision_output', history_response.revision)\n        self.assertTrue(history_response.hasMore)\n        self.assertEqual('com.example', history_response.bundleId)\n        self.assertEqual(323232, history_response.appAppleId)\n        self.assertEqual(Environment.LOCAL_TESTING, history_response.environment)\n        self.assertEqual('LocalTesting', history_response.rawEnvironment)\n        self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions)\n\n    def test_get_transaction_history_v2(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v2/history/1234', \n                                           {'revision': ['revision_input'],\n                                            'startDate': ['123455'],\n                                            'endDate': ['123456'],\n                                            'productId': ['com.example.1', 'com.example.2'],\n                                            'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],\n                                            'sort': ['ASCENDING'],\n                                            'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],\n                                            'inAppOwnershipType': ['FAMILY_SHARED'],\n                                            'revoked': ['False']},\n                                            None)\n\n        request = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],\n            endDate=123456,\n            startDate=123455,\n            revoked=False,\n            inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,\n            productIds=['com.example.1', 'com.example.2'],\n            subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']\n        )\n\n        history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2)\n\n        self.assertIsNotNone(history_response)\n        self.assertEqual('revision_output', history_response.revision)\n        self.assertTrue(history_response.hasMore)\n        self.assertEqual('com.example', history_response.bundleId)\n        self.assertEqual(323232, history_response.appAppleId)\n        self.assertEqual(Environment.LOCAL_TESTING, history_response.environment)\n        self.assertEqual('LocalTesting', history_response.rawEnvironment)\n        self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions)\n\n    def test_get_transaction_info(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionInfoResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/transactions/1234', \n                                           {},\n                                           None)\n\n        transaction_info_response = client.get_transaction_info('1234')\n\n        self.assertIsNotNone(transaction_info_response)\n        self.assertEqual('signed_transaction_info_value', transaction_info_response.signedTransactionInfo)\n\n    def test_look_up_order_id(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/lookupOrderIdResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/lookup/W002182',\n                                           {},\n                                           None)\n\n        order_lookup_response = client.look_up_order_id('W002182')\n\n        self.assertIsNotNone(order_lookup_response)\n        self.assertEqual(OrderLookupStatus.INVALID, order_lookup_response.status)\n        self.assertEqual(1, order_lookup_response.rawStatus)\n        self.assertEqual(['signed_transaction_one', 'signed_transaction_two'], order_lookup_response.signedTransactions)\n\n    def test_request_test_notification(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/requestTestNotificationResponse.json',\n                                           'POST',\n                                           'https://local-testing-base-url/inApps/v1/notifications/test',\n                                           {},\n                                           None)\n\n        send_test_notification_response = client.request_test_notification()\n\n        self.assertIsNotNone(send_test_notification_response)\n        self.assertEqual('ce3af791-365e-4c60-841b-1674b43c1609', send_test_notification_response.testNotificationToken)\n\n    def test_send_consumption_data(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/transactions/consumption/49571273',\n                                           {},\n                                           {'customerConsented': True,\n                                            'consumptionStatus': 1,\n                                            'platform': 2,\n                                            'sampleContentProvided': False,\n                                            'deliveryStatus': 3,\n                                            'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813',\n                                            'accountTenure': 4,\n                                            'playTime': 5,\n                                            'lifetimeDollarsRefunded': 6,\n                                            'lifetimeDollarsPurchased': 7,\n                                            'userStatus': 4,\n                                            'refundPreference': 3})\n\n        consumptionRequest = ConsumptionRequestV1(\n            customerConsented=True,\n            consumptionStatus=ConsumptionStatus.NOT_CONSUMED,\n            platform=Platform.NON_APPLE,\n            sampleContentProvided=False,\n            deliveryStatus=DeliveryStatusV1.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE,\n            appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813',\n            accountTenure=AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS,\n            playTime=PlayTime.ONE_DAY_TO_FOUR_DAYS,\n            lifetimeDollarsRefunded=LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS,\n            lifetimeDollarsPurchased=LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER,\n            userStatus=UserStatus.LIMITED_ACCESS,\n            refundPreference=RefundPreferenceV1.NO_PREFERENCE\n        )\n\n        client.send_consumption_data('49571273', consumptionRequest)\n\n    def test_send_consumption_information(self):\n        client = self.get_client_with_body(b'',\n                                    'PUT',\n                                    'https://local-testing-base-url/inApps/v2/transactions/consumption/49571273',\n                                    {},\n                                    {\n                                        'customerConsented': True,\n                                        'sampleContentProvided': False,\n                                        'deliveryStatus': 'DELIVERED',\n                                        'consumptionPercentage': 50000,\n                                        'refundPreference': 'GRANT_FULL'\n                                    })\n        consumptionRequest = ConsumptionRequest(\n            customerConsented=True,\n            sampleContentProvided=False,\n            deliveryStatus=DeliveryStatus.DELIVERED,\n            consumptionPercentage=50000,\n            refundPreference=RefundPreference.GRANT_FULL\n        )\n    \n        client.send_consumption_information('49571273', consumptionRequest)\n\n    def test_api_error(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/apiException.json',\n                                                     'POST',\n                                                     'https://local-testing-base-url/inApps/v1/notifications/test',\n                                                     {},\n                                                     None,\n                                                     500)\n        try:\n            client.request_test_notification()\n        except APIException as e:\n            self.assertEqual(500, e.http_status_code)\n            self.assertEqual(5000000, e.raw_api_error)\n            self.assertEqual(APIError.GENERAL_INTERNAL, e.api_error)\n            self.assertEqual(\"An unknown error occurred.\", e.error_message)\n            return\n        \n        self.assertFalse(True)\n\n    def test_xcode_not_supported_error(self):\n        try:\n            signing_key = self.get_signing_key()\n            AppStoreServerAPIClient(signing_key, 'keyId', 'issuerId', 'com.example', Environment.XCODE)\n        except ValueError as e:\n            self.assertEqual(\"Xcode is not a supported environment for an AppStoreServerAPIClient\", e.args[0])\n            return\n\n        self.assertFalse(True)\n    \n    def test_api_too_many_requests(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/apiTooManyRequestsException.json',\n                                                     'POST',\n                                                     'https://local-testing-base-url/inApps/v1/notifications/test',\n                                                     {},\n                                                     None,\n                                                     429)\n        try:\n            client.request_test_notification()\n        except APIException as e:\n            self.assertEqual(429, e.http_status_code)\n            self.assertEqual(4290000, e.raw_api_error)\n            self.assertEqual(APIError.RATE_LIMIT_EXCEEDED, e.api_error)\n            self.assertEqual(\"Rate limit exceeded.\", e.error_message)\n            return\n        \n        self.assertFalse(True)\n\n    def test_unknown_error(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/apiUnknownError.json',\n                                                     'POST',\n                                                     'https://local-testing-base-url/inApps/v1/notifications/test',\n                                                     {},\n                                                     None,\n                                                     400)\n        try:\n            client.request_test_notification()\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(9990000, e.raw_api_error)\n            self.assertIsNone(e.api_error)\n            self.assertEqual(\"Testing error.\", e.error_message)\n            return\n        \n        self.assertFalse(True)\n\n    def test_get_transaction_history_with_unknown_environment(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v2/history/1234', \n                                           {'revision': ['revision_input'],\n                                            'startDate': ['123455'],\n                                            'endDate': ['123456'],\n                                            'productId': ['com.example.1', 'com.example.2'],\n                                            'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],\n                                            'sort': ['ASCENDING'],\n                                            'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],\n                                            'inAppOwnershipType': ['FAMILY_SHARED'],\n                                            'revoked': ['False']},\n                                            None)\n\n        request = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],\n            endDate=123456,\n            startDate=123455,\n            revoked=False,\n            inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,\n            productIds=['com.example.1', 'com.example.2'],\n            subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']\n        )\n\n        history_response = client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2)\n\n        self.assertIsNone(history_response.environment)\n        self.assertEqual(\"LocalTestingxxx\", history_response.rawEnvironment)\n\n    def test_get_transaction_history_with_malformed_app_apple_id(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedAppAppleId.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/history/1234', \n                                           {'revision': ['revision_input'],\n                                            'startDate': ['123455'],\n                                            'endDate': ['123456'],\n                                            'productId': ['com.example.1', 'com.example.2'],\n                                            'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],\n                                            'sort': ['ASCENDING'],\n                                            'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],\n                                            'inAppOwnershipType': ['FAMILY_SHARED'],\n                                            'revoked': ['False']},\n                                            None)\n\n        request = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],\n            endDate=123456,\n            startDate=123455,\n            revoked=False,\n            inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,\n            productIds=['com.example.1', 'com.example.2'],\n            subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']\n        )\n\n        try:\n            client.get_transaction_history('1234', 'revision_input', request)\n        except Exception:\n            return\n        \n        self.assertFalse(True)\n\n    def test_set_app_account_token(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken',\n                                           {},\n                                           {\n                                               'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813'\n                                           })\n        update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813')\n        client.set_app_account_token('49571273', update_app_account_token_request)\n\n    def test_invalid_app_account_token_error(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/invalidAppAccountTokenUUIDError.json',\n                                                     'PUT',\n                                                     'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken',\n                                                     {},\n                                                     None,\n                                                     400)\n        try:\n            client.set_app_account_token('49571273', None)\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(4000183, e.raw_api_error)\n            self.assertEqual(APIError.INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR, e.api_error)\n            self.assertEqual(\"Invalid request. The app account token field must be a valid UUID.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n    def test_family_transaction_not_supported_error(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/familyTransactionNotSupportedError.json',\n                                                     'PUT',\n                                                     'https://local-testing-base-url/inApps/v1/transactions/1234'\n                                                     '/appAccountToken',\n                                                     {},\n                                                     None,\n                                                     400)\n        try:\n            client.set_app_account_token('1234', None)\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(4000185, e.raw_api_error)\n            self.assertEqual(APIError.FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR, e.api_error)\n            self.assertEqual(\"Invalid request. Family Sharing transactions aren't supported by this endpoint.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n    def test_transaction_id_not_original_transaction_id_error(self):\n        client = self.get_client_with_body_from_file(\n            'tests/resources/models/transactionIdNotOriginalTransactionId.json',\n            'PUT',\n            'https://local-testing-base-url/inApps/v1/transactions/1234'\n            '/appAccountToken',\n            {},\n            None,\n            400)\n        try:\n            client.set_app_account_token('1234', None)\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(4000187, e.raw_api_error)\n            self.assertEqual(APIError.TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR, e.api_error)\n            self.assertEqual(\"Invalid request. The transaction ID provided is not an original transaction ID.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n\n    def test_upload_image(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           None,\n                                           200,\n                                           bytes([1, 2, 3]),\n                                           'image/png')\n        client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3]))\n\n    def test_delete_image(self):\n        client = self.get_client_with_body(b'',\n                                           'DELETE',\n                                           'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           None)\n        client.delete_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'))\n\n    def test_get_image_list(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getImageListResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/image/list',\n                                           {},\n                                           None)\n        response = client.get_image_list()\n        self.assertIsNotNone(response)\n        self.assertEqual(1, len(response.imageIdentifiers))\n        self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.imageIdentifiers[0].imageIdentifier)\n        self.assertEqual(ImageState.APPROVED, response.imageIdentifiers[0].imageState)\n        self.assertEqual(ImageSize.FULL_SIZE, response.imageIdentifiers[0].imageSize)\n\n    def test_upload_message(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           {'header': 'Header text', 'body': 'Body text'})\n        upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text')\n        client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body)\n\n    def test_upload_message_with_image(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           {'header': 'Header text', 'body': 'Body text', 'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'}})\n        image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text')\n        upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image)\n        client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body)\n\n    def test_delete_message(self):\n        client = self.get_client_with_body(b'',\n                                           'DELETE',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           None)\n        client.delete_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'))\n\n    def test_get_message_list(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getMessageListResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/list',\n                                           {},\n                                           None)\n        response = client.get_message_list()\n        self.assertIsNotNone(response)\n        self.assertEqual(1, len(response.messageIdentifiers))\n        self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifiers[0].messageIdentifier)\n        self.assertEqual(MessageState.APPROVED, response.messageIdentifiers[0].messageState)\n\n    def test_configure_default_message(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US',\n                                           {},\n                                           {'messageIdentifier': 'a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'})\n        default_configuration_request = DefaultConfigurationRequest(messageIdentifier=UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'))\n        client.configure_default_message('com.example.product', 'en-US', default_configuration_request)\n\n    def test_delete_default_message(self):\n        client = self.get_client_with_body(b'',\n                                           'DELETE',\n                                           'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US',\n                                           {},\n                                           None)\n        client.delete_default_message('com.example.product', 'en-US')\n\n    def test_get_default_message(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getDefaultMessageResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US',\n                                           {},\n                                           None)\n        response = client.get_default_message('com.example.product', 'en-US')\n        self.assertIsNotNone(response)\n        self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifier)\n\n    def test_upload_image_with_image_size(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {'imageSize': ['FULL_SIZE']},\n                                           None,\n                                           200,\n                                           bytes([1, 2, 3]),\n                                           'image/png')\n        client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3]), ImageSize.FULL_SIZE)\n\n    def test_configure_realtime_url(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/realtime/url',\n                                           {},\n                                           {'realtimeURL': 'https://example.com/realtime'})\n        realtime_url_request = RealtimeUrlRequest(realtimeURL='https://example.com/realtime')\n        client.configure_realtime_url(realtime_url_request)\n\n    def test_delete_realtime_url(self):\n        client = self.get_client_with_body(b'',\n                                           'DELETE',\n                                           'https://local-testing-base-url/inApps/v1/messaging/realtime/url',\n                                           {},\n                                           None)\n        client.delete_realtime_url()\n\n    def test_get_realtime_url(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getRealtimeUrlResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/realtime/url',\n                                           {},\n                                           None)\n        response = client.get_realtime_url()\n        self.assertIsNotNone(response)\n        self.assertEqual('https://example.com/realtime', response.realtimeURL)\n\n    def test_upload_message_with_bullet_points(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           {'header': 'Header text', 'body': 'Body text',\n                                            'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'},\n                                            'headerPosition': 'ABOVE_IMAGE',\n                                            'bulletPoints': [{'text': 'Bullet 1', 'imageIdentifier': 'c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012', 'altText': 'Bullet alt'}]})\n        image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text')\n        bullet_point = BulletPoint(text='Bullet 1', imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt')\n        upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image, headerPosition=HeaderPosition.ABOVE_IMAGE, bulletPoints=[bullet_point])\n        client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body)\n\n    def test_initiate_performance_test(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResponse.json',\n                                           'POST',\n                                           'https://local-testing-base-url/inApps/v1/messaging/performanceTest',\n                                           {},\n                                           {'originalTransactionId': '70000500092808'})\n        performance_test_request = PerformanceTestRequest(originalTransactionId='70000500092808')\n        response = client.initiate_performance_test(performance_test_request)\n        self.assertIsNotNone(response)\n        self.assertEqual('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', response.requestId)\n        self.assertIsNotNone(response.config)\n        self.assertEqual(10, response.config.maxConcurrentRequests)\n        self.assertEqual(100, response.config.totalRequests)\n        self.assertEqual(60000, response.config.totalDuration)\n        self.assertEqual(500, response.config.responseTimeThreshold)\n        self.assertEqual(95, response.config.successRateThreshold)\n\n    def test_get_performance_test_results(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResultResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/performanceTest/result/c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d',\n                                           {},\n                                           None)\n        response = client.get_performance_test_results('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d')\n        self.assertIsNotNone(response)\n        self.assertIsNotNone(response.config)\n        self.assertEqual(10, response.config.maxConcurrentRequests)\n        self.assertEqual(100, response.config.totalRequests)\n        self.assertEqual(60000, response.config.totalDuration)\n        self.assertEqual(500, response.config.responseTimeThreshold)\n        self.assertEqual(95, response.config.successRateThreshold)\n        self.assertEqual('https://example.com/retention', response.target)\n        self.assertEqual(PerformanceTestStatus.PASS, response.result)\n        self.assertEqual('PASS', response.rawResult)\n        self.assertEqual(98, response.successRate)\n        self.assertEqual(0, response.numPending)\n        self.assertIsNotNone(response.responseTimes)\n        self.assertEqual(120, response.responseTimes.average)\n        self.assertEqual(100, response.responseTimes.p50)\n        self.assertEqual(200, response.responseTimes.p90)\n        self.assertEqual(250, response.responseTimes.p95)\n        self.assertEqual(400, response.responseTimes.p99)\n        self.assertEqual({SendAttemptResult.TIMED_OUT: 1, SendAttemptResult.NO_RESPONSE: 1}, response.failures)\n        self.assertEqual({'TIMED_OUT': 1, 'NO_RESPONSE': 1}, response.rawFailures)\n\n    def test_get_app_transaction_info_success(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/appTransactionInfoResponse.json',\n                                            'GET',\n                                            'https://local-testing-base-url/inApps/v1/transactions/appTransactions/1234',\n                                            {},\n                                            None)\n\n        app_transaction_info_response = client.get_app_transaction_info('1234')\n\n        self.assertIsNotNone(app_transaction_info_response)\n        self.assertEqual('signed_app_transaction_info_value', app_transaction_info_response.signedAppTransactionInfo)\n\n    def test_get_app_transaction_info_invalid_transaction_id(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/invalidTransactionIdError.json',\n                                                      'GET',\n                                                      'https://local-testing-base-url/inApps/v1/transactions/appTransactions/invalid_id',\n                                                      {},\n                                                      None,\n                                                      400)\n        try:\n            client.get_app_transaction_info('invalid_id')\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(4000006, e.raw_api_error)\n            self.assertEqual(APIError.INVALID_TRANSACTION_ID, e.api_error)\n            self.assertEqual(\"Invalid transaction id.\", e.error_message)\n            return\n        self.assertFalse(True)\n\n    def test_get_app_transaction_info_app_transaction_does_not_exist(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/appTransactionDoesNotExistError.json',\n                                                      'GET',\n                                                      'https://local-testing-base-url/inApps/v1/transactions/appTransactions/nonexistent_id',\n                                                      {},\n                                                      None,\n                                                      404)\n        try:\n            client.get_app_transaction_info('nonexistent_id')\n        except APIException as e:\n             self.assertEqual(404, e.http_status_code)\n             self.assertEqual(4040019, e.raw_api_error)\n             self.assertEqual(APIError.APP_TRANSACTION_DOES_NOT_EXIST_ERROR, e.api_error)\n             self.assertEqual(\"No AppTransaction exists for the customer.\", e.error_message)\n             return\n\n        self.assertFalse(True)\n\n    def test_get_app_transaction_info_transaction_id_not_found(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionIdNotFoundError.json',\n                                                      'GET',\n                                                      'https://local-testing-base-url/inApps/v1/transactions/appTransactions/not_found_id',\n                                                      {},\n                                                      None,\n                                                      404)\n        try:\n            client.get_app_transaction_info('not_found_id')\n        except APIException as e:\n            self.assertEqual(404, e.http_status_code)\n            self.assertEqual(4040010, e.raw_api_error)\n            self.assertEqual(APIError.TRANSACTION_ID_NOT_FOUND, e.api_error)\n            self.assertEqual(\"Transaction id not found.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n\n    def get_signing_key(self):\n        return read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n\n    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):\n        signing_key = self.get_signing_key()\n        client = AppStoreServerAPIClient(signing_key, 'keyId', 'issuerId', 'com.example', Environment.LOCAL_TESTING)\n        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):\n            self.assertEqual(expected_method, method)\n            self.assertEqual(expected_url, url)\n            self.assertEqual(expected_params, params)\n            self.assertTrue(headers['User-Agent'].startswith('app-store-server-library/python'))\n            self.assertTrue(headers['Authorization'].startswith('Bearer '))\n            self.assertEqual('application/json', headers['Accept'])\n            decoded_jwt = decode_json_from_signed_date(headers['Authorization'][7:])\n            self.assertEqual('appstoreconnect-v1', decoded_jwt['payload']['aud'])\n            self.assertEqual('issuerId', decoded_jwt['payload']['iss'])\n            self.assertEqual('keyId', decoded_jwt['header']['kid'])\n            self.assertEqual('com.example', decoded_jwt['payload']['bid'])\n\n            # Content-specific validation\n            if expected_data is not None:\n                self.assertEqual(['User-Agent', 'Authorization', 'Accept', 'Content-Type'], list(headers.keys()))\n                self.assertEqual(expected_content_type, headers['Content-Type'])\n                self.assertIsNone(json)\n                self.assertEqual(expected_data, data)\n            else:\n                self.assertEqual(['User-Agent', 'Authorization', 'Accept'], list(headers.keys()))\n                self.assertEqual(expected_json, json)\n\n            response = Response()\n            response.status_code = status_code\n            response.raw = BytesIO(body)\n            response.headers['Content-Type'] = 'application/json'\n            return response\n\n        client._execute_request = fake_execute_and_validate_inputs\n        return client\n\n    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):\n        body = read_data_from_binary_file(path)\n        return self.get_client_with_body(body, expected_method, expected_url, expected_params, expected_json, status_code)\n"
  },
  {
    "path": "tests/test_api_client_async.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom typing import Any, Dict, List, Union\nimport unittest\n\nfrom httpx import Response\n\nfrom appstoreserverlibrary.api_client import APIError, APIException, AsyncAppStoreServerAPIClient, GetTransactionHistoryVersion\nfrom appstoreserverlibrary.models.AccountTenure import AccountTenure\nfrom appstoreserverlibrary.models.AppTransactionInfoResponse import AppTransactionInfoResponse\nfrom appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus\nfrom appstoreserverlibrary.models.ConsumptionRequest import ConsumptionRequest\nfrom appstoreserverlibrary.models.ConsumptionRequestV1 import ConsumptionRequestV1\nfrom appstoreserverlibrary.models.ConsumptionStatus import ConsumptionStatus\nfrom appstoreserverlibrary.models.DeliveryStatus import DeliveryStatus\nfrom appstoreserverlibrary.models.DeliveryStatusV1 import DeliveryStatusV1\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent\nfrom appstoreserverlibrary.models.ExtendReasonCode import ExtendReasonCode\nfrom appstoreserverlibrary.models.ExtendRenewalDateRequest import ExtendRenewalDateRequest\nfrom appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType\nfrom appstoreserverlibrary.models.LastTransactionsItem import LastTransactionsItem\nfrom appstoreserverlibrary.models.LifetimeDollarsPurchased import LifetimeDollarsPurchased\nfrom appstoreserverlibrary.models.LifetimeDollarsRefunded import LifetimeDollarsRefunded\nfrom appstoreserverlibrary.models.MassExtendRenewalDateRequest import MassExtendRenewalDateRequest\nfrom appstoreserverlibrary.models.NotificationHistoryRequest import NotificationHistoryRequest\nfrom appstoreserverlibrary.models.NotificationHistoryResponseItem import NotificationHistoryResponseItem\nfrom appstoreserverlibrary.models.NotificationTypeV2 import NotificationTypeV2\nfrom appstoreserverlibrary.models.OfferType import OfferType\nfrom appstoreserverlibrary.models.OrderLookupStatus import OrderLookupStatus\nfrom appstoreserverlibrary.models.Platform import Platform\nfrom appstoreserverlibrary.models.PlayTime import PlayTime\nfrom appstoreserverlibrary.models.PriceIncreaseStatus import PriceIncreaseStatus\nfrom appstoreserverlibrary.models.RefundPreference import RefundPreference\nfrom appstoreserverlibrary.models.RefundPreferenceV1 import RefundPreferenceV1\nfrom appstoreserverlibrary.models.RevocationReason import RevocationReason\nfrom appstoreserverlibrary.models.SendAttemptItem import SendAttemptItem\nfrom appstoreserverlibrary.models.SendAttemptResult import SendAttemptResult\nfrom appstoreserverlibrary.models.Status import Status\nfrom appstoreserverlibrary.models.SubscriptionGroupIdentifierItem import SubscriptionGroupIdentifierItem\nfrom appstoreserverlibrary.models.Subtype import Subtype\nfrom appstoreserverlibrary.models.TransactionHistoryRequest import Order, ProductType, TransactionHistoryRequest\nfrom appstoreserverlibrary.models.TransactionReason import TransactionReason\nfrom appstoreserverlibrary.models.Type import Type\nfrom appstoreserverlibrary.models.UpdateAppAccountTokenRequest import UpdateAppAccountTokenRequest\nfrom appstoreserverlibrary.models.UserStatus import UserStatus\nfrom appstoreserverlibrary.models.DefaultConfigurationRequest import DefaultConfigurationRequest\nfrom appstoreserverlibrary.models.HeaderPosition import HeaderPosition\nfrom appstoreserverlibrary.models.ImageSize import ImageSize\nfrom appstoreserverlibrary.models.ImageState import ImageState\nfrom appstoreserverlibrary.models.MessageState import MessageState\nfrom appstoreserverlibrary.models.BulletPoint import BulletPoint\nfrom appstoreserverlibrary.models.PerformanceTestRequest import PerformanceTestRequest\nfrom appstoreserverlibrary.models.PerformanceTestStatus import PerformanceTestStatus\nfrom appstoreserverlibrary.models.RealtimeUrlRequest import RealtimeUrlRequest\nfrom appstoreserverlibrary.models.UploadMessageImage import UploadMessageImage\nfrom appstoreserverlibrary.models.UploadMessageRequestBody import UploadMessageRequestBody\nfrom uuid import UUID\n\nfrom tests.util import decode_json_from_signed_date, read_data_from_binary_file, read_data_from_file\n\nfrom io import BytesIO\n\nclass DecodedPayloads(unittest.IsolatedAsyncioTestCase):\n    async def test_extend_renewal_date_for_all_active_subscribers(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/extendRenewalDateForAllActiveSubscribersResponse.json',\n                                           'POST',\n                                           'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass',\n                                           {}, \n                                           {'extendByDays': 45, 'extendReasonCode': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac', 'storefrontCountryCodes': ['USA', 'MEX'], 'productId': 'com.example.productId'})\n        \n        extend_renewal_date_request = MassExtendRenewalDateRequest(\n             extendByDays=45,\n             extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION,\n             requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac',\n             storefrontCountryCodes=['USA', 'MEX'],\n             productId='com.example.productId')\n\n        mass_extend_renewal_date_response = await client.extend_renewal_date_for_all_active_subscribers(extend_renewal_date_request)\n\n        self.assertIsNotNone(mass_extend_renewal_date_response)\n        self.assertEqual('758883e8-151b-47b7-abd0-60c4d804c2f5', mass_extend_renewal_date_response.requestIdentifier)\n\n    async def test_extend_subscription_renewal_date(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/extendSubscriptionRenewalDateResponse.json',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/subscriptions/extend/4124214',\n                                           {},\n                                           {'extendByDays': 45, 'extendReasonCode': 1, 'requestIdentifier': 'fdf964a4-233b-486c-aac1-97d8d52688ac'})\n\n        extend_renewal_date_request = ExtendRenewalDateRequest(\n             extendByDays=45,\n             extendReasonCode=ExtendReasonCode.CUSTOMER_SATISFACTION,\n             requestIdentifier='fdf964a4-233b-486c-aac1-97d8d52688ac'\n        )\n\n        extend_renewal_date_response = await client.extend_subscription_renewal_date('4124214', extend_renewal_date_request)\n\n        self.assertIsNotNone(extend_renewal_date_response)\n        self.assertEqual('2312412', extend_renewal_date_response.originalTransactionId)\n        self.assertEqual('9993', extend_renewal_date_response.webOrderLineItemId)\n        self.assertTrue(extend_renewal_date_response.success)\n        self.assertEqual(1698148900000, extend_renewal_date_response.effectiveDate)\n\n    async def test_get_all_subscription_statuses(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getAllSubscriptionStatusesResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/subscriptions/4321', \n                                           {'status': [2, 1]},\n                                           None)\n\n        status_response = await client.get_all_subscription_statuses('4321', [Status.EXPIRED, Status.ACTIVE])\n\n        self.assertIsNotNone(status_response)\n        self.assertEqual(Environment.LOCAL_TESTING, status_response.environment)\n        self.assertEqual('LocalTesting', status_response.rawEnvironment)\n        self.assertEqual('com.example', status_response.bundleId)\n        self.assertEqual(5454545, status_response.appAppleId)\n\n\n        expected_body = [\n             SubscriptionGroupIdentifierItem(\n                subscriptionGroupIdentifier='sub_group_one',\n                lastTransactions=[\n                    LastTransactionsItem(\n                        status=Status.ACTIVE,\n                        originalTransactionId='3749183',\n                        signedTransactionInfo='signed_transaction_one',\n                        signedRenewalInfo='signed_renewal_one'\n                    ),\n                    LastTransactionsItem(\n                        status=Status.REVOKED,\n                        originalTransactionId='5314314134',\n                        signedTransactionInfo='signed_transaction_two',\n                        signedRenewalInfo='signed_renewal_two'\n                    )\n                ]\n            ),\n            SubscriptionGroupIdentifierItem(\n                 subscriptionGroupIdentifier='sub_group_two',\n                 lastTransactions=[\n                      LastTransactionsItem(\n                           status=Status.EXPIRED,\n                           originalTransactionId='3413453',\n                           signedTransactionInfo='signed_transaction_three',\n                           signedRenewalInfo='signed_renewal_three'\n                      )\n                 ]\n            )\n        ]\n        self.assertEqual(expected_body, status_response.data)\n\n    async def test_get_refund_history(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getRefundHistoryResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v2/refund/lookup/555555', \n                                           {'revision': ['revision_input']}, \n                                           None)\n\n        refund_history_response = await client.get_refund_history('555555', 'revision_input')\n\n        self.assertIsNotNone(refund_history_response)\n        self.assertEqual(['signed_transaction_one', 'signed_transaction_two'], refund_history_response.signedTransactions)\n        self.assertEqual('revision_output', refund_history_response.revision)\n        self.assertTrue(refund_history_response.hasMore)\n\n    async def test_get_status_of_subscription_renewal_date_extensions(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getStatusOfSubscriptionRenewalDateExtensionsResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/subscriptions/extend/mass/20fba8a0-2b80-4a7d-a17f-85c1854727f8/com.example.product', \n                                           {},\n                                           None)\n\n        mass_extend_renewal_date_status_response = await client.get_status_of_subscription_renewal_date_extensions('com.example.product', '20fba8a0-2b80-4a7d-a17f-85c1854727f8')\n\n        self.assertIsNotNone(mass_extend_renewal_date_status_response)\n        self.assertEqual('20fba8a0-2b80-4a7d-a17f-85c1854727f8', mass_extend_renewal_date_status_response.requestIdentifier)\n        self.assertTrue(mass_extend_renewal_date_status_response.complete)\n        self.assertEqual(1698148900000, mass_extend_renewal_date_status_response.completeDate)\n        self.assertEqual(30, mass_extend_renewal_date_status_response.succeededCount)\n        self.assertEqual(2, mass_extend_renewal_date_status_response.failedCount)\n\n    async def test_get_test_notification_status(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getTestNotificationStatusResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/notifications/test/8cd2974c-f905-492a-bf9a-b2f47c791d19',\n                                           {},\n                                           None)\n\n        check_test_notification_response = await client.get_test_notification_status('8cd2974c-f905-492a-bf9a-b2f47c791d19')\n\n        self.assertIsNotNone(check_test_notification_response)\n        self.assertEqual('signed_payload', check_test_notification_response.signedPayload)\n        sendAttemptItems = [\n                                SendAttemptItem(attemptDate=1698148900000,sendAttemptResult=SendAttemptResult.NO_RESPONSE),\n                                SendAttemptItem(attemptDate=1698148950000,sendAttemptResult=SendAttemptResult.SUCCESS)\n                            ]\n        self.assertEqual(sendAttemptItems, check_test_notification_response.sendAttempts)\n\n    async def test_get_notification_history(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getNotificationHistoryResponse.json',\n                                           'POST',\n                                           'https://local-testing-base-url/inApps/v1/notifications/history', \n                                           {'paginationToken': ['a036bc0e-52b8-4bee-82fc-8c24cb6715d6']},\n                                           {'startDate': 1698148900000, 'endDate': 1698148950000, 'notificationType': 'SUBSCRIBED', 'notificationSubtype': 'INITIAL_BUY', 'transactionId': '999733843', 'onlyFailures': True})\n\n        notification_history_request = NotificationHistoryRequest(\n            startDate=1698148900000,\n            endDate=1698148950000,\n            notificationType=NotificationTypeV2.SUBSCRIBED,\n            notificationSubtype=Subtype.INITIAL_BUY,\n            transactionId='999733843',\n            onlyFailures=True\n        )\n        \n        notification_history_response = await client.get_notification_history('a036bc0e-52b8-4bee-82fc-8c24cb6715d6', notification_history_request)\n\n        self.assertIsNotNone(notification_history_response)\n        self.assertEqual('57715481-805a-4283-8499-1c19b5d6b20a', notification_history_response.paginationToken)\n        self.assertTrue(notification_history_response.hasMore)\n        expected_notification_history = [\n             NotificationHistoryResponseItem(sendAttempts=[\n                  SendAttemptItem(\n                       attemptDate=1698148900000,\n                       sendAttemptResult=SendAttemptResult.NO_RESPONSE\n                  ),\n                  SendAttemptItem(\n                       attemptDate=1698148950000,\n                       rawSendAttemptResult='SUCCESS'\n                  )\n             ], signedPayload='signed_payload_one'),\n             NotificationHistoryResponseItem(sendAttempts=[\n                  SendAttemptItem(\n                       attemptDate=1698148800000,\n                        sendAttemptResult=SendAttemptResult.CIRCULAR_REDIRECT\n                    )\n             ], signedPayload='signed_payload_two')\n        ]\n        self.assertEqual(expected_notification_history, notification_history_response.notificationHistory)\n\n    async def test_get_transaction_history_v1(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/history/1234', \n                                           {'revision': ['revision_input'],\n                                            'startDate': ['123455'],\n                                            'endDate': ['123456'],\n                                            'productId': ['com.example.1', 'com.example.2'],\n                                            'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],\n                                            'sort': ['ASCENDING'],\n                                            'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],\n                                            'inAppOwnershipType': ['FAMILY_SHARED'],\n                                            'revoked': ['False']},\n                                            None)\n\n        request = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],\n            endDate=123456,\n            startDate=123455,\n            revoked=False,\n            inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,\n            productIds=['com.example.1', 'com.example.2'],\n            subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']\n        )\n\n        history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V1)\n\n        self.assertIsNotNone(history_response)\n        self.assertEqual('revision_output', history_response.revision)\n        self.assertTrue(history_response.hasMore)\n        self.assertEqual('com.example', history_response.bundleId)\n        self.assertEqual(323232, history_response.appAppleId)\n        self.assertEqual(Environment.LOCAL_TESTING, history_response.environment)\n        self.assertEqual('LocalTesting', history_response.rawEnvironment)\n        self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions)\n\n    async def test_get_transaction_history_v2(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v2/history/1234', \n                                           {'revision': ['revision_input'],\n                                            'startDate': ['123455'],\n                                            'endDate': ['123456'],\n                                            'productId': ['com.example.1', 'com.example.2'],\n                                            'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],\n                                            'sort': ['ASCENDING'],\n                                            'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],\n                                            'inAppOwnershipType': ['FAMILY_SHARED'],\n                                            'revoked': ['False']},\n                                            None)\n\n        request = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],\n            endDate=123456,\n            startDate=123455,\n            revoked=False,\n            inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,\n            productIds=['com.example.1', 'com.example.2'],\n            subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']\n        )\n\n        history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2)\n\n        self.assertIsNotNone(history_response)\n        self.assertEqual('revision_output', history_response.revision)\n        self.assertTrue(history_response.hasMore)\n        self.assertEqual('com.example', history_response.bundleId)\n        self.assertEqual(323232, history_response.appAppleId)\n        self.assertEqual(Environment.LOCAL_TESTING, history_response.environment)\n        self.assertEqual('LocalTesting', history_response.rawEnvironment)\n        self.assertEqual(['signed_transaction_value', 'signed_transaction_value2'], history_response.signedTransactions)\n\n    async def test_get_transaction_info(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionInfoResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/transactions/1234', \n                                           {},\n                                           None)\n\n        transaction_info_response = await client.get_transaction_info('1234')\n\n        self.assertIsNotNone(transaction_info_response)\n        self.assertEqual('signed_transaction_info_value', transaction_info_response.signedTransactionInfo)\n\n    async def test_look_up_order_id(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/lookupOrderIdResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/lookup/W002182',\n                                           {},\n                                           None)\n\n        order_lookup_response = await client.look_up_order_id('W002182')\n\n        self.assertIsNotNone(order_lookup_response)\n        self.assertEqual(OrderLookupStatus.INVALID, order_lookup_response.status)\n        self.assertEqual(1, order_lookup_response.rawStatus)\n        self.assertEqual(['signed_transaction_one', 'signed_transaction_two'], order_lookup_response.signedTransactions)\n\n    async def test_request_test_notification(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/requestTestNotificationResponse.json',\n                                           'POST',\n                                           'https://local-testing-base-url/inApps/v1/notifications/test',\n                                           {},\n                                           None)\n\n        send_test_notification_response = await client.request_test_notification()\n\n        self.assertIsNotNone(send_test_notification_response)\n        self.assertEqual('ce3af791-365e-4c60-841b-1674b43c1609', send_test_notification_response.testNotificationToken)\n\n    async def test_send_consumption_data(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/transactions/consumption/49571273',\n                                           {},\n                                           {'customerConsented': True,\n                                            'consumptionStatus': 1,\n                                            'platform': 2,\n                                            'sampleContentProvided': False,\n                                            'deliveryStatus': 3,\n                                            'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813',\n                                            'accountTenure': 4,\n                                            'playTime': 5,\n                                            'lifetimeDollarsRefunded': 6,\n                                            'lifetimeDollarsPurchased': 7,\n                                            'userStatus': 4,\n                                            'refundPreference': 3})\n\n        consumptionRequest = ConsumptionRequestV1(\n            customerConsented=True,\n            consumptionStatus=ConsumptionStatus.NOT_CONSUMED,\n            platform=Platform.NON_APPLE,\n            sampleContentProvided=False,\n            deliveryStatus=DeliveryStatusV1.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE,\n            appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813',\n            accountTenure=AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS,\n            playTime=PlayTime.ONE_DAY_TO_FOUR_DAYS,\n            lifetimeDollarsRefunded=LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS,\n            lifetimeDollarsPurchased=LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER,\n            userStatus=UserStatus.LIMITED_ACCESS,\n            refundPreference=RefundPreferenceV1.NO_PREFERENCE\n        )\n\n        await client.send_consumption_data('49571273', consumptionRequest)\n\n    async def test_send_consumption_information(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v2/transactions/consumption/49571273',\n                                           {},\n                                           {'customerConsented': True,\n                                            'sampleContentProvided': False,\n                                            'deliveryStatus': 'DELIVERED',\n                                            'consumptionPercentage': 50000,\n                                            'refundPreference': 'GRANT_FULL'})\n\n        consumptionRequest = ConsumptionRequest(\n            customerConsented=True,\n            sampleContentProvided=False,\n            deliveryStatus=DeliveryStatus.DELIVERED,\n            consumptionPercentage=50000,\n            refundPreference=RefundPreference.GRANT_FULL\n        )\n\n        await client.send_consumption_information('49571273', consumptionRequest)\n\n    async def test_api_error(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/apiException.json',\n                                                     'POST',\n                                                     'https://local-testing-base-url/inApps/v1/notifications/test',\n                                                     {},\n                                                     None,\n                                                     500)\n        try:\n            await client.request_test_notification()\n        except APIException as e:\n            self.assertEqual(500, e.http_status_code)\n            self.assertEqual(5000000, e.raw_api_error)\n            self.assertEqual(APIError.GENERAL_INTERNAL, e.api_error)\n            self.assertEqual(\"An unknown error occurred.\", e.error_message)\n            return\n        \n        self.assertFalse(True)\n\n    async def test_xcode_not_supported_error(self):\n        try:\n            signing_key = self.get_signing_key()\n            AsyncAppStoreServerAPIClient(signing_key, 'keyId', 'issuerId', 'com.example', Environment.XCODE)\n        except ValueError as e:\n            self.assertEqual(\"Xcode is not a supported environment for an AppStoreServerAPIClient\", e.args[0])\n            return\n\n        self.assertFalse(True)\n    \n    async def test_api_too_many_requests(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/apiTooManyRequestsException.json',\n                                                     'POST',\n                                                     'https://local-testing-base-url/inApps/v1/notifications/test',\n                                                     {},\n                                                     None,\n                                                     429)\n        try:\n            await client.request_test_notification()\n        except APIException as e:\n            self.assertEqual(429, e.http_status_code)\n            self.assertEqual(4290000, e.raw_api_error)\n            self.assertEqual(APIError.RATE_LIMIT_EXCEEDED, e.api_error)\n            self.assertEqual(\"Rate limit exceeded.\", e.error_message)\n            return\n        \n        self.assertFalse(True)\n\n    async def test_unknown_error(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/apiUnknownError.json',\n                                                     'POST',\n                                                     'https://local-testing-base-url/inApps/v1/notifications/test',\n                                                     {},\n                                                     None,\n                                                     400)\n        try:\n            await client.request_test_notification()\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(9990000, e.raw_api_error)\n            self.assertIsNone(e.api_error)\n            self.assertEqual(\"Testing error.\", e.error_message)\n            return\n        \n        self.assertFalse(True)\n\n    async def test_get_transaction_history_with_unknown_environment(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedEnvironment.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v2/history/1234', \n                                           {'revision': ['revision_input'],\n                                            'startDate': ['123455'],\n                                            'endDate': ['123456'],\n                                            'productId': ['com.example.1', 'com.example.2'],\n                                            'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],\n                                            'sort': ['ASCENDING'],\n                                            'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],\n                                            'inAppOwnershipType': ['FAMILY_SHARED'],\n                                            'revoked': ['False']},\n                                            None)\n\n        request = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],\n            endDate=123456,\n            startDate=123455,\n            revoked=False,\n            inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,\n            productIds=['com.example.1', 'com.example.2'],\n            subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']\n        )\n\n        history_response = await client.get_transaction_history('1234', 'revision_input', request, GetTransactionHistoryVersion.V2)\n\n        self.assertIsNone(history_response.environment)\n        self.assertEqual(\"LocalTestingxxx\", history_response.rawEnvironment)\n\n    async def test_get_transaction_history_with_malformed_app_apple_id(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionHistoryResponseWithMalformedAppAppleId.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/history/1234', \n                                           {'revision': ['revision_input'],\n                                            'startDate': ['123455'],\n                                            'endDate': ['123456'],\n                                            'productId': ['com.example.1', 'com.example.2'],\n                                            'productType': ['CONSUMABLE', 'AUTO_RENEWABLE'],\n                                            'sort': ['ASCENDING'],\n                                            'subscriptionGroupIdentifier': ['sub_group_id', 'sub_group_id_2'],\n                                            'inAppOwnershipType': ['FAMILY_SHARED'],\n                                            'revoked': ['False']},\n                                            None)\n\n        request = TransactionHistoryRequest(\n            sort=Order.ASCENDING,\n            productTypes=[ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],\n            endDate=123456,\n            startDate=123455,\n            revoked=False,\n            inAppOwnershipType=InAppOwnershipType.FAMILY_SHARED,\n            productIds=['com.example.1', 'com.example.2'],\n            subscriptionGroupIdentifiers=['sub_group_id', 'sub_group_id_2']\n        )\n\n        try:\n            await client.get_transaction_history('1234', 'revision_input', request)\n        except Exception:\n            return\n        \n        self.assertFalse(True)\n\n    async def test_set_app_account_token(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken',\n                                           {},\n                                           {\n                                               'appAccountToken': '7389a31a-fb6d-4569-a2a6-db7d85d84813'\n                                           })\n        update_app_account_token_request = UpdateAppAccountTokenRequest(appAccountToken='7389a31a-fb6d-4569-a2a6-db7d85d84813')\n        await client.set_app_account_token('49571273', update_app_account_token_request)\n\n    async def test_invalid_app_account_token_error(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/invalidAppAccountTokenUUIDError.json',\n                                                     'PUT',\n                                                     'https://local-testing-base-url/inApps/v1/transactions/49571273/appAccountToken',\n                                                     {},\n                                                     None,\n                                                     400)\n        try:\n            await client.set_app_account_token('49571273', None)\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(4000183, e.raw_api_error)\n            self.assertEqual(APIError.INVALID_APP_ACCOUNT_TOKEN_UUID_ERROR, e.api_error)\n            self.assertEqual(\"Invalid request. The app account token field must be a valid UUID.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n    async def test_family_transaction_not_supported_error(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/familyTransactionNotSupportedError.json',\n                                                     'PUT',\n                                                     'https://local-testing-base-url/inApps/v1/transactions/1234'\n                                                     '/appAccountToken',\n                                                     {},\n                                                     None,\n                                                     400)\n        try:\n            await client.set_app_account_token('1234', None)\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(4000185, e.raw_api_error)\n            self.assertEqual(APIError.FAMILY_TRANSACTION_NOT_SUPPORTED_ERROR, e.api_error)\n            self.assertEqual(\"Invalid request. Family Sharing transactions aren't supported by this endpoint.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n    async def test_transaction_id_not_original_transaction_id_error(self):\n        client = self.get_client_with_body_from_file(\n            'tests/resources/models/transactionIdNotOriginalTransactionId.json',\n            'PUT',\n            'https://local-testing-base-url/inApps/v1/transactions/1234'\n            '/appAccountToken',\n            {},\n            None,\n            400)\n        try:\n            await client.set_app_account_token('1234', None)\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(4000187, e.raw_api_error)\n            self.assertEqual(APIError.TRANSACTION_ID_IS_NOT_ORIGINAL_TRANSACTION_ID_ERROR, e.api_error)\n            self.assertEqual(\"Invalid request. The transaction ID provided is not an original transaction ID.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n    async def test_upload_image(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           None,\n                                           200,\n                                           bytes([1, 2, 3]),\n                                           'image/png')\n        await client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3]))\n\n    async def test_delete_image(self):\n        client = self.get_client_with_body(b'',\n                                           'DELETE',\n                                           'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           None)\n        await client.delete_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'))\n\n    async def test_get_image_list(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getImageListResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/image/list',\n                                           {},\n                                           None)\n        response = await client.get_image_list()\n        self.assertIsNotNone(response)\n        self.assertEqual(1, len(response.imageIdentifiers))\n        self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.imageIdentifiers[0].imageIdentifier)\n        self.assertEqual(ImageState.APPROVED, response.imageIdentifiers[0].imageState)\n        self.assertEqual(ImageSize.FULL_SIZE, response.imageIdentifiers[0].imageSize)\n\n    async def test_upload_message(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           {'header': 'Header text', 'body': 'Body text'})\n        upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text')\n        await client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body)\n\n    async def test_upload_message_with_image(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           {'header': 'Header text', 'body': 'Body text', 'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'}})\n        image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text')\n        upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image)\n        await client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body)\n\n    async def test_delete_message(self):\n        client = self.get_client_with_body(b'',\n                                           'DELETE',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           None)\n        await client.delete_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'))\n\n    async def test_get_message_list(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getMessageListResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/list',\n                                           {},\n                                           None)\n        response = await client.get_message_list()\n        self.assertIsNotNone(response)\n        self.assertEqual(1, len(response.messageIdentifiers))\n        self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifiers[0].messageIdentifier)\n        self.assertEqual(MessageState.APPROVED, response.messageIdentifiers[0].messageState)\n\n    async def test_configure_default_message(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US',\n                                           {},\n                                           {'messageIdentifier': 'a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'})\n        default_configuration_request = DefaultConfigurationRequest(messageIdentifier=UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'))\n        await client.configure_default_message('com.example.product', 'en-US', default_configuration_request)\n\n    async def test_delete_default_message(self):\n        client = self.get_client_with_body(b'',\n                                           'DELETE',\n                                           'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US',\n                                           {},\n                                           None)\n        await client.delete_default_message('com.example.product', 'en-US')\n\n    async def test_get_default_message(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getDefaultMessageResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/default/com.example.product/en-US',\n                                           {},\n                                           None)\n        response = await client.get_default_message('com.example.product', 'en-US')\n        self.assertIsNotNone(response)\n        self.assertEqual(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), response.messageIdentifier)\n\n    async def test_upload_image_with_image_size(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/image/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {'imageSize': ['FULL_SIZE']},\n                                           None,\n                                           200,\n                                           bytes([1, 2, 3]),\n                                           'image/png')\n        await client.upload_image(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), bytes([1, 2, 3]), ImageSize.FULL_SIZE)\n\n    async def test_configure_realtime_url(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/realtime/url',\n                                           {},\n                                           {'realtimeURL': 'https://example.com/realtime'})\n        realtime_url_request = RealtimeUrlRequest(realtimeURL='https://example.com/realtime')\n        await client.configure_realtime_url(realtime_url_request)\n\n    async def test_delete_realtime_url(self):\n        client = self.get_client_with_body(b'',\n                                           'DELETE',\n                                           'https://local-testing-base-url/inApps/v1/messaging/realtime/url',\n                                           {},\n                                           None)\n        await client.delete_realtime_url()\n\n    async def test_get_realtime_url(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/getRealtimeUrlResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/realtime/url',\n                                           {},\n                                           None)\n        response = await client.get_realtime_url()\n        self.assertIsNotNone(response)\n        self.assertEqual('https://example.com/realtime', response.realtimeURL)\n\n    async def test_upload_message_with_bullet_points(self):\n        client = self.get_client_with_body(b'',\n                                           'PUT',\n                                           'https://local-testing-base-url/inApps/v1/messaging/message/a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890',\n                                           {},\n                                           {'header': 'Header text', 'body': 'Body text',\n                                            'image': {'imageIdentifier': 'b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', 'altText': 'Alt text'},\n                                            'headerPosition': 'ABOVE_IMAGE',\n                                            'bulletPoints': [{'text': 'Bullet 1', 'imageIdentifier': 'c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012', 'altText': 'Bullet alt'}]})\n        image = UploadMessageImage(imageIdentifier=UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901'), altText='Alt text')\n        bullet_point = BulletPoint(text='Bullet 1', imageIdentifier=UUID('c3d4e5f6-a7b8-9012-c3d4-e5f6a7b89012'), altText='Bullet alt')\n        upload_message_request_body = UploadMessageRequestBody(header='Header text', body='Body text', image=image, headerPosition=HeaderPosition.ABOVE_IMAGE, bulletPoints=[bullet_point])\n        await client.upload_message(UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890'), upload_message_request_body)\n\n    async def test_initiate_performance_test(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResponse.json',\n                                           'POST',\n                                           'https://local-testing-base-url/inApps/v1/messaging/performanceTest',\n                                           {},\n                                           {'originalTransactionId': '70000500092808'})\n        performance_test_request = PerformanceTestRequest(originalTransactionId='70000500092808')\n        response = await client.initiate_performance_test(performance_test_request)\n        self.assertIsNotNone(response)\n        self.assertEqual('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d', response.requestId)\n        self.assertIsNotNone(response.config)\n        self.assertEqual(10, response.config.maxConcurrentRequests)\n        self.assertEqual(100, response.config.totalRequests)\n        self.assertEqual(60000, response.config.totalDuration)\n        self.assertEqual(500, response.config.responseTimeThreshold)\n        self.assertEqual(95, response.config.successRateThreshold)\n\n    async def test_get_performance_test_results(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/performanceTestResultResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/messaging/performanceTest/result/c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d',\n                                           {},\n                                           None)\n        response = await client.get_performance_test_results('c4b87a1d-2e3f-4a5b-9c6d-7e8f9a0b1c2d')\n        self.assertIsNotNone(response)\n        self.assertIsNotNone(response.config)\n        self.assertEqual(10, response.config.maxConcurrentRequests)\n        self.assertEqual(100, response.config.totalRequests)\n        self.assertEqual(60000, response.config.totalDuration)\n        self.assertEqual(500, response.config.responseTimeThreshold)\n        self.assertEqual(95, response.config.successRateThreshold)\n        self.assertEqual('https://example.com/retention', response.target)\n        self.assertEqual(PerformanceTestStatus.PASS, response.result)\n        self.assertEqual('PASS', response.rawResult)\n        self.assertEqual(98, response.successRate)\n        self.assertEqual(0, response.numPending)\n        self.assertIsNotNone(response.responseTimes)\n        self.assertEqual(120, response.responseTimes.average)\n        self.assertEqual(100, response.responseTimes.p50)\n        self.assertEqual(200, response.responseTimes.p90)\n        self.assertEqual(250, response.responseTimes.p95)\n        self.assertEqual(400, response.responseTimes.p99)\n        self.assertEqual({SendAttemptResult.TIMED_OUT: 1, SendAttemptResult.NO_RESPONSE: 1}, response.failures)\n        self.assertEqual({'TIMED_OUT': 1, 'NO_RESPONSE': 1}, response.rawFailures)\n\n    async def test_get_app_transaction_info_success(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/appTransactionInfoResponse.json',\n                                           'GET',\n                                           'https://local-testing-base-url/inApps/v1/transactions/appTransactions/1234',\n                                           {},\n                                           None)\n\n        app_transaction_info_response = await client.get_app_transaction_info('1234')\n\n        self.assertIsNotNone(app_transaction_info_response)\n        self.assertEqual('signed_app_transaction_info_value', app_transaction_info_response.signedAppTransactionInfo)\n\n    async def test_get_app_transaction_info_invalid_transaction_id(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/invalidTransactionIdError.json',\n                                                     'GET',\n                                                     'https://local-testing-base-url/inApps/v1/transactions/appTransactions/invalid_id',\n                                                     {},\n                                                     None,\n                                                     400)\n        try:\n            await client.get_app_transaction_info('invalid_id')\n        except APIException as e:\n            self.assertEqual(400, e.http_status_code)\n            self.assertEqual(4000006, e.raw_api_error)\n            self.assertEqual(APIError.INVALID_TRANSACTION_ID, e.api_error)\n            self.assertEqual(\"Invalid transaction id.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n    async def test_get_app_transaction_info_app_transaction_does_not_exist(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/appTransactionDoesNotExistError.json',\n                                                     'GET',\n                                                     'https://local-testing-base-url/inApps/v1/transactions/appTransactions/nonexistent_id',\n                                                     {},\n                                                     None,\n                                                     404)\n        try:\n            await client.get_app_transaction_info('nonexistent_id')\n        except APIException as e:\n            self.assertEqual(404, e.http_status_code)\n            self.assertEqual(4040019, e.raw_api_error)\n            self.assertEqual(APIError.APP_TRANSACTION_DOES_NOT_EXIST_ERROR, e.api_error)\n            self.assertEqual(\"No AppTransaction exists for the customer.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n    async def test_get_app_transaction_info_transaction_id_not_found(self):\n        client = self.get_client_with_body_from_file('tests/resources/models/transactionIdNotFoundError.json',\n                                                     'GET',\n                                                     'https://local-testing-base-url/inApps/v1/transactions/appTransactions/not_found_id',\n                                                     {},\n                                                     None,\n                                                     404)\n        try:\n            await client.get_app_transaction_info('not_found_id')\n        except APIException as e:\n            self.assertEqual(404, e.http_status_code)\n            self.assertEqual(4040010, e.raw_api_error)\n            self.assertEqual(APIError.TRANSACTION_ID_NOT_FOUND, e.api_error)\n            self.assertEqual(\"Transaction id not found.\", e.error_message)\n            return\n\n        self.assertFalse(True)\n\n    def get_signing_key(self):\n        return read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n\n    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):\n        signing_key = self.get_signing_key()\n        client = AsyncAppStoreServerAPIClient(signing_key, 'keyId', 'issuerId', 'com.example', Environment.LOCAL_TESTING)\n        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):\n            self.assertEqual(expected_method, method)\n            self.assertEqual(expected_url, url)\n            self.assertEqual(expected_params, params)\n            self.assertTrue(headers['User-Agent'].startswith('app-store-server-library/python'))\n            self.assertTrue(headers['Authorization'].startswith('Bearer '))\n            self.assertEqual('application/json', headers['Accept'])\n            decoded_jwt = decode_json_from_signed_date(headers['Authorization'][7:])\n            self.assertEqual('appstoreconnect-v1', decoded_jwt['payload']['aud'])\n            self.assertEqual('issuerId', decoded_jwt['payload']['iss'])\n            self.assertEqual('keyId', decoded_jwt['header']['kid'])\n            self.assertEqual('com.example', decoded_jwt['payload']['bid'])\n\n            # Content-specific validation\n            if expected_data is not None:\n                self.assertEqual(['User-Agent', 'Authorization', 'Accept', 'Content-Type'], list(headers.keys()))\n                self.assertEqual(expected_content_type, headers['Content-Type'])\n                self.assertIsNone(json)\n                self.assertEqual(expected_data, data)\n            else:\n                self.assertEqual(['User-Agent', 'Authorization', 'Accept'], list(headers.keys()))\n                self.assertEqual(expected_json, json)\n\n            response = Response(status_code, headers={'Content-Type': 'application/json'}, content=body)\n            return response\n\n        client._execute_request = fake_execute_and_validate_inputs\n        return client\n\n    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):\n        body = read_data_from_binary_file(path)\n        return self.get_client_with_body(body, expected_method, expected_url, expected_params, expected_json, status_code)\n"
  },
  {
    "path": "tests/test_app_data.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nimport json\nimport unittest\n\nfrom appstoreserverlibrary.models.AppData import AppData\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter\nfrom tests.util import read_data_from_file\n\n\nclass AppDataTest(unittest.TestCase):\n    def test_app_data_deserialization(self):\n        json_data = read_data_from_file('tests/resources/models/appData.json')\n\n        app_data_dict = json.loads(json_data)\n        app_data = _get_cattrs_converter(AppData).structure(app_data_dict, AppData)\n\n        self.assertEqual(987654321, app_data.appAppleId)\n        self.assertEqual(\"com.example\", app_data.bundleId)\n        self.assertEqual(Environment.SANDBOX, app_data.environment)\n        self.assertEqual(\"Sandbox\", app_data.rawEnvironment)\n        self.assertEqual(\"signed-app-transaction-info\", app_data.signedAppTransactionInfo)\n"
  },
  {
    "path": "tests/test_decoded_payloads.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom typing import Optional\nimport unittest\nfrom uuid import UUID\nfrom appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus\nfrom appstoreserverlibrary.models.ConsumptionRequestReason import ConsumptionRequestReason\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.models.ExpirationIntent import ExpirationIntent\nfrom appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType\nfrom appstoreserverlibrary.models.NotificationTypeV2 import NotificationTypeV2\nfrom appstoreserverlibrary.models.OfferDiscountType import OfferDiscountType\nfrom appstoreserverlibrary.models.OfferType import OfferType\nfrom appstoreserverlibrary.models.PriceIncreaseStatus import PriceIncreaseStatus\nfrom appstoreserverlibrary.models.PurchasePlatform import PurchasePlatform\nfrom appstoreserverlibrary.models.RevocationReason import RevocationReason\nfrom appstoreserverlibrary.models.RevocationType import RevocationType\nfrom appstoreserverlibrary.models.Status import Status\nfrom appstoreserverlibrary.models.Subtype import Subtype\nfrom appstoreserverlibrary.models.TransactionReason import TransactionReason\nfrom appstoreserverlibrary.models.Type import Type\n\nfrom tests.util import create_signed_data_from_json, get_default_signed_data_verifier, get_signed_data_verifier\n\nclass DecodedPayloads(unittest.TestCase):\n    def test_app_transaction_decoding(self):\n        signed_app_transaction = create_signed_data_from_json('tests/resources/models/appTransaction.json')\n        \n        signed_data_verifier = get_default_signed_data_verifier()\n\n        app_transaction = signed_data_verifier.verify_and_decode_app_transaction(signed_app_transaction)\n        self.assertEqual(Environment.LOCAL_TESTING, app_transaction.receiptType)\n        self.assertEqual(\"LocalTesting\", app_transaction.rawReceiptType)\n        self.assertEqual(531412, app_transaction.appAppleId)\n        self.assertEqual(\"com.example\", app_transaction.bundleId)\n        self.assertEqual(\"1.2.3\", app_transaction.applicationVersion)\n        self.assertEqual(512, app_transaction.versionExternalIdentifier)\n        self.assertEqual(1698148900000, app_transaction.receiptCreationDate)\n        self.assertEqual(1698148800000, app_transaction.originalPurchaseDate)\n        self.assertEqual(\"1.1.2\", app_transaction.originalApplicationVersion)\n        self.assertEqual(\"device_verification_value\", app_transaction.deviceVerification)\n        self.assertEqual(\"48ccfa42-7431-4f22-9908-7e88983e105a\", app_transaction.deviceVerificationNonce)\n        self.assertEqual(1698148700000, app_transaction.preorderDate)\n        self.assertEqual(\"71134\", app_transaction.appTransactionId)\n        self.assertEqual(PurchasePlatform.IOS, app_transaction.originalPlatform)\n        self.assertEqual(\"iOS\", app_transaction.rawOriginalPlatform)\n\n    def test_transaction_decoding(self):\n        signed_transaction = create_signed_data_from_json('tests/resources/models/signedTransaction.json')\n        \n        signed_data_verifier = get_default_signed_data_verifier()\n\n        transaction = signed_data_verifier.verify_and_decode_signed_transaction(signed_transaction)\n        \n        self.assertEqual(\"12345\", transaction.originalTransactionId)\n        self.assertEqual(\"23456\", transaction.transactionId)\n        self.assertEqual(\"34343\", transaction.webOrderLineItemId)\n        self.assertEqual(\"com.example\", transaction.bundleId)\n        self.assertEqual(\"com.example.product\", transaction.productId)\n        self.assertEqual(\"55555\", transaction.subscriptionGroupIdentifier)\n        self.assertEqual(1698148800000, transaction.originalPurchaseDate)\n        self.assertEqual(1698148900000, transaction.purchaseDate)\n        self.assertEqual(1698148950000, transaction.revocationDate)\n        self.assertEqual(1698149000000, transaction.expiresDate)\n        self.assertEqual(1, transaction.quantity)\n        self.assertEqual(Type.AUTO_RENEWABLE_SUBSCRIPTION, transaction.type)\n        self.assertEqual(\"Auto-Renewable Subscription\", transaction.rawType)\n        self.assertEqual(\"7e3fb20b-4cdb-47cc-936d-99d65f608138\", transaction.appAccountToken)\n        self.assertEqual(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType)\n        self.assertEqual(\"PURCHASED\", transaction.rawInAppOwnershipType)\n        self.assertEqual(1698148900000, transaction.signedDate)\n        self.assertEqual(RevocationReason.REFUNDED_DUE_TO_ISSUE, transaction.revocationReason)\n        self.assertEqual(1, transaction.rawRevocationReason)\n        self.assertEqual(\"abc.123\", transaction.offerIdentifier)\n        self.assertTrue(transaction.isUpgraded)\n        self.assertEqual(OfferType.INTRODUCTORY_OFFER, transaction.offerType)\n        self.assertEqual(1, transaction.rawOfferType)\n        self.assertEqual(\"USA\", transaction.storefront)\n        self.assertEqual(\"143441\", transaction.storefrontId)\n        self.assertEqual(TransactionReason.PURCHASE, transaction.transactionReason)\n        self.assertEqual(\"PURCHASE\", transaction.rawTransactionReason)\n        self.assertEqual(Environment.LOCAL_TESTING, transaction.environment)\n        self.assertEqual(\"LocalTesting\", transaction.rawEnvironment)\n        self.assertEqual(10990, transaction.price)\n        self.assertEqual(\"USD\", transaction.currency)\n        self.assertEqual(OfferDiscountType.PAY_AS_YOU_GO, transaction.offerDiscountType)\n        self.assertEqual(\"PAY_AS_YOU_GO\", transaction.rawOfferDiscountType)\n        self.assertEqual(\"71134\", transaction.appTransactionId)\n        self.assertEqual(\"P1Y\", transaction.offerPeriod)\n\n    def test_transaction_with_revocation_decoding(self):\n        signed_transaction = create_signed_data_from_json('tests/resources/models/signedTransactionWithRevocation.json')\n\n        signed_data_verifier = get_default_signed_data_verifier()\n\n        transaction = signed_data_verifier.verify_and_decode_signed_transaction(signed_transaction)\n\n        self.assertEqual(\"12345\", transaction.originalTransactionId)\n        self.assertEqual(\"23456\", transaction.transactionId)\n        self.assertEqual(\"34343\", transaction.webOrderLineItemId)\n        self.assertEqual(\"com.example\", transaction.bundleId)\n        self.assertEqual(\"com.example.product\", transaction.productId)\n        self.assertEqual(\"55555\", transaction.subscriptionGroupIdentifier)\n        self.assertEqual(1698148800000, transaction.originalPurchaseDate)\n        self.assertEqual(1698148900000, transaction.purchaseDate)\n        self.assertEqual(1698148950000, transaction.revocationDate)\n        self.assertEqual(1698149000000, transaction.expiresDate)\n        self.assertEqual(1, transaction.quantity)\n        self.assertEqual(Type.AUTO_RENEWABLE_SUBSCRIPTION, transaction.type)\n        self.assertEqual(\"Auto-Renewable Subscription\", transaction.rawType)\n        self.assertEqual(\"7e3fb20b-4cdb-47cc-936d-99d65f608138\", transaction.appAccountToken)\n        self.assertEqual(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType)\n        self.assertEqual(\"PURCHASED\", transaction.rawInAppOwnershipType)\n        self.assertEqual(1698148900000, transaction.signedDate)\n        self.assertEqual(RevocationReason.REFUNDED_DUE_TO_ISSUE, transaction.revocationReason)\n        self.assertEqual(1, transaction.rawRevocationReason)\n        self.assertEqual(\"abc.123\", transaction.offerIdentifier)\n        self.assertTrue(transaction.isUpgraded)\n        self.assertEqual(OfferType.INTRODUCTORY_OFFER, transaction.offerType)\n        self.assertEqual(1, transaction.rawOfferType)\n        self.assertEqual(\"USA\", transaction.storefront)\n        self.assertEqual(\"143441\", transaction.storefrontId)\n        self.assertEqual(TransactionReason.PURCHASE, transaction.transactionReason)\n        self.assertEqual(\"PURCHASE\", transaction.rawTransactionReason)\n        self.assertEqual(Environment.LOCAL_TESTING, transaction.environment)\n        self.assertEqual(\"LocalTesting\", transaction.rawEnvironment)\n        self.assertEqual(10990, transaction.price)\n        self.assertEqual(\"USD\", transaction.currency)\n        self.assertEqual(OfferDiscountType.PAY_AS_YOU_GO, transaction.offerDiscountType)\n        self.assertEqual(\"PAY_AS_YOU_GO\", transaction.rawOfferDiscountType)\n        self.assertEqual(\"71134\", transaction.appTransactionId)\n        self.assertEqual(\"P1Y\", transaction.offerPeriod)\n        self.assertEqual(RevocationType.REFUND_PRORATED, transaction.revocationType)\n        self.assertEqual(\"REFUND_PRORATED\", transaction.rawRevocationType)\n        self.assertEqual(50000, transaction.revocationPercentage)\n\n\n    \n    def test_renewal_info_decoding(self):\n        signed_renewal_info = create_signed_data_from_json('tests/resources/models/signedRenewalInfo.json')\n        \n        signed_data_verifier = get_default_signed_data_verifier()\n\n        renewal_info = signed_data_verifier.verify_and_decode_renewal_info(signed_renewal_info)\n\n        self.assertEqual(ExpirationIntent.CUSTOMER_CANCELLED, renewal_info.expirationIntent)\n        self.assertEqual(1, renewal_info.rawExpirationIntent)\n        self.assertEqual(\"12345\", renewal_info.originalTransactionId)\n        self.assertEqual(\"com.example.product.2\", renewal_info.autoRenewProductId)\n        self.assertEqual(\"com.example.product\", renewal_info.productId)\n        self.assertEqual(AutoRenewStatus.ON, renewal_info.autoRenewStatus)\n        self.assertEqual(1, renewal_info.rawAutoRenewStatus)\n        self.assertTrue(renewal_info.isInBillingRetryPeriod)\n        self.assertEqual(PriceIncreaseStatus.CUSTOMER_HAS_NOT_RESPONDED, renewal_info.priceIncreaseStatus)\n        self.assertEqual(0, renewal_info.rawPriceIncreaseStatus)\n        self.assertEqual(1698148900000, renewal_info.gracePeriodExpiresDate)\n        self.assertEqual(OfferType.PROMOTIONAL_OFFER, renewal_info.offerType)\n        self.assertEqual(2, renewal_info.rawOfferType)\n        self.assertEqual(\"abc.123\", renewal_info.offerIdentifier)\n        self.assertEqual(1698148800000, renewal_info.signedDate)\n        self.assertEqual(Environment.LOCAL_TESTING, renewal_info.environment)\n        self.assertEqual(\"LocalTesting\", renewal_info.rawEnvironment)\n        self.assertEqual(1698148800000, renewal_info.recentSubscriptionStartDate)\n        self.assertEqual(1698148850000, renewal_info.renewalDate)\n        self.assertEqual(9990, renewal_info.renewalPrice)\n        self.assertEqual(\"USD\", renewal_info.currency)\n        self.assertEqual(OfferDiscountType.PAY_AS_YOU_GO, renewal_info.offerDiscountType)\n        self.assertEqual(\"PAY_AS_YOU_GO\", renewal_info.rawOfferDiscountType)\n        self.assertEqual(['eligible1', 'eligible2'], renewal_info.eligibleWinBackOfferIds)\n        self.assertEqual(\"71134\", renewal_info.appTransactionId)\n        self.assertEqual(\"P1Y\", renewal_info.offerPeriod)\n        self.assertEqual(\"7e3fb20b-4cdb-47cc-936d-99d65f608138\", renewal_info.appAccountToken)\n\n    def test_notification_decoding(self):\n        signed_notification = create_signed_data_from_json('tests/resources/models/signedNotification.json')\n        \n        signed_data_verifier = get_default_signed_data_verifier()\n\n        notification = signed_data_verifier.verify_and_decode_notification(signed_notification)\n\n        self.assertEqual(NotificationTypeV2.SUBSCRIBED, notification.notificationType)\n        self.assertEqual(\"SUBSCRIBED\", notification.rawNotificationType)\n        self.assertEqual(Subtype.INITIAL_BUY, notification.subtype)\n        self.assertEqual(\"INITIAL_BUY\", notification.rawSubtype)\n        self.assertEqual(\"002e14d5-51f5-4503-b5a8-c3a1af68eb20\", notification.notificationUUID)\n        self.assertEqual(\"2.0\", notification.version)\n        self.assertEqual(1698148900000, notification.signedDate)\n        self.assertIsNotNone(notification.data)\n        self.assertIsNone(notification.summary)\n        self.assertIsNone(notification.externalPurchaseToken)\n        self.assertEqual(Environment.LOCAL_TESTING, notification.data.environment)\n        self.assertEqual(\"LocalTesting\", notification.data.rawEnvironment)\n        self.assertEqual(41234, notification.data.appAppleId)\n        self.assertEqual(\"com.example\", notification.data.bundleId)\n        self.assertEqual(\"1.2.3\", notification.data.bundleVersion)\n        self.assertEqual(\"signed_transaction_info_value\", notification.data.signedTransactionInfo)\n        self.assertEqual(\"signed_renewal_info_value\", notification.data.signedRenewalInfo);\n        self.assertEqual(Status.ACTIVE, notification.data.status)\n        self.assertEqual(1, notification.data.rawStatus)\n        self.assertIsNone(notification.data.consumptionRequestReason)\n        self.assertIsNone(notification.data.rawConsumptionRequestReason)\n\n    def test_consumption_request_notification_decoding(self):\n        signed_notification = create_signed_data_from_json('tests/resources/models/signedConsumptionRequestNotification.json')\n        \n        signed_data_verifier = get_default_signed_data_verifier()\n\n        notification = signed_data_verifier.verify_and_decode_notification(signed_notification)\n\n        self.assertEqual(NotificationTypeV2.CONSUMPTION_REQUEST, notification.notificationType)\n        self.assertEqual(\"CONSUMPTION_REQUEST\", notification.rawNotificationType)\n        self.assertIsNone(notification.subtype)\n        self.assertIsNone(notification.rawSubtype)\n        self.assertEqual(\"002e14d5-51f5-4503-b5a8-c3a1af68eb20\", notification.notificationUUID)\n        self.assertEqual(\"2.0\", notification.version)\n        self.assertEqual(1698148900000, notification.signedDate)\n        self.assertIsNotNone(notification.data)\n        self.assertIsNone(notification.summary)\n        self.assertIsNone(notification.externalPurchaseToken)\n        self.assertEqual(Environment.LOCAL_TESTING, notification.data.environment)\n        self.assertEqual(\"LocalTesting\", notification.data.rawEnvironment)\n        self.assertEqual(41234, notification.data.appAppleId)\n        self.assertEqual(\"com.example\", notification.data.bundleId)\n        self.assertEqual(\"1.2.3\", notification.data.bundleVersion)\n        self.assertEqual(\"signed_transaction_info_value\", notification.data.signedTransactionInfo)\n        self.assertEqual(\"signed_renewal_info_value\", notification.data.signedRenewalInfo);\n        self.assertEqual(Status.ACTIVE, notification.data.status)\n        self.assertEqual(1, notification.data.rawStatus)\n        self.assertEqual(ConsumptionRequestReason.UNINTENDED_PURCHASE, notification.data.consumptionRequestReason)\n        self.assertEqual(\"UNINTENDED_PURCHASE\", notification.data.rawConsumptionRequestReason)\n        \n    def test_summary_notification_decoding(self):\n        signed_summary_notification = create_signed_data_from_json('tests/resources/models/signedSummaryNotification.json')\n        \n        signed_data_verifier = get_default_signed_data_verifier()\n\n        notification = signed_data_verifier.verify_and_decode_notification(signed_summary_notification)\n        \n        self.assertEqual(NotificationTypeV2.RENEWAL_EXTENSION, notification.notificationType)\n        self.assertEqual(\"RENEWAL_EXTENSION\", notification.rawNotificationType)\n        self.assertEqual(Subtype.SUMMARY, notification.subtype)\n        self.assertEqual(\"SUMMARY\", notification.rawSubtype)\n        self.assertEqual(\"002e14d5-51f5-4503-b5a8-c3a1af68eb20\", notification.notificationUUID)\n        self.assertEqual(\"2.0\", notification.version)\n        self.assertEqual(1698148900000, notification.signedDate)\n        self.assertIsNone(notification.data)\n        self.assertIsNotNone(notification.summary)\n        self.assertIsNone(notification.externalPurchaseToken)\n        self.assertEqual(Environment.LOCAL_TESTING, notification.summary.environment)\n        self.assertEqual(\"LocalTesting\", notification.summary.rawEnvironment)\n        self.assertEqual(41234, notification.summary.appAppleId)\n        self.assertEqual(\"com.example\", notification.summary.bundleId)\n        self.assertEqual(\"com.example.product\", notification.summary.productId)\n        self.assertEqual(\"efb27071-45a4-4aca-9854-2a1e9146f265\", notification.summary.requestIdentifier)\n        self.assertEqual([\"CAN\", \"USA\", \"MEX\"], notification.summary.storefrontCountryCodes)\n        self.assertEqual(5, notification.summary.succeededCount)\n        self.assertEqual(2, notification.summary.failedCount)\n    \n    def test_external_purchase_token_notification_decoding(self):\n        signed_external_purchase_token_notification = create_signed_data_from_json('tests/resources/models/signedExternalPurchaseTokenNotification.json')\n        \n        signed_data_verifier = get_default_signed_data_verifier()\n\n        def check_environment_and_bundle_id(bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]):\n            self.assertEqual(\"com.example\", bundle_id)\n            self.assertEqual(55555, app_apple_id)\n            self.assertEqual(Environment.PRODUCTION, environment)\n\n        signed_data_verifier._verify_notification = check_environment_and_bundle_id\n\n        notification = signed_data_verifier.verify_and_decode_notification(signed_external_purchase_token_notification)\n        \n        self.assertEqual(NotificationTypeV2.EXTERNAL_PURCHASE_TOKEN, notification.notificationType)\n        self.assertEqual(\"EXTERNAL_PURCHASE_TOKEN\", notification.rawNotificationType)\n        self.assertEqual(Subtype.UNREPORTED, notification.subtype)\n        self.assertEqual(\"UNREPORTED\", notification.rawSubtype)\n        self.assertEqual(\"002e14d5-51f5-4503-b5a8-c3a1af68eb20\", notification.notificationUUID)\n        self.assertEqual(\"2.0\", notification.version)\n        self.assertEqual(1698148900000, notification.signedDate)\n        self.assertIsNone(notification.data)\n        self.assertIsNone(notification.summary)\n        self.assertIsNotNone(notification.externalPurchaseToken)\n        self.assertEqual(\"b2158121-7af9-49d4-9561-1f588205523e\", notification.externalPurchaseToken.externalPurchaseId)\n        self.assertEqual(1698148950000, notification.externalPurchaseToken.tokenCreationDate)\n        self.assertEqual(55555, notification.externalPurchaseToken.appAppleId)\n        self.assertEqual(\"com.example\", notification.externalPurchaseToken.bundleId)\n    \n    def test_external_purchase_token_sandbox_notification_decoding(self):\n        signed_external_purchase_token_notification = create_signed_data_from_json('tests/resources/models/signedExternalPurchaseTokenSandboxNotification.json')\n        \n        signed_data_verifier = get_default_signed_data_verifier()\n\n        def check_environment_and_bundle_id(bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]):\n            self.assertEqual(\"com.example\", bundle_id)\n            self.assertEqual(55555, app_apple_id)\n            self.assertEqual(Environment.SANDBOX, environment)\n\n        signed_data_verifier._verify_notification = check_environment_and_bundle_id\n\n        notification = signed_data_verifier.verify_and_decode_notification(signed_external_purchase_token_notification)\n        \n        self.assertEqual(NotificationTypeV2.EXTERNAL_PURCHASE_TOKEN, notification.notificationType)\n        self.assertEqual(\"EXTERNAL_PURCHASE_TOKEN\", notification.rawNotificationType)\n        self.assertEqual(Subtype.UNREPORTED, notification.subtype)\n        self.assertEqual(\"UNREPORTED\", notification.rawSubtype)\n        self.assertEqual(\"002e14d5-51f5-4503-b5a8-c3a1af68eb20\", notification.notificationUUID)\n        self.assertEqual(\"2.0\", notification.version)\n        self.assertEqual(1698148900000, notification.signedDate)\n        self.assertIsNone(notification.data)\n        self.assertIsNone(notification.summary)\n        self.assertIsNotNone(notification.externalPurchaseToken)\n        self.assertEqual(\"SANDBOX_b2158121-7af9-49d4-9561-1f588205523e\", notification.externalPurchaseToken.externalPurchaseId)\n        self.assertEqual(1698148950000, notification.externalPurchaseToken.tokenCreationDate)\n        self.assertEqual(55555, notification.externalPurchaseToken.appAppleId)\n        self.assertEqual(\"com.example\", notification.externalPurchaseToken.bundleId)\n    \n    def test_realtime_request_decoding(self):\n        signed_realtime_request = create_signed_data_from_json('tests/resources/models/decodedRealtimeRequest.json')\n\n        signed_data_verifier = get_default_signed_data_verifier()\n        request = signed_data_verifier.verify_and_decode_realtime_request(signed_realtime_request)\n\n        self.assertEqual('99371282', request.originalTransactionId)\n        self.assertEqual(531412, request.appAppleId)\n        self.assertEqual('com.example.product', request.productId)\n        self.assertEqual('en-US', request.userLocale)\n        self.assertEqual(UUID('3db5c98d-8acf-4e29-831e-8e1f82f9f6e9'), request.requestIdentifier)\n        self.assertEqual(Environment.LOCAL_TESTING, request.environment)\n        self.assertEqual('LocalTesting', request.rawEnvironment)\n        self.assertEqual(1698148900000, request.signedDate)\n\n    def test_rescind_consent_notification_decoding(self):\n        signed_notification = create_signed_data_from_json('tests/resources/models/signedRescindConsentNotification.json')\n\n        signed_data_verifier = get_default_signed_data_verifier()\n\n        notification = signed_data_verifier.verify_and_decode_notification(signed_notification)\n\n        self.assertEqual(NotificationTypeV2.RESCIND_CONSENT, notification.notificationType)\n        self.assertEqual(\"RESCIND_CONSENT\", notification.rawNotificationType)\n        self.assertIsNone(notification.subtype)\n        self.assertIsNone(notification.rawSubtype)\n        self.assertEqual(\"002e14d5-51f5-4503-b5a8-c3a1af68eb20\", notification.notificationUUID)\n        self.assertEqual(\"2.0\", notification.version)\n        self.assertEqual(1698148900000, notification.signedDate)\n        self.assertIsNone(notification.data)\n        self.assertIsNone(notification.summary)\n        self.assertIsNone(notification.externalPurchaseToken)\n        self.assertIsNotNone(notification.appData)\n        self.assertEqual(Environment.LOCAL_TESTING, notification.appData.environment)\n        self.assertEqual(\"LocalTesting\", notification.appData.rawEnvironment)\n        self.assertEqual(41234, notification.appData.appAppleId)\n        self.assertEqual(\"com.example\", notification.appData.bundleId)\n        self.assertEqual(\"signed_app_transaction_info_value\", notification.appData.signedAppTransactionInfo)\n"
  },
  {
    "path": "tests/test_jws_signature_creator.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nfrom attr import define\nimport attr\nimport base64\nimport json\nimport jwt\nimport unittest\nfrom appstoreserverlibrary.jws_signature_creator import AdvancedCommerceAPIInAppRequest, AdvancedCommerceAPIInAppSignatureCreator, IntroductoryOfferEligibilitySignatureCreator, PromotionalOfferV2SignatureCreator\nfrom tests.util import read_data_from_binary_file\n\n@define\nclass TestInAppRequest(AdvancedCommerceAPIInAppRequest):\n    test_value: str\n\nclass JWSSignatureCreatorTest(unittest.TestCase):\n    def test_promotional_offer_signature_creator(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        signature = signature_creator.create_signature(\"productId\", \"offerIdentifier\", \"transactionId\")\n        self.assertIsNotNone(signature)\n        headers = jwt.get_unverified_header(signature)\n        payload = jwt.decode(signature, options={\"verify_signature\": False})\n\n        # Header\n        self.assertEqual(\"JWT\", headers[\"typ\"])\n        self.assertEqual(\"ES256\", headers[\"alg\"])\n        self.assertEqual(\"keyId\", headers[\"kid\"])\n        # Payload\n        self.assertEqual(\"issuerId\", payload['iss'])\n        self.assertIsNotNone(payload['iat'])\n        self.assertFalse('exp' in payload)\n        self.assertEqual(\"promotional-offer\", payload['aud'])\n        self.assertEqual('bundleId', payload['bid'])\n        self.assertIsNotNone(payload['nonce'])\n        self.assertEqual('productId', payload['productId'])\n        self.assertEqual('offerIdentifier', payload['offerIdentifier'])\n        self.assertEqual('transactionId', payload['transactionId'])\n\n    def test_promotional_offer_signature_creator_transaction_id_missing(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        signature = signature_creator.create_signature(\"productId\", \"offerIdentifier\", None)\n        payload = jwt.decode(signature, options={\"verify_signature\": False})\n        self.assertFalse('transactionId' in payload)\n\n    def test_promotional_offer_signature_creator_offer_identifier_missing(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        with self.assertRaises(ValueError):\n            signature_creator.create_signature(\"productId\", None, \"transactionId\")\n\n    def test_promotional_offer_signature_creator_product_id_missing(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = PromotionalOfferV2SignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        with self.assertRaises(ValueError):\n            signature_creator.create_signature(None, \"offerIdentifier\", \"transactionId\")\n    \n    def test_introductory_offer_eligibility_signature_creator(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        signature = signature_creator.create_signature(\"productId\", True, \"transactionId\")\n        self.assertIsNotNone(signature)\n        headers = jwt.get_unverified_header(signature)\n        payload = jwt.decode(signature, options={\"verify_signature\": False})\n\n        # Header\n        self.assertEqual(\"JWT\", headers[\"typ\"])\n        self.assertEqual(\"ES256\", headers[\"alg\"])\n        self.assertEqual(\"keyId\", headers[\"kid\"])\n        # Payload\n        self.assertEqual(\"issuerId\", payload['iss'])\n        self.assertIsNotNone(payload['iat'])\n        self.assertFalse('exp' in payload)\n        self.assertEqual(\"introductory-offer-eligibility\", payload['aud'])\n        self.assertEqual('bundleId', payload['bid'])\n        self.assertIsNotNone(payload['nonce'])\n        self.assertEqual('productId', payload['productId'])\n        self.assertEqual(True, payload['allowIntroductoryOffer'])\n        self.assertEqual('transactionId', payload['transactionId'])\n\n    def test_introductory_offer_eligibility_signature_creator_transaction_id_missing(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        with self.assertRaises(ValueError):\n            signature_creator.create_signature(\"productId\", True, None)\n    \n    def test_introductory_offer_eligibility_signature_creator_allow_introductory_offer_missing(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        with self.assertRaises(ValueError):\n            signature_creator.create_signature(\"productId\", None, \"transactionId\")\n\n    def test_introductory_offer_eligibility_signature_creator_product_id_missing(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = IntroductoryOfferEligibilitySignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        with self.assertRaises(ValueError):\n            signature_creator.create_signature(None, True, \"transactionId\")\n\n    def test_advanced_commerce_api_in_app_signature_creator(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = AdvancedCommerceAPIInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        request = TestInAppRequest(\"testValue\")\n        signature = signature_creator.create_signature(request)\n        self.assertIsNotNone(signature)\n        headers = jwt.get_unverified_header(signature)\n        payload = jwt.decode(signature, options={\"verify_signature\": False})\n\n        # Header\n        self.assertEqual(\"JWT\", headers[\"typ\"])\n        self.assertEqual(\"ES256\", headers[\"alg\"])\n        self.assertEqual(\"keyId\", headers[\"kid\"])\n        # Payload\n        self.assertEqual(\"issuerId\", payload['iss'])\n        self.assertIsNotNone(payload['iat'])\n        self.assertFalse('exp' in payload)\n        self.assertEqual(\"advanced-commerce-api\", payload['aud'])\n        self.assertEqual('bundleId', payload['bid'])\n        self.assertIsNotNone(payload['nonce'])\n        request = payload['request']\n        decode_json = json.loads(str(base64.b64decode(request).decode('utf-8')))\n        self.assertEqual('testValue', decode_json['test_value'])\n\n    def test_advanced_commerce_api_in_app_signature_creator_request_missing(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = AdvancedCommerceAPIInAppSignatureCreator(signing_key, 'keyId', 'issuerId', 'bundleId')\n        with self.assertRaises(ValueError):\n            signature_creator.create_signature(None)"
  },
  {
    "path": "tests/test_payload_verification.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nimport unittest\nfrom base64 import b64decode\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.models.NotificationHistoryRequest import NotificationTypeV2\n\nfrom appstoreserverlibrary.signed_data_verifier import VerificationException, VerificationStatus, SignedDataVerifier\n\nfrom tests.util import get_signed_data_verifier, read_data_from_file\n\nclass PayloadVerification(unittest.TestCase):\n    def test_app_store_server_notification_decoding(self):\n        verifier = get_signed_data_verifier(Environment.SANDBOX, \"com.example\")\n        test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification')\n        notification = verifier.verify_and_decode_notification(test_notification)\n        self.assertEqual(notification.notificationType, NotificationTypeV2.TEST)\n\n    def test_app_store_server_notification_decoding_production(self):\n        verifier = get_signed_data_verifier(Environment.PRODUCTION, \"com.example\")\n        test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification')\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_and_decode_notification(test_notification)\n        self.assertEqual(context.exception.status, VerificationStatus.INVALID_ENVIRONMENT)\n\n    def test_missing_x5c_header(self):\n        verifier = get_signed_data_verifier(Environment.SANDBOX, \"com.example\")\n        missing_x5c_header_claim = read_data_from_file('tests/resources/mock_signed_data/missingX5CHeaderClaim')\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_and_decode_notification(missing_x5c_header_claim)\n        self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE)\n\n    def test_wrong_bundle_id_for_server_notification(self):\n        verifier = get_signed_data_verifier(Environment.SANDBOX, \"com.examplex\")\n        wrong_bundle = read_data_from_file('tests/resources/mock_signed_data/wrongBundleId')\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_and_decode_notification(wrong_bundle)\n        self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER)\n\n    def test_wrong_app_apple_id_for_server_notification(self):\n        verifier = get_signed_data_verifier(Environment.PRODUCTION, \"com.example\", 1235)\n        test_notification = read_data_from_file('tests/resources/mock_signed_data/testNotification')\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_and_decode_notification(test_notification)\n        self.assertEqual(context.exception.status, VerificationStatus.INVALID_APP_IDENTIFIER)\n\n    def test_renewal_info_decoding(self):\n        verifier = get_signed_data_verifier(Environment.SANDBOX, \"com.example\")\n        renewal_info = read_data_from_file('tests/resources/mock_signed_data/renewalInfo')\n        notification = verifier.verify_and_decode_renewal_info(renewal_info)\n        self.assertEqual(notification.environment, Environment.SANDBOX)\n\n    def test_transaction_info_decoding(self):\n        verifier = get_signed_data_verifier(Environment.SANDBOX, \"com.example\")\n        transaction_info = read_data_from_file('tests/resources/mock_signed_data/transactionInfo')\n        notification = verifier.verify_and_decode_signed_transaction(transaction_info)\n        self.assertEqual(notification.environment, Environment.SANDBOX)\n\n    def test_malformed_jwt_with_too_many_parts(self):\n        verifier = get_signed_data_verifier(Environment.SANDBOX, \"com.example\")\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_and_decode_notification(\"a.b.c.d\")\n        self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE)\n\n    def test_malformed_jwt_with_malformed_data(self):\n        verifier = get_signed_data_verifier(Environment.SANDBOX, \"com.example\")\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_and_decode_notification(\"a.b.c\")\n        self.assertEqual(context.exception.status, VerificationStatus.VERIFICATION_FAILURE)\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_promotional_offer_signature_creator.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nimport unittest\nfrom appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator\n\nfrom tests.util import read_data_from_binary_file\nfrom uuid import UUID\n\n\nclass PromotionalOfferSignatureCreatorTest(unittest.TestCase):\n    def test_signature_creator(self):\n        signing_key = read_data_from_binary_file('tests/resources/certs/testSigningKey.p8')\n        signature_creator = PromotionalOfferSignatureCreator(signing_key, 'keyId', 'bundleId')\n        signature = signature_creator.create_signature(\"productId\", \"offerId\", \"appAccountToken\", UUID(\"20fba8a0-2b80-4a7d-a17f-85c1854727f8\"), 1698148900000)\n        self.assertIsNotNone(signature)"
  },
  {
    "path": "tests/test_receipt_utility.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nimport unittest\nfrom appstoreserverlibrary.receipt_utility import ReceiptUtility\n\nfrom tests.util import read_data_from_file\n\nAPP_RECEIPT_EXPECTED_TRANSACTION_ID = \"0\"\nTRANSACTION_RECEIPT_EXPECTED_TRANSACTION_ID = \"33993399\"\n\nclass ReceiptUtilityTest(unittest.TestCase):\n    def test_xcode_app_receipt_extraction_with_no_transactions(self):\n        receipt = read_data_from_file(\"tests/resources/xcode/xcode-app-receipt-empty\")\n\n        receipt_util = ReceiptUtility()\n\n        extracted_transaction_id = receipt_util.extract_transaction_id_from_app_receipt(receipt)\n\n        self.assertIsNone(extracted_transaction_id)\n\n    def test_xcode_app_receipt_extraction_with_transactions(self):\n        receipt = read_data_from_file(\"tests/resources/xcode/xcode-app-receipt-with-transaction\")\n\n        receipt_util = ReceiptUtility()\n\n        extracted_transaction_id = receipt_util.extract_transaction_id_from_app_receipt(receipt)\n\n        self.assertEqual(APP_RECEIPT_EXPECTED_TRANSACTION_ID, extracted_transaction_id)\n\n    def test_transaction_receipt_extraction(self):\n        receipt = read_data_from_file(\"tests/resources/mock_signed_data/legacyTransaction\")\n\n        receipt_util = ReceiptUtility()\n\n        extracted_transaction_id = receipt_util.extract_transaction_id_from_transaction_receipt(receipt)\n\n        self.assertEqual(TRANSACTION_RECEIPT_EXPECTED_TRANSACTION_ID, extracted_transaction_id)"
  },
  {
    "path": "tests/test_retention_messaging.py",
    "content": "# Copyright (c) 2025 Apple Inc. Licensed under MIT License.\n\nimport unittest\nfrom uuid import UUID\n\nfrom appstoreserverlibrary.models.AlternateProduct import AlternateProduct\nfrom appstoreserverlibrary.models.DecodedRealtimeRequestBody import DecodedRealtimeRequestBody\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.models.Message import Message\nfrom appstoreserverlibrary.models.PromotionalOffer import PromotionalOffer\nfrom appstoreserverlibrary.models.PromotionalOfferSignatureV1 import PromotionalOfferSignatureV1\nfrom appstoreserverlibrary.models.RealtimeResponseBody import RealtimeResponseBody\nfrom appstoreserverlibrary.models.LibraryUtility import _get_cattrs_converter\n\n\nclass RetentionMessaging(unittest.TestCase):\n    def test_realtime_response_body_with_message(self):\n        # Create a RealtimeResponseBody with a Message\n        message_id = UUID('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890')\n        message = Message(messageIdentifier=message_id)\n        response_body = RealtimeResponseBody(message=message)\n\n        # Serialize to dict\n        c = _get_cattrs_converter(RealtimeResponseBody)\n        json_dict = c.unstructure(response_body)\n\n        # Validate structure\n        self.assertIn('message', json_dict)\n        self.assertIn('messageIdentifier', json_dict['message'])\n        self.assertEqual('a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890', json_dict['message']['messageIdentifier'])\n        self.assertNotIn('alternateProduct', json_dict)\n        self.assertNotIn('promotionalOffer', json_dict)\n\n        # Deserialize back\n        deserialized = c.structure(json_dict, RealtimeResponseBody)\n\n        # Verify\n        self.assertIsNotNone(deserialized.message)\n        self.assertEqual(message_id, deserialized.message.messageIdentifier)\n        self.assertIsNone(deserialized.alternateProduct)\n        self.assertIsNone(deserialized.promotionalOffer)\n\n    def test_realtime_response_body_with_alternate_product(self):\n        # Create a RealtimeResponseBody with an AlternateProduct\n        message_id = UUID('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901')\n        product_id = 'com.example.alternate.product'\n        alternate_product = AlternateProduct(messageIdentifier=message_id, productId=product_id)\n        response_body = RealtimeResponseBody(alternateProduct=alternate_product)\n\n        # Serialize to dict\n        c = _get_cattrs_converter(RealtimeResponseBody)\n        json_dict = c.unstructure(response_body)\n\n        # Validate structure\n        self.assertIn('alternateProduct', json_dict)\n        self.assertIn('messageIdentifier', json_dict['alternateProduct'])\n        self.assertIn('productId', json_dict['alternateProduct'])\n        self.assertEqual('b2c3d4e5-f6a7-8901-b2c3-d4e5f6a78901', json_dict['alternateProduct']['messageIdentifier'])\n        self.assertEqual('com.example.alternate.product', json_dict['alternateProduct']['productId'])\n        self.assertNotIn('message', json_dict)\n        self.assertNotIn('promotionalOffer', json_dict)\n\n        # Deserialize back\n        deserialized = c.structure(json_dict, RealtimeResponseBody)\n\n        # Verify\n        self.assertIsNone(deserialized.message)\n        self.assertIsNotNone(deserialized.alternateProduct)\n        self.assertEqual(message_id, deserialized.alternateProduct.messageIdentifier)\n        self.assertEqual(product_id, deserialized.alternateProduct.productId)\n        self.assertIsNone(deserialized.promotionalOffer)\n\n    def test_realtime_response_body_with_promotional_offer_v2(self):\n        # Create a RealtimeResponseBody with a PromotionalOffer (V2 signature)\n        message_id = UUID('c3d4e5f6-a789-0123-c3d4-e5f6a7890123')\n        signature_v2 = 'signature2'\n        promotional_offer = PromotionalOffer(messageIdentifier=message_id, promotionalOfferSignatureV2=signature_v2)\n        response_body = RealtimeResponseBody(promotionalOffer=promotional_offer)\n\n        # Serialize to dict\n        c = _get_cattrs_converter(RealtimeResponseBody)\n        json_dict = c.unstructure(response_body)\n\n        # Validate structure\n        self.assertIn('promotionalOffer', json_dict)\n        self.assertIn('messageIdentifier', json_dict['promotionalOffer'])\n        self.assertIn('promotionalOfferSignatureV2', json_dict['promotionalOffer'])\n        self.assertEqual('c3d4e5f6-a789-0123-c3d4-e5f6a7890123', json_dict['promotionalOffer']['messageIdentifier'])\n        self.assertEqual('signature2', json_dict['promotionalOffer']['promotionalOfferSignatureV2'])\n        self.assertNotIn('promotionalOfferSignatureV1', json_dict['promotionalOffer'])\n        self.assertNotIn('message', json_dict)\n        self.assertNotIn('alternateProduct', json_dict)\n\n        # Deserialize back\n        deserialized = c.structure(json_dict, RealtimeResponseBody)\n\n        # Verify\n        self.assertIsNone(deserialized.message)\n        self.assertIsNone(deserialized.alternateProduct)\n        self.assertIsNotNone(deserialized.promotionalOffer)\n        self.assertEqual(message_id, deserialized.promotionalOffer.messageIdentifier)\n        self.assertEqual(signature_v2, deserialized.promotionalOffer.promotionalOfferSignatureV2)\n        self.assertIsNone(deserialized.promotionalOffer.promotionalOfferSignatureV1)\n\n    def test_realtime_response_body_with_promotional_offer_v1(self):\n        # Create a RealtimeResponseBody with a PromotionalOffer (V1 signature)\n        message_id = UUID('d4e5f6a7-8901-2345-d4e5-f6a789012345')\n        nonce = UUID('e5f6a789-0123-4567-e5f6-a78901234567')\n        app_account_token = UUID('f6a78901-2345-6789-f6a7-890123456789')\n        signature_v1 = PromotionalOfferSignatureV1(\n            encodedSignature='base64encodedSignature',\n            productId='com.example.product',\n            nonce=nonce,\n            timestamp=1698148900000,\n            keyId='keyId123',\n            offerIdentifier='offer123',\n            appAccountToken=app_account_token\n        )\n\n        promotional_offer = PromotionalOffer(messageIdentifier=message_id, promotionalOfferSignatureV1=signature_v1)\n        response_body = RealtimeResponseBody(promotionalOffer=promotional_offer)\n\n        # Serialize to dict\n        c = _get_cattrs_converter(RealtimeResponseBody)\n        json_dict = c.unstructure(response_body)\n\n        # Validate structure\n        self.assertIn('promotionalOffer', json_dict)\n        self.assertIn('messageIdentifier', json_dict['promotionalOffer'])\n        self.assertIn('promotionalOfferSignatureV1', json_dict['promotionalOffer'])\n        self.assertEqual('d4e5f6a7-8901-2345-d4e5-f6a789012345', json_dict['promotionalOffer']['messageIdentifier'])\n\n        v1_node = json_dict['promotionalOffer']['promotionalOfferSignatureV1']\n        self.assertIn('encodedSignature', v1_node)\n        self.assertIn('productId', v1_node)\n        self.assertIn('nonce', v1_node)\n        self.assertIn('timestamp', v1_node)\n        self.assertIn('keyId', v1_node)\n        self.assertIn('offerIdentifier', v1_node)\n        self.assertIn('appAccountToken', v1_node)\n        self.assertEqual('base64encodedSignature', v1_node['encodedSignature'])\n        self.assertEqual('com.example.product', v1_node['productId'])\n        self.assertEqual('e5f6a789-0123-4567-e5f6-a78901234567', v1_node['nonce'])\n        self.assertEqual(1698148900000, v1_node['timestamp'])\n        self.assertEqual('keyId123', v1_node['keyId'])\n        self.assertEqual('offer123', v1_node['offerIdentifier'])\n        self.assertEqual('f6a78901-2345-6789-f6a7-890123456789', v1_node['appAccountToken'])\n\n        self.assertNotIn('promotionalOfferSignatureV2', json_dict['promotionalOffer'])\n        self.assertNotIn('message', json_dict)\n        self.assertNotIn('alternateProduct', json_dict)\n\n        # Deserialize back\n        deserialized = c.structure(json_dict, RealtimeResponseBody)\n\n        # Verify\n        self.assertIsNone(deserialized.message)\n        self.assertIsNone(deserialized.alternateProduct)\n        self.assertIsNotNone(deserialized.promotionalOffer)\n        self.assertEqual(message_id, deserialized.promotionalOffer.messageIdentifier)\n        self.assertIsNone(deserialized.promotionalOffer.promotionalOfferSignatureV2)\n        self.assertIsNotNone(deserialized.promotionalOffer.promotionalOfferSignatureV1)\n\n        deserialized_v1 = deserialized.promotionalOffer.promotionalOfferSignatureV1\n        self.assertEqual('com.example.product', deserialized_v1.productId)\n        self.assertEqual('offer123', deserialized_v1.offerIdentifier)\n        self.assertEqual(nonce, deserialized_v1.nonce)\n        self.assertEqual(1698148900000, deserialized_v1.timestamp)\n        self.assertEqual('keyId123', deserialized_v1.keyId)\n        self.assertEqual(app_account_token, deserialized_v1.appAccountToken)\n        self.assertEqual('base64encodedSignature', deserialized_v1.encodedSignature)\n"
  },
  {
    "path": "tests/test_x509_verifiction.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nimport unittest\nfrom unittest import mock\nfrom unittest.mock import MagicMock, patch\n\nfrom appstoreserverlibrary.signed_data_verifier import _ChainVerifier, VerificationException, VerificationStatus\nfrom base64 import b64decode, b64encode\n\nROOT_CA_BASE64_ENCODED = \"MIIBgjCCASmgAwIBAgIJALUc5ALiH5pbMAoGCCqGSM49BAMDMDYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8wHhcNMjMwMTA1MjEzMDIyWhcNMzMwMTAyMjEzMDIyWjA2MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJQ3VwZXJ0aW5vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc+/Bl+gospo6tf9Z7io5tdKdrlN1YdVnqEhEDXDShzdAJPQijamXIMHf8xWWTa1zgoYTxOKpbuJtDplz1XriTaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDRwAwRAIgemWQXnMAdTad2JDJWng9U4uBBL5mA7WI05H7oH7c6iQCIHiRqMjNfzUAyiu9h6rOU/K+iTR0I/3Y/NSWsXHX+acc\"\nINTERMEDIATE_CA_BASE64_ENCODED = \"MIIBnzCCAUWgAwIBAgIBCzAKBggqhkjOPQQDAzA2MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJQ3VwZXJ0aW5vMB4XDTIzMDEwNTIxMzEwNVoXDTMzMDEwMTIxMzEwNVowRTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xFTATBgNVBAoMDEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBUN5V9rKjfRiMAIojEA0Av5Mp0oF+O0cL4gzrTF178inUHugj7Et46NrkQ7hKgMVnjogq45Q1rMs+cMHVNILWqjNTAzMA8GA1UdEwQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMBAGCiqGSIb3Y2QGAgEEAgUAMAoGCCqGSM49BAMDA0gAMEUCIQCmsIKYs41ullssHX4rVveUT0Z7Is5/hLK1lFPTtun3hAIgc2+2RG5+gNcFVcs+XJeEl4GZ+ojl3ROOmll+ye7dynQ=\"\nLEAF_CERT_BASE64_ENCODED = \"MIIBoDCCAUagAwIBAgIBDDAKBggqhkjOPQQDAzBFMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUN1cGVydGlubzEVMBMGA1UECgwMSW50ZXJtZWRpYXRlMB4XDTIzMDEwNTIxMzEzNFoXDTMzMDEwMTIxMzEzNFowPTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xDTALBgNVBAoMBExlYWYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATitYHEaYVuc8g9AjTOwErMvGyPykPa+puvTI8hJTHZZDLGas2qX1+ErxgQTJgVXv76nmLhhRJH+j25AiAI8iGsoy8wLTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNIADBFAiBX4c+T0Fp5nJ5QRClRfu5PSByRvNPtuaTsk0vPB3WAIAIhANgaauAj/YP9s0AkEhyJhxQO/6Q2zouZ+H1CIOehnMzQ\"\n\nINTERMEDIATE_CA_INVALID_OID_BASE64_ENCODED = \"MIIBnjCCAUWgAwIBAgIBDTAKBggqhkjOPQQDAzA2MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJQ3VwZXJ0aW5vMB4XDTIzMDEwNTIxMzYxNFoXDTMzMDEwMTIxMzYxNFowRTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xFTATBgNVBAoMDEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBUN5V9rKjfRiMAIojEA0Av5Mp0oF+O0cL4gzrTF178inUHugj7Et46NrkQ7hKgMVnjogq45Q1rMs+cMHVNILWqjNTAzMA8GA1UdEwQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMBAGCiqGSIb3Y2QGAgIEAgUAMAoGCCqGSM49BAMDA0cAMEQCIFROtTE+RQpKxNXETFsf7Mc0h+5IAsxxo/X6oCC/c33qAiAmC5rn5yCOOEjTY4R1H1QcQVh+eUwCl13NbQxWCuwxxA==\"\nLEAF_CERT_FOR_INTERMEDIATE_CA_INVALID_OID_BASE64_ENCODED = \"MIIBnzCCAUagAwIBAgIBDjAKBggqhkjOPQQDAzBFMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUN1cGVydGlubzEVMBMGA1UECgwMSW50ZXJtZWRpYXRlMB4XDTIzMDEwNTIxMzY1OFoXDTMzMDEwMTIxMzY1OFowPTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xDTALBgNVBAoMBExlYWYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATitYHEaYVuc8g9AjTOwErMvGyPykPa+puvTI8hJTHZZDLGas2qX1+ErxgQTJgVXv76nmLhhRJH+j25AiAI8iGsoy8wLTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNHADBEAiAUAs+gzYOsEXDwQquvHYbcVymyNqDtGw9BnUFp2YLuuAIgXxQ3Ie9YU0cMqkeaFd+lyo0asv9eyzk6stwjeIeOtTU=\"\nLEAF_CERT_INVALID_OID_BASE64_ENCODED = \"MIIBoDCCAUagAwIBAgIBDzAKBggqhkjOPQQDAzBFMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUN1cGVydGlubzEVMBMGA1UECgwMSW50ZXJtZWRpYXRlMB4XDTIzMDEwNTIxMzczMVoXDTMzMDEwMTIxMzczMVowPTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlDdXBlcnRpbm8xDTALBgNVBAoMBExlYWYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATitYHEaYVuc8g9AjTOwErMvGyPykPa+puvTI8hJTHZZDLGas2qX1+ErxgQTJgVXv76nmLhhRJH+j25AiAI8iGsoy8wLTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsCBAIFADAKBggqhkjOPQQDAwNIADBFAiAb+7S3i//bSGy7skJY9+D4VgcQLKFeYfIMSrUCmdrFqwIhAIMVwzD1RrxPRtJyiOCXLyibIvwcY+VS73HYfk0O9lgz\"\n\nLEAF_CERT_PUBLIC_KEY = \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4rWBxGmFbnPIPQI0zsBKzLxsj8pD\\n2vqbr0yPISUx2WQyxmrNql9fhK8YEEyYFV7++p5i4YUSR/o9uQIgCPIhrA==\\n-----END PUBLIC KEY-----\\n\"\n\nREAL_APPLE_ROOT_BASE64_ENCODED = \"MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtfTjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySrMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM6BgD56KyKA==\"\nREAL_APPLE_INTERMEDIATE_BASE64_ENCODED = \"MIIDFjCCApygAwIBAgIUIsGhRwp0c2nvU4YSycafPTjzbNcwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMjEwMzE3MjAzNzEwWhcNMzYwMzE5MDAwMDAwWjB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbsQKC94PrlWmZXnXgtxzdVJL8T0SGYngDRGpngn3N6PT8JMEb7FDi4bBmPhCnZ3/sq6PF/cGcKXWsL5vOteRhyJ45x3ASP7cOB+aao90fcpxSv/EZFbniAbNgZGhIhpIo4H6MIH3MBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUu7DeoVgziJqkipnevr3rr9rLJKswRgYIKwYBBQUHAQEEOjA4MDYGCCsGAQUFBzABhipodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLWFwcGxlcm9vdGNhZzMwNwYDVR0fBDAwLjAsoCqgKIYmaHR0cDovL2NybC5hcHBsZS5jb20vYXBwbGVyb290Y2FnMy5jcmwwHQYDVR0OBBYEFD8vlCNR01DJmig97bB85c+lkGKZMA4GA1UdDwEB/wQEAwIBBjAQBgoqhkiG92NkBgIBBAIFADAKBggqhkjOPQQDAwNoADBlAjBAXhSq5IyKogMCPtw490BaB677CaEGJXufQB/EqZGd6CSjiCtOnuMTbXVXmxxcxfkCMQDTSPxarZXvNrkxU3TkUMI33yzvFVVRT4wxWJC994OsdcZ4+RGNsYDyR5gmdr0nDGg=\"\nREAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED = \"MIIEMTCCA7agAwIBAgIQR8KHzdn554Z/UoradNx9tzAKBggqhkjOPQQDAzB1MUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzYxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTI1MDkxOTE5NDQ1MVoXDTI3MTAxMzE3NDcyM1owgZIxQDA+BgNVBAMMN1Byb2QgRUNDIE1hYyBBcHAgU3RvcmUgYW5kIGlUdW5lcyBTdG9yZSBSZWNlaXB0IFNpZ25pbmcxLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNnVvhcv7iT+7Ex5tBMBgrQspHzIsXRi0Yxfek7lv8wEmj/bHiWtNwJqc2BoHzsQiEjP7KFIIKg4Y8y0/nynuAmjggIIMIICBDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFD8vlCNR01DJmig97bB85c+lkGKZMHAGCCsGAQUFBwEBBGQwYjAtBggrBgEFBQcwAoYhaHR0cDovL2NlcnRzLmFwcGxlLmNvbS93d2RyZzYuZGVyMDEGCCsGAQUFBzABhiVodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLXd3ZHJnNjAyMIIBHgYDVR0gBIIBFTCCAREwggENBgoqhkiG92NkBQYBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wHQYDVR0OBBYEFIFioG4wMMVA1ku9zJmGNPAVn3eqMA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgsBBAIFADAKBggqhkjOPQQDAwNpADBmAjEA+qXnREC7hXIWVLsLxznjRpIzPf7VHz9V/CTm8+LJlrQepnmcPvGLNcX6XPnlcgLAAjEA5IjNZKgg5pQ79knF4IbTXdKv8vutIDMXDmjPVT3dGvFtsGRwXOywR2kZCdSrfeot\"\n\nEFFECTIVE_DATE = 1761962975\nCLOCK_DATE = 41231\n\nclass X509Verification(unittest.TestCase):\n    def test_valid_chain_without_ocsp(self):\n        verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False)\n        public_key = verifier.verify_chain([\n            LEAF_CERT_BASE64_ENCODED,\n            INTERMEDIATE_CA_BASE64_ENCODED,\n            ROOT_CA_BASE64_ENCODED\n        ], False, EFFECTIVE_DATE)\n        self.assertEqual(LEAF_CERT_PUBLIC_KEY, public_key)\n\n    def test_valid_chain_invalid_intermediate_OID_without_OCSP(self):\n        verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False)\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_chain([\n                LEAF_CERT_FOR_INTERMEDIATE_CA_INVALID_OID_BASE64_ENCODED,\n                INTERMEDIATE_CA_INVALID_OID_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], False, EFFECTIVE_DATE)\n        self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status)\n\n    def test_valid_chain_invalid_leaf_OID_without_OCSP(self):\n        verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False)\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_chain([\n                LEAF_CERT_INVALID_OID_BASE64_ENCODED,\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], False, EFFECTIVE_DATE)\n        self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status)\n\n    def test_invalid_chain_length(self):\n        verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False)\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_chain([INTERMEDIATE_CA_BASE64_ENCODED, ROOT_CA_BASE64_ENCODED], False, EFFECTIVE_DATE)\n        self.assertIn('Verification failed with status INVALID_CHAIN_LENGTH', str(context.exception))\n\n    def test_invalid_base64_in_certificate_list(self):\n        verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)])\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_chain([\n                \"abc\",\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], False, EFFECTIVE_DATE)\n        self.assertEqual(VerificationStatus.INVALID_CERTIFICATE, context.exception.status)\n\n    def test_invalid_data_in_certificate_list(self):\n        verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False)\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_chain([\n                str(b64encode(b\"abc\")),\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], False, EFFECTIVE_DATE)\n        self.assertEqual(VerificationStatus.INVALID_CERTIFICATE, context.exception.status)\n    def test_malformed_root_cert(self):\n        verifier = _ChainVerifier([b64decode(b64encode(b\"abc\"))])\n        with self.assertRaises(Exception) as context:\n            verifier.verify_chain([\n                LEAF_CERT_BASE64_ENCODED,\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], False, EFFECTIVE_DATE)\n        self.assertEqual(VerificationStatus.INVALID_CERTIFICATE, context.exception.status)\n\n    def test_chain_different_than_root_certificate(self):\n        verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)], False)\n\n        with self.assertRaises(Exception) as context:\n            verifier.verify_chain([\n                LEAF_CERT_BASE64_ENCODED,\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], False, EFFECTIVE_DATE)\n        self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status)\n\n    def test_valid_expired_chain(self):\n        verifier = _ChainVerifier([b64decode(ROOT_CA_BASE64_ENCODED)], False)\n        with self.assertRaises(VerificationException) as context:\n            verifier.verify_chain([\n                LEAF_CERT_BASE64_ENCODED,\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], False, 2280946846)\n        self.assertEqual(VerificationStatus.VERIFICATION_FAILURE, context.exception.status)\n\n    def test_apple_chain_is_valid_with_ocsp_and_strict(self):\n        verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])\n        verifier.verify_chain([\n            REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,\n            REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,\n            REAL_APPLE_ROOT_BASE64_ENCODED\n        ], True, EFFECTIVE_DATE)\n\n    def test_ocsp_response_caching(self):\n        verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])\n        magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED)\n        verifier._verify_chain_without_caching = magic_mock\n        with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)):\n            verifier.verify_chain([\n                REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,\n                REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,\n                REAL_APPLE_ROOT_BASE64_ENCODED\n            ], True, EFFECTIVE_DATE)\n        self.assertEqual(1, magic_mock.call_count)\n        with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE + 1)): # 1 second\n            verifier.verify_chain([\n                REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,\n                REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,\n                REAL_APPLE_ROOT_BASE64_ENCODED\n            ], True, EFFECTIVE_DATE)\n        self.assertEqual(1, magic_mock.call_count)\n\n    def test_ocsp_response_caching_has_expiration(self):\n        verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])\n        magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED)\n        verifier._verify_chain_without_caching = magic_mock\n        with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)):\n            verifier.verify_chain([\n                REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,\n                REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,\n                REAL_APPLE_ROOT_BASE64_ENCODED\n            ], True, EFFECTIVE_DATE)\n        self.assertEqual(1, magic_mock.call_count)\n        with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE + 900)): # 15 minutes\n            verifier.verify_chain([\n                REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,\n                REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,\n                REAL_APPLE_ROOT_BASE64_ENCODED\n            ], True, EFFECTIVE_DATE)\n        self.assertEqual(2, magic_mock.call_count)\n\n    def test_ocsp_response_caching_with_different_chain(self):\n        verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])\n        magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED)\n        verifier._verify_chain_without_caching = magic_mock\n        with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)):\n            verifier.verify_chain([\n                LEAF_CERT_BASE64_ENCODED,\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], True, EFFECTIVE_DATE)\n        self.assertEqual(1, magic_mock.call_count)\n        with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): # Same\n            verifier.verify_chain([\n                REAL_APPLE_SIGNING_CERTIFICATE_BASE64_ENCODED,\n                REAL_APPLE_INTERMEDIATE_BASE64_ENCODED,\n                REAL_APPLE_ROOT_BASE64_ENCODED\n            ], True, EFFECTIVE_DATE)\n        self.assertEqual(2, magic_mock.call_count)\n\n    def test_ocsp_response_caching_with_slightly_different_chain(self):\n        verifier = _ChainVerifier([b64decode(REAL_APPLE_ROOT_BASE64_ENCODED)])\n        magic_mock = MagicMock(return_value=LEAF_CERT_BASE64_ENCODED)\n        verifier._verify_chain_without_caching = magic_mock\n        with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)):\n            verifier.verify_chain([\n                LEAF_CERT_BASE64_ENCODED,\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                ROOT_CA_BASE64_ENCODED\n            ], True, EFFECTIVE_DATE)\n        self.assertEqual(1, magic_mock.call_count)\n        with patch('time.time', mock.MagicMock(return_value=CLOCK_DATE)): # Same\n            verifier.verify_chain([\n                LEAF_CERT_BASE64_ENCODED,\n                INTERMEDIATE_CA_BASE64_ENCODED,\n                REAL_APPLE_ROOT_BASE64_ENCODED\n            ], True, EFFECTIVE_DATE)\n        self.assertEqual(2, magic_mock.call_count)\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "tests/test_xcode_signed_data.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nimport unittest\nfrom appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus\nfrom appstoreserverlibrary.models.Environment import Environment\nfrom appstoreserverlibrary.models.InAppOwnershipType import InAppOwnershipType\nfrom appstoreserverlibrary.models.OfferType import OfferType\nfrom appstoreserverlibrary.models.TransactionReason import TransactionReason\nfrom appstoreserverlibrary.models.Type import Type\nfrom appstoreserverlibrary.receipt_utility import ReceiptUtility\nfrom appstoreserverlibrary.signed_data_verifier import VerificationException\n\nfrom tests.util import get_signed_data_verifier, read_data_from_file\n\nXCODE_BUNDLE_ID = \"com.example.naturelab.backyardbirds.example\"\n\nclass ReceiptUtilityTest(unittest.TestCase):\n    def test_xcode_signed_app_transaction(self):\n        verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID)\n        encoded_app_transaction = read_data_from_file(\"tests/resources/xcode/xcode-signed-app-transaction\")\n\n        app_transaction = verifier.verify_and_decode_app_transaction(encoded_app_transaction)\n\n        self.assertIsNotNone(app_transaction)\n        self.assertIsNone(app_transaction.appAppleId)\n        self.assertEqual(XCODE_BUNDLE_ID, app_transaction.bundleId)\n        self.assertEqual(\"1\", app_transaction.applicationVersion)\n        self.assertIsNone(app_transaction.versionExternalIdentifier)\n        self.assertEqual(-62135769600000, app_transaction.originalPurchaseDate)\n        self.assertEqual(\"1\", app_transaction.originalApplicationVersion)\n        self.assertEqual(\"cYUsXc53EbYc0pOeXG5d6/31LGHeVGf84sqSN0OrJi5u/j2H89WWKgS8N0hMsMlf\", app_transaction.deviceVerification)\n        self.assertEqual(\"48c8b92d-ce0d-4229-bedf-e61b4f9cfc92\", app_transaction.deviceVerificationNonce)\n        self.assertIsNone(app_transaction.preorderDate)\n        self.assertEqual(Environment.XCODE, app_transaction.receiptType)\n        self.assertEqual(\"Xcode\", app_transaction.rawReceiptType)\n\n    def test_xcode_signed_transaction(self):\n        verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID)\n        encoded_transaction = read_data_from_file(\"tests/resources/xcode/xcode-signed-transaction\")\n\n        transaction = verifier.verify_and_decode_signed_transaction(encoded_transaction)\n\n        self.assertEqual(\"0\", transaction.originalTransactionId)\n        self.assertEqual(\"0\", transaction.transactionId)\n        self.assertEqual(\"0\", transaction.webOrderLineItemId)\n        self.assertEqual(XCODE_BUNDLE_ID, transaction.bundleId)\n        self.assertEqual(\"pass.premium\", transaction.productId)\n        self.assertEqual(\"6F3A93AB\", transaction.subscriptionGroupIdentifier)\n        self.assertEqual(1697679936049, transaction.purchaseDate)\n        self.assertEqual(1697679936049, transaction.originalPurchaseDate)\n        self.assertEqual(1700358336049, transaction.expiresDate)\n        self.assertEqual(1, transaction.quantity)\n        self.assertEqual(Type.AUTO_RENEWABLE_SUBSCRIPTION, transaction.type)\n        self.assertEqual(\"Auto-Renewable Subscription\", transaction.rawType)\n        self.assertIsNone(transaction.appAccountToken)\n        self.assertEqual(InAppOwnershipType.PURCHASED, transaction.inAppOwnershipType)\n        self.assertEqual(\"PURCHASED\", transaction.rawInAppOwnershipType)\n        self.assertEqual(1697679936056, transaction.signedDate)\n        self.assertIsNone(transaction.revocationReason)\n        self.assertIsNone(transaction.revocationDate)\n        self.assertFalse(transaction.isUpgraded)\n        self.assertEqual(OfferType.INTRODUCTORY_OFFER, transaction.offerType)\n        self.assertEqual(1, transaction.rawOfferType)\n        self.assertIsNone(transaction.offerIdentifier)\n        self.assertEqual(Environment.XCODE, transaction.environment)\n        self.assertEqual(\"Xcode\", transaction.rawEnvironment)\n        self.assertEqual(\"USA\", transaction.storefront)\n        self.assertEqual(\"143441\", transaction.storefrontId)\n        self.assertEqual(TransactionReason.PURCHASE, transaction.transactionReason)\n        self.assertEqual(\"PURCHASE\", transaction.rawTransactionReason)\n\n    def test_xcode_signed_renewal_info(self):\n        verifier = get_signed_data_verifier(Environment.XCODE, XCODE_BUNDLE_ID)\n        encoded_renewal_info = read_data_from_file(\"tests/resources/xcode/xcode-signed-renewal-info\")\n\n        renewal_info = verifier.verify_and_decode_renewal_info(encoded_renewal_info)\n\n        self.assertIsNone(renewal_info.expirationIntent)\n        self.assertEqual(\"0\", renewal_info.originalTransactionId)\n        self.assertEqual(\"pass.premium\", renewal_info.autoRenewProductId)\n        self.assertEqual(\"pass.premium\", renewal_info.productId)\n        self.assertEqual(AutoRenewStatus.ON, renewal_info.autoRenewStatus)\n        self.assertEqual(1, renewal_info.rawAutoRenewStatus)\n        self.assertIsNone(renewal_info.isInBillingRetryPeriod)\n        self.assertIsNone(renewal_info.priceIncreaseStatus)\n        self.assertIsNone(renewal_info.gracePeriodExpiresDate)\n        self.assertIsNone(renewal_info.offerType)\n        self.assertIsNone(renewal_info.offerIdentifier)\n        self.assertEqual(1697679936711, renewal_info.signedDate)\n        self.assertEqual(Environment.XCODE, renewal_info.environment)\n        self.assertEqual(\"Xcode\", renewal_info.rawEnvironment)\n        self.assertEqual(1697679936049, renewal_info.recentSubscriptionStartDate)\n        self.assertEqual(1700358336049, renewal_info.renewalDate)\n\n    def test_xcode_signed_app_transaction_with_production_environment(self):\n        verifier = get_signed_data_verifier(Environment.PRODUCTION, XCODE_BUNDLE_ID)\n        encoded_app_transaction = read_data_from_file(\"tests/resources/xcode/xcode-signed-app-transaction\")\n        try:\n            verifier.verify_and_decode_app_transaction(encoded_app_transaction)\n        except VerificationException:\n            return\n        self.assertFalse(True)\n\n"
  },
  {
    "path": "tests/util.py",
    "content": "# Copyright (c) 2023 Apple Inc. Licensed under MIT License.\n\nfrom typing import Any, Dict\nimport jwt\nfrom jwt.api_jwt import decode_complete\nimport json\nimport os\n\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives import serialization\nfrom appstoreserverlibrary.models.Environment import Environment\n\nfrom appstoreserverlibrary.signed_data_verifier import SignedDataVerifier\n\ndef create_signed_data_from_json(path: str) -> str:\n    data = read_data_from_file(path)\n    decoded_data = json.loads(data)\n    private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode()\n    return jwt.encode(payload=decoded_data, key=private_key, algorithm='ES256')\n\ndef decode_json_from_signed_date(data: str) -> Dict[str, Any]:\n    public_key = ec.generate_private_key(ec.SECP256R1()).public_key().public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo).decode()\n    return decode_complete(jwt=data, key=public_key, algorithms=['ES256'], options={\"verify_signature\": False})\n\ndef read_data_from_file(path: str) -> str:\n    full_path = os.path.join(path)\n    with open(full_path, mode='r') as test_file:\n        return test_file.read()\n    \ndef read_data_from_binary_file(path: str) -> str:\n    full_path = os.path.join(path)\n    with open(full_path, mode='rb') as test_file:\n        return test_file.read()\n\ndef get_signed_data_verifier(env: Environment, bundle_id: str, app_apple_id: int = 1234) -> SignedDataVerifier:\n    verifier = SignedDataVerifier([read_data_from_binary_file('tests/resources/certs/testCA.der')], False, env, bundle_id, app_apple_id)\n    verifier._chain_verifier.enable_strict_checks = False # We don't have authority identifiers on test certs\n    return verifier\n\ndef get_default_signed_data_verifier():\n    return get_signed_data_verifier(Environment.LOCAL_TESTING, \"com.example\")"
  }
]