diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..5fcf9011b Binary files /dev/null and b/.DS_Store differ diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 000000000..5ef78cb3f --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,45 @@ +--- +name: Issue Report +about: Report a bug, suggest an enhancement, or ask a question +title: "" +labels: "" +assignees: "" +--- + +## Issue Type + +Please inform the type(s) of issue(s) you are reporting: + +- Bug Report +- Feature Request +- Discussion +- Question + +## Description + +Please provide a clear and concise description of the issue or enhancement. +Include any relevant information that could help reproduce the issue or +understand the request. + +## Steps to Reproduce (for bugs) + +1. Step one +2. Step two +3. ... + +## Expected Behavior + +Describe what you expected to happen. + +## Actual Behavior + +Describe what actually happened. + +## Additional Context + +Add any other context about the issue here, including screenshots, logs, or +other supporting information. + +--- + +Thank you for taking the time to report this issue! diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..988cd8c60 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ + +## What is this Contribution About? + +Please provide a brief description of the changes or enhancements you are proposing in this pull request. + +## Issue Link + +Please link to the relevant issue that this pull request addresses: + +- Issue: [#ISSUE_NUMBER](link_to_issue) + +## Loom Video + +> Record a quick screencast describing your changes to help the team understand and review your contribution. This will greatly assist in the review process. + +## Demonstration Link + +> Provide a link to a branch or environment where this pull request can be tested and seen in action. diff --git a/.github/release.yaml b/.github/release.yaml new file mode 100644 index 000000000..c7ff97780 --- /dev/null +++ b/.github/release.yaml @@ -0,0 +1,20 @@ +changelog: + exclude: + labels: + - ignore-for-release + categories: + - title: Breaking Changes 🚨 + labels: + - breaking-change + - title: Exciting New Features 🎉 + labels: + - enhancement + - title: Preview features 🍪 + labels: + - preview-feature + - title: Bugs fixed 🐛 + labels: + - bug + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..aa1d2bb32 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,54 @@ +name: ci + +on: + push: + branches: + - main + tags: + - v* + pull_request: + branches: + - main +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + name: "Bundle & Check Apps" + steps: + - uses: actions/checkout@v3 + - name: cache deno installation and deno.land dependencies + uses: actions/cache@v2 + with: + key: ${{ runner.os }}-deno-${{ hashFiles('**/*') }} + restore-keys: ${{ runner.os }}-deno- + path: | + /home/runner/.deno + /home/runner/.cache/deno/deps/https/deno.land + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + - name: Bundle Apps + run: deno run -A --lock=deno.lock --lock-write --reload scripts/start.ts + + - name: Check + run: deno task check + + - name: Check if there are changes on ${{ matrix.os }} + id: changes + shell: bash + run: | + git status --porcelain + if [[ $(git status --porcelain | wc -c) -eq 0 ]]; then + echo "uncommitted changes detected" + exit 1 + fi + + - name: Test + continue-on-error: true + run: deno test --lock=deno.lock --lock-write -A . + + - name: Benchmark + continue-on-error: true + run: deno bench --lock=deno.lock --lock-write -A . diff --git a/.github/workflows/issues.yaml b/.github/workflows/issues.yaml new file mode 100644 index 000000000..d5f99f015 --- /dev/null +++ b/.github/workflows/issues.yaml @@ -0,0 +1,18 @@ +name: Label issues +on: + issues: + types: + - reopened + - opened +jobs: + label_issues: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - run: gh issue edit "$NUMBER" --add-label "$LABELS" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.issue.number }} + LABELS: triage diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000..c7e75e636 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,45 @@ +# https://docs.github.com/en/actions + +name: "Release" + +on: # yamllint disable-line rule:truthy + push: + tags: + - "**" + workflow_dispatch: # Allows manual dispatch with parameters + inputs: + tag_name: + description: "The tag to be published" + required: true +permissions: write-all +jobs: + release: + permissions: write-all + name: "Release" + runs-on: "ubuntu-latest" + + steps: + - name: "Create release" + env: + RELEASE_TAG: ${{ github.event.inputs.tag_name || github.ref_name }} + uses: "actions/github-script@v6" + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + script: | + const tag = process.env.RELEASE_TAG; + try { + const response = await github.rest.repos.createRelease({ + draft: false, + generate_release_notes: true, + name: `Release ${tag}`, + owner: context.repo.owner, + prerelease: tag.includes("rc-") || tag.includes("preview") || tag.includes("beta") || tag.includes("alpha"), + repo: context.repo.repo, + tag_name: tag, + }); + + core.exportVariable('RELEASE_ID', response.data.id); + core.exportVariable('RELEASE_UPLOAD_URL', response.data.upload_url); + } catch (error) { + core.setFailed(error.message); + } diff --git a/.github/workflows/releaser.yaml b/.github/workflows/releaser.yaml new file mode 100644 index 000000000..f4fb8d2a0 --- /dev/null +++ b/.github/workflows/releaser.yaml @@ -0,0 +1,172 @@ +name: Release Tagging + +on: + pull_request_target: + types: [opened] + + push: + branches: + - main + +permissions: + contents: write # Necessary for accessing and modifying repository content + pull-requests: write # Necessary for interacting with pull requests + actions: write # Necessary for triggering other workflows + +jobs: + tag-discussion: + if: github.event_name == 'pull_request_target' && github.event.action == 'opened' + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.base.ref }} # Checkout the base branch (target repository) + repository: ${{ github.event.pull_request.base.repo.full_name }} # Checkout from the target repo + + - name: Calculate new versions + id: calculate_versions + run: | + git fetch --tags + LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) + if [ -z "$LATEST_TAG" ]; then + LATEST_TAG="0.0.0" + fi + MAJOR=$(echo $LATEST_TAG | cut -d. -f1) + MINOR=$(echo $LATEST_TAG | cut -d. -f2) + PATCH=$(echo $LATEST_TAG | cut -d. -f3) + NEW_PATCH_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" + NEW_MINOR_VERSION="$MAJOR.$((MINOR + 1)).0" + NEW_MAJOR_VERSION="$((MAJOR + 1)).0.0" + echo "patch_version=$NEW_PATCH_VERSION" >> $GITHUB_OUTPUT + echo "minor_version=$NEW_MINOR_VERSION" >> $GITHUB_OUTPUT + echo "major_version=$NEW_MAJOR_VERSION" >> $GITHUB_OUTPUT + + - name: Start Discussion for Tagging + uses: peter-evans/create-or-update-comment@v2 + id: comment + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body: | + ## Tagging Options + Should a new tag be published when this PR is merged? + - 👍 for **Patch** ${{ steps.calculate_versions.outputs.patch_version }} update + - 🎉 for **Minor** ${{ steps.calculate_versions.outputs.minor_version }} update + - 🚀 for **Major** ${{ steps.calculate_versions.outputs.major_version }} update + + determine-tag: + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Find the Merged Pull Request + id: find_pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BASE_BRANCH="main" + RECENT_PR=$(gh pr list --state closed --base $BASE_BRANCH --json number,title,closedAt --jq '.[] | select(.closedAt >= "'$(date -u -d '1 minute ago' +%Y-%m-%dT%H:%M:%SZ)'") | {number, title}') + echo "RECENT_PR=$RECENT_PR" >> $GITHUB_ENV + echo "PR_NUMBER=$(echo $RECENT_PR | jq -r '.number')" >> $GITHUB_ENV + + - name: Fetch latest stable tag (excluding prerelease tags) + id: get_latest_tag + run: | + git fetch --tags + LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) + if [ -z "$LATEST_TAG" ]; then + LATEST_TAG="0.0.0" + fi + echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT + + - name: Determine the next version based on comments + id: determine_version + if: env.PR_NUMBER != '' + env: + PR_NUMBER: ${{ env.PR_NUMBER }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + LATEST_TAG=${{ steps.get_latest_tag.outputs.latest_tag }} + MAJOR=$(echo $LATEST_TAG | cut -d. -f1) + MINOR=$(echo $LATEST_TAG | cut -d. -f2) + PATCH=$(echo $LATEST_TAG | cut -d. -f3) + + # Define allowed users as a JSON array + ALLOWED_USERS=$(cat MAINTAINERS.txt | jq -R -s -c 'split("\n")[:-1]') + echo "Maintainers list: $ALLOWED_USERS" + + # Fetch reactions and filter by allowed users + REACTION=$(gh api graphql -f query=' + query { + repository(owner:"${{ github.repository_owner }}", name:"${{ github.event.repository.name }}") { + pullRequest(number: '${PR_NUMBER}') { + comments(last: 100) { + nodes { + body + id + reactions(last: 100) { + nodes { + content + user { + login + } + } + } + } + } + } + } + }' | jq -r --argjson allowed_users "$ALLOWED_USERS" ' + .data.repository.pullRequest.comments.nodes[] | + select(.body | contains("## Tagging Options")) | + .reactions.nodes[] | + select(.user.login | IN($allowed_users[])) | + .content' + ) + # Print the reaction to debug + echo "Captured reaction: $REACTION" + + # Convert reaction to uppercase to handle any case inconsistencies + REACTION=$(echo "$REACTION" | tr '[:lower:]' '[:upper:]') + + # Determine the new tag version based on the allowed reactions + if [[ "$REACTION" == *"ROCKET"* ]]; then + NEW_TAG="$((MAJOR + 1)).0.0" + elif [[ "$REACTION" == *"HOORAY"* ]]; then + NEW_TAG="$MAJOR.$((MINOR + 1)).0" + elif [[ "$REACTION" == *"THUMBS_UP"* ]]; then # Ensure thumbs up reaction is correctly identified + NEW_TAG="$MAJOR.$MINOR.$((PATCH + 1))" + else + echo "No valid reactions found for version bump. Exiting." + exit 0 + fi + + + echo "new_version=$NEW_TAG" >> $GITHUB_OUTPUT + + - name: Update deno.json Version + if: steps.determine_version.outputs.new_version != '' + run: | + jq --arg new_version "${{ steps.determine_version.outputs.new_version }}" '.version = $new_version' deno.json > tmp.$$.json && mv tmp.$$.json deno.json + git config user.name "decobot" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add deno.json + git commit -m "Update version to ${{ steps.determine_version.outputs.new_version }}" + git push origin main + + - name: Create and Push Tag + if: steps.determine_version.outputs.new_version != '' + run: | + git tag ${{ steps.determine_version.outputs.new_version }} + git push origin ${{ steps.determine_version.outputs.new_version }} + + - name: Trigger Release Workflow + run: | + curl -X POST \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.everest-preview+json" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/release.yaml/dispatches \ + -d '{"ref":"main", "inputs":{"tag_name":"${{ steps.determine_version.outputs.new_version }}"}}' diff --git a/.vscode/settings.json b/.vscode/settings.json index 1535e139e..e40716fdd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,4 @@ "deno.enable": true, "deno.lint": true, "deno.unstable": true -} \ No newline at end of file +} diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 000000000..e967f6cd7 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at {{ email }}. All complaints will be +reviewed and investigated and will result in a response that is deemed necessary +and appropriate to the circumstances. The project team is obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..5114d78e1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,88 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to the `deco-cx/apps` repository! We +are excited to have community members like you involved in improving our +collection of powerful apps. This document outlines how you can contribute +effectively to the project. + +## How to Contribute + +### Issues + +When submitting an issue, please use one of the following types: + +- **Issue/Bug**: Report bugs or track existing issues. +- **Issue/Discussion**: Start a discussion to gather input on a topic before it + becomes a proposal. +- **Issue/Proposal**: Propose a new idea or functionality. This allows for + feedback before any code is written. +- **Issue/Question**: Ask for help or clarification on any topic related to the + project. + +### Before You File an Issue + +Before submitting an issue, ensure the following: + +1. **Correct Repository**: Verify that you are filing the issue in the correct + repository within the deco ecosystem. +2. **Existing Issues**: Search through [open issues](./issues) to check if the + issue has already been reported or the feature has already been requested. +3. **For Bugs**: + - Confirm that it’s not an environment-specific issue. Ensure all + prerequisites (e.g., dependencies, configurations) are met. + - Provide detailed logs, stack traces, or any other relevant data to help + diagnose the issue. +4. **For Proposals**: + - Discuss potential features in the appropriate issue to gather feedback + before coding. + +### Pull Requests + +We welcome contributions via pull requests (PRs). Follow this workflow to submit +your changes: + +1. **Issue Reference**: Ensure there is an issue raised that corresponds to your + PR. +2. **Fork and Branch**: Fork the repository and create a new branch for your + changes. +3. **Code Changes**: + - Include appropriate tests with your code changes. + - Run linters and format the code according to project standards: + - Run `deno task check` +4. **Documentation**: Update any relevant documentation with your changes. +5. **Commit and PR**: Commit your changes and submit a PR for review. +6. **CI Checks**: Ensure that all Continuous Integration (CI) checks pass + successfully. +7. **Review Process**: A maintainer will review your PR, usually within a few + days. + +#### Work-in-Progress (WIP) PRs + +If you’d like early feedback on your work, you can create a PR with the prefix +`[WIP]` to indicate that it is still under development and not ready for +merging. + +### Testing Your Contributions + +Since this repository contains integrations that must be tested against a deco +site, you cannot test your contributions in isolation. Please refer to this +[Helpful content](https://www.notion.so/decocx/Helpful-Community-Content-101eec7ce64f4becaebb685dd9571e24) +for instructions on how to set up a deco site for testing purposes. + +### Use of Third-Party Code + +- Ensure that any third-party code included in your contributions comes with the + appropriate licenses. + +### Releasing a New Version + +We follow semantic versioning, and all apps in this repository are versioned +collectively using git tags. To release a new version: + +1. Fork the repository and create a pull request with your changes. +2. After the PR is approved and merged, request a maintainer to react the + releaser comment with the required emoji 👍 for **Patch** 🎉 for **Minor** 🚀 + for **Major** + +When your PR got merged, a new tag will arrive with the desired semver +modification. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..2b04acdad --- /dev/null +++ b/LICENSE @@ -0,0 +1,204 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 The Dapr Authors. + + and others that have contributed code to the public domain. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/MAINTAINERS.txt b/MAINTAINERS.txt new file mode 100644 index 000000000..70e7f287b --- /dev/null +++ b/MAINTAINERS.txt @@ -0,0 +1,3 @@ +guitavano +matheusgr +viktormarinho diff --git a/README.md b/README.md new file mode 100644 index 000000000..ed19abb41 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +
+ +Discord +  +Deco Twitter +  +![Build Status](https://github.com/deco-cx/apps/workflows/ci/badge.svg?event=push&branch=main) + +
+ +# Deco Standard **Apps** Library + +The Deco Framework logo: A capybara in its natural habitat + +Welcome to the `deco-cx/apps` repository! This repository is home to a collection of powerful apps that can be seamlessly integrated into your deco sites. Here, we provide you with a brief overview of the deco framework and introduce the concept of apps. We'll also delve into the repository structure, how to contribute, and more. Read more about apps in the [docs](https://www.deco.cx/docs/en/concepts/app), also if you want to see apps in action check our [storefront](https://github.com/deco-sites/storefront) repository. + +## About the deco Framework + +Deco, formerly known as `live`, is a modern and versatile framework that empowers developers to build dynamic and interactive websites with ease. Apps are a fundamental component of deco, offering a way to bundle sets of blocks together, all configured through an intuitive UI and sharing a common state defined within the app module. + +## Repository Structure + +The `deco-cx/apps` repository is structured as a monorepo, housing a collection of individual apps, each stored in a dedicated subfolder. These apps can be installed separately, providing you with the flexibility to choose and integrate only the functionalities that suit your project's needs. + +The `deco.ts` file serves as a hub where you can specify the apps of this repository. However it is important to notice that whether you choose to create apps within this repository or within your own organization's repository, there are no limitations on where apps should be developed. + +## Overview + +At the core of all websites within this repository is the `website` app, located in the `website` folder. This app lays the foundation for websites, offering a set of common features that are essential regardless of whether your site is an e-commerce platform or not. We've also structured the repository to accommodate specific platforms, such as e-commerce platforms like VTEX, Shopify, VNDA, and more. These platform-specific apps depend on the `website` app, leveraging its shared features while adding platform-specific functionality. + +## Contributing + +Check [CONTRIBUTING.md](https://github.com/deco-cx/apps/blob/main/CONTRIBUTING.md) + +### Apps + +| App Name | Description | Manifest | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| Algolia | Algolia search integration. Provides loaders and workflows for searching and indexing on Algolia | [manifest](/algolia/manifest.gen.ts) | +| ai-assistant | AI Assistant is a hub for artificial intelligence (AI) assistants, allowing you to register your own AI assistants and invoke them through automated actions. | [manifest](/ai-assistant/manifest.gen.ts) | +| analytics | Analytics is a powerful data analysis tool, providing valuable insights into the performance and behavior of users on your application or platform. | [manifest](/analytics/manifest.gen.ts) | +| brand-assistant | Brand Assistant is a brand assistance tool. | [manifest](/brand-assistant/manifest.gen.ts) | +| commerce | A simple configurable start for any e-commerce platform, lets you switch between any of those | [manifest](/commerce/manifest.gen.ts) | +| $live | An app for compatibility with $live blocks. | [manifest](/compat/$live/manifest.gen.ts) | +| deco-sites/std | An app for compatibility with deco-sites/std app, contains various blocks merged from e-commerce apps. | [manifest](/compat/std/manifest.gen.ts) | +| decohub | The best place to find an app for your business case, here is where apps published by any developer in the deco ecosystem will live. | [manifest](/decohub/manifest.gen.ts) | +| implementation | App for project implementation details. | [manifest](/implementation/manifest.gen.ts) | +| Konfidency | An app that uses extension block to add Aggregate Rating to products by integrating with the "[Konfidency](https://www.konfidency.com.br/)" provider. | [manifest](/konfidency/manifest.gen.ts) | +| Linx | The app for e-commerce that uses Linx as a platform. | [manifest](/linx/manifest.gen.ts) | +| nuvemshop | The app for e-commerce that uses Nuvemshop as a platform. | [manifest](/nuvemshop/manifest.gen.ts) | +| openai | Connects to OpenAI services to generate AI-powered content. | [manifest](/openai/manifest.gen.ts) | +| power-reviews | Power Reviews is an integration to show reviews and ratings of your products. It allow your customers to give a rating, write texts, emphasis pros/cons and upload images and videos. | [manifest](/power-reviews/manifest.gen.ts) | +| Resend | App for sending emails using [https://resend.com/](https://resend.com/) | [manifest](/resend/manifest.gen.ts) | +| EmailJS | App for sending emails using [https://www.emailjs.com/](https://www.emailjs.com/) | [manifest](/emailjs/manifest.gen.ts) | +| Shopify | The app for e-commerce that uses Shopify as a platform. | [manifest](/shopify/manifest.gen.ts) | +| sourei | Digitalize your business with Sourei. Offering a wide range of digital solutions, from domain registration to advanced project infrastructure. | [manifest](/sourei/manifest.gen.ts) | +| typesense | Typesense search integration. Provides loaders and workflows for searching and indexing on Typesense. | [manifest](/typesense/manifest.gen.ts) | +| Verified Reviews | An app that uses extension block to add Aggregate Rating to products by integrating with the "[Opiniões Verificadas](https://www.opinioes-verificadas.com.br/br/)" provider. | [manifest](/verified-reviews/manifest.gen.ts) | +| VNDA | The app for e-commerce that uses VNDA as a platform. | [manifest](/vnda/manifest.gen.ts) | +| VTEX | The app for e-commerce that uses VTEX as a platform. | [manifest](/vtex/manifest.gen.ts) | +| Wake | The app for e-commerce that uses Wake as a platform. | [manifest](/wake/manifest.gen.ts) | +| Weather | Weather is an application that provides accurate and up-to-date weather information. | [manifest](/weather/manifest.gen.ts) | +| Website | The base app of any website. Contains `Page.tsx` block and other common loaders like image and fonts. | [manifest](/website/manifest.gen.ts) | +| Workflows | App for managing workflows. | [manifest](/workflows/manifest.gen.ts) | + +## E-commerce Integrations - Status + +| Integrations | Home | PLP | PDP | Cart | Checkout proxy | Order placed proxy | My account proxy | +| :--------------------------------------------------------------------------------------------------------- | :--- | :-- | :-- | :--- | :------------- | :----------------- | :--------------- | +| [VTEX](https://github.com/deco-cx/apps/blob/main/vtex/README.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [VNDA](https://github.com/deco-cx/apps/blob/main/vnda/README.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Shopify](https://github.com/deco-cx/apps/blob/b072c1fdfab8d5f1647ed42f9dbaae618f28f05f/shopify/README.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | +| [Linx](https://github.com/deco-cx/apps/blob/main/linx/README.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Linx impulse | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Nuvemshop](https://github.com/deco-cx/apps/blob/main/nuvemshop/README.MD) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | +| [Wake](https://github.com/deco-cx/apps/blob/main/wake/README.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | + +## Review Integrations - Status + +| Integrations | Extension PDP | Extension ProductList | Extension Listing Page | Submit Review | +| :--------------------------------------------------------------------------------------- | :------------ | :-------------------- | :--------------------- | :------------ | +| [Power Reviews](https://github.com/deco-cx/apps/blob/main/power-reviews/README.md) | ✅ | ✅ | ✅ | ✅ | +| [Verified Reviews](https://github.com/deco-cx/apps/blob/main/verified-reviews/README.md) | ✅ | ✅ | ✅ | 🔴 | +| [Konfidency](https://github.com/deco-cx/apps/blob/main/konfidency/README.md) | ✅ | 🔴 | 🔴 | 🔴 | + +#### Adding a new app to Deco Hub + +In order to make your app available to be installable in any deco site, just import/export your app inside decohub/apps folder. + +## Thanks to all contributors + + + + diff --git a/admin/types.ts b/admin/types.ts new file mode 100644 index 000000000..ad617df3b --- /dev/null +++ b/admin/types.ts @@ -0,0 +1,41 @@ +import { type fjp } from "./deps.ts"; +import { type Resolvable } from "@deco/deco"; +export interface Pagination { + data: T[]; + page: number; + pageSize: number; + total: number; +} +export interface PatchState { + type: "patch-state"; + payload: fjp.Operation[]; + revision: string; +} +export interface FetchState { + type: "fetch-state"; +} +export interface StatePatched { + type: "state-patched"; + payload: fjp.Operation[]; + revision: string; + // Maybe add data and user info in here + metadata?: unknown; +} +export interface StateFetched { + type: "state-fetched"; + payload: State; +} +export interface OperationFailed { + type: "operation-failed"; + code: "UNAUTHORIZED" | "INTERNAL_SERVER_ERROR"; + reason: string; +} +export type Acked = T & { + ack: string; +}; +export interface State { + decofile: Record; + revision: string; +} +export type Commands = PatchState | FetchState; +export type Events = StatePatched | StateFetched | OperationFailed; diff --git a/admin/widgets.ts b/admin/widgets.ts new file mode 100644 index 000000000..6ed9942ee --- /dev/null +++ b/admin/widgets.ts @@ -0,0 +1,71 @@ +/** + * @format image-uri + */ +export type ImageWidget = string; + +/** + * @format html + */ +export type HTMLWidget = string; + +/** + * @format video-uri + */ +export type VideoWidget = string; + +/** + * @format dynamic-options + * @options website/loaders/options/routes.ts + */ +export type SiteRoute = string; + +/** + * @format map + */ +export type MapWidget = string; + +/** + * @format textarea + */ +export type TextArea = string; + +/** + * @format rich-text + */ +export type RichText = string; + +/** + * @format color-input + * @default #000000 + */ +export type Color = string; + +/** + * @format code + * @language css + */ +export type CSS = string; + +/** + * @format code + * @language typescript + */ +export type TypeScript = string; + +/** + * @format code + * @language json + */ +export type Json = string; + +/** + * @format file-uri + * @accept text/csv + */ +export type CSVWidget = string; + +/** + * @format file-uri + * @accept application/pdf + */ +export type PDFWidget = string; diff --git a/ai-assistants/actions/awsUploadImage.ts b/ai-assistants/actions/awsUploadImage.ts new file mode 100644 index 000000000..34eb20426 --- /dev/null +++ b/ai-assistants/actions/awsUploadImage.ts @@ -0,0 +1,66 @@ +import base64ToBlob from "../utils/blobConversion.ts"; +import { AssistantIds } from "../types.ts"; +import { AppContext } from "../mod.ts"; +import { logger, meter, ValueType } from "@deco/deco/o11y"; +const stats = { + awsUploadImageError: meter.createCounter("assistant_aws_upload_error", { + unit: "1", + valueType: ValueType.INT, + }), +}; +export interface AWSUploadImageProps { + file: string | ArrayBuffer | null; + assistantIds?: AssistantIds; +} +// TODO(ItamarRocha): Check if possible to upload straight to bucket instead of using presigned url +async function getSignedUrl( + mimetype: string, + ctx: AppContext, +): Promise { + const randomID = crypto.randomUUID(); + const name = `${randomID}.${mimetype.split("/")[1]}`; + // Get signed URL from S3 + const s3Params = { + Bucket: ctx.assistantAwsProps?.assistantBucketName.get?.() ?? "", + Key: name, + ContentType: mimetype, + ACL: "public-read", + }; + const uploadURL = await ctx.s3?.getSignedUrlPromise("putObject", s3Params); + return uploadURL as string; +} +async function uploadFileToS3(presignedUrl: string, data: Blob) { + const response = await fetch(presignedUrl, { method: "PUT", body: data }); + return response; +} +// TODO(ItamarRocha): Rate limit +export default async function awsUploadImage( + awsUploadImageProps: AWSUploadImageProps, + _req: Request, + ctx: AppContext, +) { + const assistantId = awsUploadImageProps.assistantIds?.assistantId; + const threadId = awsUploadImageProps.assistantIds?.threadId; + const blobData = base64ToBlob( + awsUploadImageProps.file, + "image", + awsUploadImageProps.assistantIds, + ); + const uploadURL = await getSignedUrl(blobData.type, ctx); + const uploadResponse = await uploadFileToS3(uploadURL, blobData); + if (!uploadResponse.ok) { + stats.awsUploadImageError.add(1, { + assistantId, + }); + throw new Error(`Failed to upload file: ${uploadResponse.statusText}`); + } + logger.info({ + assistantId: assistantId, + threadId: threadId, + context: "awsUploadImage", + response: JSON.stringify(uploadResponse), + uploadUrl: uploadURL, + }); + const imageUrl = new URL(uploadURL); + return `${imageUrl.origin}${imageUrl.pathname}`; +} diff --git a/ai-assistants/actions/chat.ts b/ai-assistants/actions/chat.ts new file mode 100644 index 000000000..10f777d37 --- /dev/null +++ b/ai-assistants/actions/chat.ts @@ -0,0 +1,178 @@ +import { AppContext } from "../mod.ts"; +import { messageProcessorFor } from "../chat/messages.ts"; +import { Notify, Queue } from "../deps.ts"; +import { badRequest, notFound } from "@deco/deco"; +export interface Props { + thread?: string; + assistant: string; + message?: string; +} +/** + * Processes messages from the message queue. + * @param {Queue} q - The message queue. + * @param {Notify} abort - The notification object for aborting the message processing. + * @param {(c: ChatMessage) => Promise} processor - The function for processing each message. + * @returns {Promise} - A promise representing the completion of the message processing. + */ +const process = async ( + q: Queue, + abort: Notify, + processor: (c: ChatMessage) => Promise, +) => { + while (true) { + const message = await Promise.race([ + abort.notified(), + q.pop(), + ]); + if (typeof message !== "object") { + break; + } + await Promise.race([ + processor(message), + ]); + } +}; +export interface MessageContentText { + type: "text"; + value: string; + options?: string[]; +} +export interface MessageContentFile { + type: "file"; + fileId: string; +} +export interface ReplyMessage { + threadId: string; + messageId: string; + type: "message" | "error"; + content: Array; + role: "user" | "assistant"; +} +export interface FunctionCall { + name: string; + props: unknown; +} +export interface FunctionCallReply extends FunctionCall { + response: T; +} +export interface ReplyStartFunctionCall { + threadId: string; + messageId: string; + type: "start_function_call"; + content: FunctionCall; +} +export interface ReplyFunctionCalls { + threadId: string; + messageId: string; + type: "function_calls"; + content: FunctionCallReply[]; +} +export type Reply = + | ReplyMessage + | ReplyFunctionCalls + | ReplyStartFunctionCall; +export interface ChatMessage { + text: string; + reply: (reply: Reply) => void; +} +/** + * Initializes a WebSocket chat connection and processes incoming messages. + * @param {Props} props - The properties for the chat session. + * @param {Request} req - The HTTP request object containing the WebSocket connection. + * @param {AppContext} ctx - The application context. + * @returns {Response} - The HTTP response object. + */ +export default async function openChat( + props: Props, + req: Request, + ctx: AppContext, +): Promise< + Response | { + replies: Reply[]; + thread: string; + } +> { + if (!props.assistant) { + notFound(); + } + const assistant = ctx.assistants[props.assistant]; + if (!assistant) { + notFound(); + } + const threads = ctx.openAI.beta.threads; + const threadId = props.thread; + const threadPromise = threadId + ? threads.retrieve(threadId) + : threads.create(); + const processorPromise = assistant.then(async (aiAssistant) => + messageProcessorFor(aiAssistant, ctx, await threadPromise) + ); + if (req.headers.get("upgrade") !== "websocket") { + if (!props.message) { + badRequest({ message: "message is required when websocket is not used" }); + } + const processor = await processorPromise; + const replies: Reply[] = []; + await processor({ + text: props.message!, + reply: (reply) => replies.push(reply), + }); + return { replies, thread: (await threadPromise).id }; + } + const { socket, response } = Deno.upgradeWebSocket(req); + const abort = new Notify(); + const messagesQ = new Queue(); + if (props.message) { + messagesQ.push({ + text: props.message, + reply: (replyMsg) => socket.send(JSON.stringify(replyMsg)), + }); + } + /** + * Handles the WebSocket connection on open event. + */ + socket.onopen = async () => { + process( + messagesQ, + abort, + await processorPromise.catch((err) => { + socket.send( + `An error was suddently ocurred when message processor was created. ${err}`, + ); + socket.close(); + abort.notifyAll(); + throw err; + }), + ); + assistant.then((aiAssistant) => { + socket.send(JSON.stringify({ + isWelcomeMessage: true, + threadId: aiAssistant.threadId, + assistantId: aiAssistant.id, + type: "message", + content: [{ + type: "text", + value: aiAssistant.welcomeMessage ?? "Welcome to the chat!", + }], + role: "assistant", + })); + }); + }; + /** + * Handles the WebSocket connection on close event. + */ + socket.onclose = () => { + abort.notifyAll(); + }; + /** + * Handles the WebSocket connection on message event. + * @param {MessageEvent} event - The WebSocket message event. + */ + socket.onmessage = (event) => { + messagesQ.push({ + text: event.data, + reply: (replyMsg) => socket.send(JSON.stringify(replyMsg)), + }); + }; + return response; +} diff --git a/ai-assistants/actions/describeImage.ts b/ai-assistants/actions/describeImage.ts new file mode 100644 index 000000000..22296dc35 --- /dev/null +++ b/ai-assistants/actions/describeImage.ts @@ -0,0 +1,91 @@ +import { AssistantIds } from "../types.ts"; +import { AppContext } from "../mod.ts"; +import { logger, meter, ValueType } from "@deco/deco/o11y"; +import { shortcircuit } from "@deco/deco"; +const stats = { + promptTokens: meter.createHistogram("assistant_image_prompt_tokens", { + description: "Tokens used in Sales Assistant Describe Image Input - OpenAI", + valueType: ValueType.INT, + }), + completionTokens: meter.createHistogram("assistant_image_completion_tokens", { + description: + "Tokens used in Sales Assistant Describe Image Output - OpenAI", + valueType: ValueType.INT, + }), + describeImageError: meter.createCounter("assistant_describe_image_error", { + unit: "1", + valueType: ValueType.INT, + }), +}; +export interface DescribeImageProps { + uploadURL: string; + userPrompt: string; + assistantIds?: AssistantIds; +} +// TODO(ItamarRocha): Rate limit +// TODO(@ItamarRocha): Refactor to use https://github.com/deco-cx/apps/blob/main/openai/loaders/vision.ts +export default async function describeImage( + describeImageProps: DescribeImageProps, + _req: Request, + ctx: AppContext, +) { + const assistantId = describeImageProps.assistantIds?.assistantId; + const threadId = describeImageProps.assistantIds?.threadId; + try { + const response = await ctx.openAI.chat.completions.create({ + model: "gpt-4-vision-preview", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: + `Describe this image in few words focus on it's main characteristics. + This description will be used to search similar items in an e-commerce store, + so describe name of the product and other relevant information. Use NO MORE than 3 words to describe the product. + Avoid using colors. Also, take into consideration the user prompt and describe the object it + focuses on if there is one. Output should be 1-2 sentences and should be a request summarizing + the user's need/request to a sales assistant that will search the product in an e-commerce store. + + * Use the same language as the user prompt in your answer * + + User prompt: + ${describeImageProps.userPrompt}`, + }, + { + type: "image_url", + image_url: { + "url": describeImageProps.uploadURL, + }, + }, + ], + }, + ], + }); + logger.info({ + assistantId: assistantId, + threadId: threadId, + context: "describeImage", + response: JSON.stringify(response), + props: describeImageProps, + }); + stats.promptTokens.record(response.usage?.prompt_tokens ?? 0, { + assistant_id: assistantId, + }); + stats.completionTokens.record(response.usage?.completion_tokens ?? 0, { + assistant_id: assistantId, + }); + return response; + } catch (error) { + stats.describeImageError.add(1, { + assistantId, + }); + shortcircuit( + new Response(JSON.stringify({ error: error.error.message }), { + status: error.status, + headers: error.headers, + }), + ); + } +} diff --git a/ai-assistants/actions/transcribeAudio.ts b/ai-assistants/actions/transcribeAudio.ts new file mode 100644 index 000000000..453417b22 --- /dev/null +++ b/ai-assistants/actions/transcribeAudio.ts @@ -0,0 +1,60 @@ +import base64ToBlob from "../utils/blobConversion.ts"; +import { AssistantIds } from "../types.ts"; +import { AppContext } from "../mod.ts"; +import { logger, meter, ValueType } from "@deco/deco/o11y"; +const stats = { + audioSize: meter.createHistogram("assistant_transcribe_audio_size", { + description: + "Audio size used in Sales Assistant Transcribe Image Input - OpenAI", + unit: "s", + valueType: ValueType.DOUBLE, + }), + transcribeAudioError: meter.createCounter( + "assistant_transcribe_audio_error", + { + unit: "1", + valueType: ValueType.INT, + }, + ), +}; +export interface TranscribeAudioProps { + file: string | ArrayBuffer | null; + assistantIds?: AssistantIds; + audioDuration: number; +} +// TODO(ItamarRocha): Rate limit +export default async function transcribeAudio( + transcribeAudioProps: TranscribeAudioProps, + _req: Request, + ctx: AppContext, +) { + const assistantId = transcribeAudioProps.assistantIds?.assistantId; + const threadId = transcribeAudioProps.assistantIds?.threadId; + if (!transcribeAudioProps.file) { + stats.transcribeAudioError.add(1, { + assistantId, + }); + throw new Error("Audio file is empty"); + } + const blobData = base64ToBlob( + transcribeAudioProps.file, + "audio", + transcribeAudioProps.assistantIds, + ); + const file = new File([blobData], "input.wav", { type: "audio/wav" }); + stats.audioSize.record(transcribeAudioProps.audioDuration, { + assistant_id: assistantId, + }); + const response = await ctx.openAI.audio.transcriptions.create({ + model: "whisper-1", + file: file, + }); + logger.info({ + assistantId: assistantId, + threadId: threadId, + context: "transcribeAudio", + subcontext: "response", + response: JSON.stringify(response), + }); + return response; +} diff --git a/ai-assistants/chat/messages.ts b/ai-assistants/chat/messages.ts new file mode 100644 index 000000000..169eab460 --- /dev/null +++ b/ai-assistants/chat/messages.ts @@ -0,0 +1,319 @@ +import { Context, type JSONSchema7, lazySchemaFor } from "@deco/deco"; +import { meter, ValueType } from "@deco/deco/o11y"; +import { weakcache } from "../../utils/weakcache.ts"; +import { + ChatMessage, + FunctionCallReply, + Reply, + ReplyMessage, +} from "../actions/chat.ts"; +import { + AssistantCreateParams, + RequiredActionFunctionToolCall, + Thread, +} from "../deps.ts"; +import { threadMessageToReply, Tokens } from "../loaders/messages.ts"; +import { AIAssistant, AppContext } from "../mod.ts"; +import { dereferenceJsonSchema } from "../schema.ts"; +const stats = { + latency: meter.createHistogram("assistant_latency", { + description: + "assistant latency (time it takes from the moment the server receives the request to the moment it sends the response)", + unit: "ms", + valueType: ValueType.DOUBLE, + }), +}; +// Max length of instructions. The maximum context of the assistant is 32K chars. We use 25K for instructions to be safe. +const MAX_INSTRUCTIONS_LENGTH = 25000; +const notUndefined = (v: T | undefined): v is T => v !== undefined; +const toolsCache = new weakcache.WeakLRUCache({ + cacheSize: 16, // up to 16 different schemas stored here. +}); +/** + * Builds assistant tools that can be used by OpenAI assistant to execute actions based on users requests. + * @param assistant the assistant that will handle the request + * @returns an array of available functions that can be used. + */ +const appTools = async ( + assistant: AIAssistant, +): Promise => { + const ctx = Context.active(); + const assistantsKey = assistant.availableFunctions?.join(",") ?? "all"; + const revision = await ctx.release!.revision(); + const cacheKey = `${assistantsKey}@${revision}`; + if (toolsCache.has(cacheKey)) { + return toolsCache.get(cacheKey)!; + } + const toolsPromise = ctx.runtime!.then(async (runtime) => { + const schemas = await lazySchemaFor(ctx).value; + const functionKeys = assistant.availableFunctions ?? Object.keys({ + ...runtime.manifest.loaders, + ...runtime.manifest.actions, + }); + const tools = functionKeys.map((functionKey) => { + const functionDefinition = btoa(functionKey); + const schema = schemas.definitions[functionDefinition]; + if ( + (schema as { + ignoreAI?: boolean; + })?.ignoreAI + ) { + return undefined; + } + const propsRef = (schema?.allOf?.[0] as JSONSchema7)?.$ref; + if (!propsRef) { + return undefined; + } + const dereferenced = dereferenceJsonSchema({ + $ref: propsRef, + ...schemas, + }); + if ( + dereferenced.type !== "object" || + (dereferenced.oneOf || dereferenced.anyOf || + dereferenced?.allOf || dereferenced?.enum || dereferenced?.not) + ) { + return undefined; + } + return { + type: "function" as const, + function: { + name: functionKey, + description: + `Usage for: ${schema?.description}. Example: ${schema?.examples}`, + parameters: { + ...dereferenced, + definitions: undefined, + root: undefined, + }, + }, + }; + }).filter(notUndefined); + toolsCache.set(ctx, tools); + return tools; + }); + toolsCache.set(cacheKey, toolsPromise); + return toolsPromise; +}; +export interface ProcessorOpts { + assistantId: string; + instructions: string; +} +const sleep = (ns: number) => new Promise((resolve) => setTimeout(resolve, ns)); +const cache: Record = {}; +const invokeFor = ( + ctx: AppContext, + assistant: AIAssistant, + onFunctionCallStart: ( + tool: RequiredActionFunctionToolCall, + props: unknown, + ) => void, + onFunctionCallEnd: ( + tool: RequiredActionFunctionToolCall, + props: unknown, + response: unknown, + ) => void, +) => { + return async (call: RequiredActionFunctionToolCall) => { + try { + const props = JSON.parse(call.function.arguments || "{}"); + const cacheKey = `${call.function.arguments}${call.function.name}`; + const assistantProps = assistant?.useProps?.(props) ?? props; + cache[cacheKey] ??= ctx.invoke( + // deno-lint-ignore no-explicit-any + call.function.name as any, + assistantProps, + ); + onFunctionCallStart(call, assistantProps); + const invokeResponse = await cache[cacheKey]; + onFunctionCallEnd(call, assistantProps, invokeResponse); + return { + tool_call_id: call.id, + output: JSON.stringify(invokeResponse), + }; + } catch (err) { + console.error("invoke error", err); + return { + tool_call_id: call.id, + output: "[]", + }; + } + }; +}; +/** + * Creates a message processor function for the given AI assistant and context. + * @param {AIAssistant} assistant - The AI assistant for processing messages. + * @param {AppContext} ctx - The application context. + * @returns {Promise<(message: ChatMessage) => void>} - A function that processes incoming chat messages. + */ +export const messageProcessorFor = async ( + assistant: AIAssistant, + ctx: AppContext, + thread: Thread, +) => { + const openAI = ctx.openAI; + const threads = openAI.beta.threads; + const instructions = + `${ctx.instructions}. Introduce yourself as ${assistant.name}. ${assistant.instructions}. The files uploaded to the assistant should + give you a good context of how the products you are dealing with are formatted. ${ + assistant.prompts + ? "Below are arbitrary prompt that gives you information about the current context, it can be empty." + : "" + }\n${ + (assistant.prompts ?? []).map((prompt) => + `this is the ${prompt.context}: ${prompt.content}` + ) + }. DO NOT CHANGE FUNCTIONS NAMES, do not remove .ts at the end of function name. do not remove / at the end of function name. + If you are positive that your response contains the information that the user requests (like product descriptions, product names, prices, colors, and sizes), add an ${Tokens.POSITIVE} symbol at the end of the phrase. Otherwise, add a ${Tokens.NEGATIVE} symbol. + For example, if the user asks about product availability and you have the information, respond with "The product is in stock. @". If you don't have the information, respond with "I'm sorry, the product is currently unavailable. ${Tokens.NEGATIVE}". + `; + const assistantId = await ctx.assistant.then((assistant) => assistant.id); + const tools = await appTools(assistant); + // Update the assistant object with the thread and assistant id + assistant.threadId = thread.id; + assistant.id = assistantId; + /** + * Processes an incoming chat message. + * @param {ChatMessage} message - The incoming chat message. + * @returns {Promise} - A promise representing the completion of message processing. + */ + return async ({ text: content, reply: _reply }: ChatMessage) => { + // send message + const start = performance.now(); + await threads.messages.create(thread.id, { + content, + role: "user", + }); + // create run + const run = await threads.runs.create(thread.id, { + model: typeof assistant.model === "object" + ? assistant.model.custom + : assistant.model, + assistant_id: assistantId, + instructions: instructions.slice(0, MAX_INSTRUCTIONS_LENGTH), + tools, + }); + const messageId = run.id; + // Wait for the assistant answer + const functionCallReplies: FunctionCallReply[] = []; + // Reply to the user + const reply = (message: Reply) => { + assistant.onMessageSent?.({ + assistantId: run.assistant_id, + threadId: thread.id, + runId: run.id, + model: run.model, + message, + }); + return _reply(message); + }; + assistant.onMessageReceived?.({ + assistantId: run.assistant_id, + threadId: thread.id, + runId: run.id, + model: run.model, + message: { type: "text", value: content }, + }); + const invoke = invokeFor(ctx, assistant, (call, props) => { + reply({ + threadId: thread.id, + messageId, + type: "start_function_call", + content: { + name: call.function.name, + props, + }, + }); + stats.latency.record(performance.now() - start, { + type: "start_function_call", + assistant_id: run.assistant_id, + }); + }, (call, props, response) => { + functionCallReplies.push({ + name: call.function.name, + props, + response, + }); + }); + let runStatus; + do { + runStatus = await threads.runs.retrieve(thread.id, run.id); + if (runStatus.status === "requires_action") { + const actions = runStatus.required_action!; + const outputs = actions.submit_tool_outputs; + const tool_outputs = await Promise.all(outputs.tool_calls.map(invoke)); + if (tool_outputs.length === 0) { + const message: ReplyMessage = { + messageId: Date.now().toString(), + threadId: thread.id, + type: "error", + content: [], + role: "assistant", + }; + reply(message); + return; + } + await threads.runs.submitToolOutputs(thread.id, run.id, { + tool_outputs, + }); + runStatus = await threads.runs.retrieve(thread.id, run.id); + } + await sleep(500); + } while (["in_progress", "queued"].includes(runStatus.status)); + const messages = await threads.messages.list(thread.id); + const threadMessages = messages.data; + const lastMsg = threadMessages + .findLast((message) => + message.run_id == run.id && message.role === "assistant" + ); + if (!lastMsg) { + // TODO(@mcandeia) in some cases the bot didn't respond anything. + const message: ReplyMessage = { + messageId: Date.now().toString(), + threadId: thread.id, + type: "error", + content: [], + role: "assistant", + }; + reply(message); + return; + } + const replyMessage = threadMessageToReply(lastMsg); + // multi tool use parallel seems to be some sort of openai bug, and it seems to have no solution yet. + // https://community.openai.com/t/model-tries-to-call-unknown-function-multi-tool-use-parallel/490653 + // It's an error that only happens every now and then. Open ai tries to call "multi_tool_use.parallel" function that doesn't even exist and isn't even in the OpenAI documentation + // If functionCallReplies is not an array it should also be considered an error + if ( + (functionCallReplies.length === 1 && + functionCallReplies[0].name === "multi_tool_use.parallel") || + !Array.isArray(functionCallReplies) + ) { + const message: ReplyMessage = { + messageId: Date.now().toString(), + threadId: thread.id, + type: "error", + content: [], + role: "assistant", + }; + reply(message); + } else { + reply(replyMessage); + stats.latency.record(performance.now() - start, { + type: "text", + assistant_id: run.assistant_id, + }); + } + if (functionCallReplies.length > 0) { + reply({ + threadId: thread.id, + messageId, + type: "function_calls" as const, + content: functionCallReplies, + }); + stats.latency.record(performance.now() - start, { + type: "function_calls", + assistant_id: run.assistant_id, + }); + } + }; +}; diff --git a/ai-assistants/deps.ts b/ai-assistants/deps.ts new file mode 100644 index 000000000..8f283a6dc --- /dev/null +++ b/ai-assistants/deps.ts @@ -0,0 +1,18 @@ +export type { + Assistant, + AssistantCreateParams, +} from "https://deno.land/x/openai@v4.19.1/resources/beta/assistants/assistants.ts"; + +export { Notify } from "https://deno.land/x/async@v2.0.2/notify.ts"; +export { Queue } from "https://deno.land/x/async@v2.0.2/queue.ts"; +export type { + MessageContentImageFile, + MessageContentText, + ThreadMessage, +} from "https://deno.land/x/openai@v4.19.1/resources/beta/threads/messages/messages.ts"; +export type { + RequiredActionFunctionToolCall, +} from "https://deno.land/x/openai@v4.19.1/resources/beta/threads/runs/runs.ts"; +export type { + Thread, +} from "https://deno.land/x/openai@v4.19.1/resources/beta/threads/threads.ts"; diff --git a/ai-assistants/hooks/useFileUpload.ts b/ai-assistants/hooks/useFileUpload.ts new file mode 100644 index 000000000..7ce05dd7a --- /dev/null +++ b/ai-assistants/hooks/useFileUpload.ts @@ -0,0 +1,9 @@ +import { invoke } from "../runtime.ts"; + +const state = { + describeImage: invoke["ai-assistants"].actions.describeImage, + awsUploadImage: invoke["ai-assistants"].actions.awsUploadImage, + transcribeAudio: invoke["ai-assistants"].actions.transcribeAudio, +}; + +export const useFileUpload = () => state; diff --git a/ai-assistants/loaders/messages.ts b/ai-assistants/loaders/messages.ts new file mode 100644 index 000000000..51c26dca5 --- /dev/null +++ b/ai-assistants/loaders/messages.ts @@ -0,0 +1,90 @@ +import { ReplyMessage } from "../actions/chat.ts"; +import { + MessageContentImageFile, + MessageContentText, + ThreadMessage, +} from "../deps.ts"; +import { AppContext } from "../mod.ts"; +export interface Props { + thread: string; + after?: string; + before?: string; +} + +export const Tokens = { + POSITIVE: "@", + NEGATIVE: "#", + OPTIONS: "&&&", +}; + +const normalize = (strContent: string) => { + const hasOptions = strContent.includes(Tokens.OPTIONS); + + if (!hasOptions) { + return strContent.endsWith(Tokens.POSITIVE) || + strContent.endsWith(Tokens.NEGATIVE) + ? strContent.slice(0, strContent.length - 2) + : strContent; + } + + return strContent.split(Tokens.OPTIONS)[0]; +}; + +export const getToken = (message: ThreadMessage): string => { + const text = (message.content[0] as MessageContentText).text?.value; + if (!text) { + return Tokens.NEGATIVE; + } + return text.endsWith(Tokens.POSITIVE) ? Tokens.POSITIVE : Tokens.NEGATIVE; +}; + +export const threadMessageToReply = (message: ThreadMessage): ReplyMessage => { + return { + threadId: message.thread_id!, + messageId: message.run_id!, + type: "message", + content: message.content.map((cnt) => + isFileContent(cnt) ? { type: "file", fileId: cnt.image_file.file_id! } : { + type: "text", + value: normalize(cnt.text!.value), + options: getOptions(cnt), + } + ), + role: message.role, + }; +}; + +// Function to for the token OPTIONS in the message content and get everything after it to get the options. +// If your response contains options for the user to choose from, make sure to include the ${Tokens.OPTIONS} symbol in your response, followed by the options separated by commas, followed by another ${Tokens.OPTIONS} symbol. +const getOptions = (content: MessageContentText): string[] => { + const text = content.text?.value; + + if (!text) { + return []; + } + const options = text.split(Tokens.OPTIONS)[1]; + + if (!options) { + return []; + } + + return options.split(",").map((option) => option.trim()); +}; + +const isFileContent = ( + v: MessageContentImageFile | MessageContentText, +): v is MessageContentImageFile => { + return (v as MessageContentImageFile)?.image_file?.file_id !== undefined; +}; + +export default async function messages( + { thread, after, before }: Props, + _req: Request, + ctx: AppContext, +): Promise { + const messages = await ctx.openAI.beta.threads.messages.list(thread, { + after, + before, + }); + return messages.data.map(threadMessageToReply); +} diff --git a/ai-assistants/logo.png b/ai-assistants/logo.png new file mode 100644 index 000000000..be41da6b9 Binary files /dev/null and b/ai-assistants/logo.png differ diff --git a/ai-assistants/manifest.gen.ts b/ai-assistants/manifest.gen.ts new file mode 100644 index 000000000..84b6b47d7 --- /dev/null +++ b/ai-assistants/manifest.gen.ts @@ -0,0 +1,27 @@ +// DO NOT EDIT. This file is generated by deco. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $$$$$$$$$0 from "./actions/awsUploadImage.ts"; +import * as $$$$$$$$$1 from "./actions/chat.ts"; +import * as $$$$$$$$$2 from "./actions/describeImage.ts"; +import * as $$$$$$$$$3 from "./actions/transcribeAudio.ts"; +import * as $$$0 from "./loaders/messages.ts"; + +const manifest = { + "loaders": { + "ai-assistants/loaders/messages.ts": $$$0, + }, + "actions": { + "ai-assistants/actions/awsUploadImage.ts": $$$$$$$$$0, + "ai-assistants/actions/chat.ts": $$$$$$$$$1, + "ai-assistants/actions/describeImage.ts": $$$$$$$$$2, + "ai-assistants/actions/transcribeAudio.ts": $$$$$$$$$3, + }, + "name": "ai-assistants", + "baseUrl": import.meta.url, +}; + +export type Manifest = typeof manifest; + +export default manifest; diff --git a/ai-assistants/mod.ts b/ai-assistants/mod.ts new file mode 100644 index 000000000..ae3b64a21 --- /dev/null +++ b/ai-assistants/mod.ts @@ -0,0 +1,207 @@ +import AWS from "https://esm.sh/aws-sdk@2.1585.0"; +import { deferred } from "std/async/deferred.ts"; +import openai, { + Props as OpenAIProps, + State as OpenAIState, +} from "../openai/mod.ts"; +import { Assistant } from "./deps.ts"; +import manifest, { Manifest } from "./manifest.gen.ts"; +import { Secret } from "../website/loaders/secret.ts"; +import { PreviewContainer } from "../utils/preview.tsx"; +import { + type App, + type AppContext as AC, + type AppManifest, + asResolved, + type AvailableActions, + type AvailableLoaders, + isDeferred, +} from "@deco/deco"; +import { isAwaitable } from "@deco/deco/utils"; +export type GPTModel = + | "gpt-4-0613" + | "gpt-4-0314" + | "gpt-4-1106-preview" + | "gpt-4" + | "gpt-3.5-turbo-1106" + | "gpt-3.5-turbo-16k" + | "gpt-3.5-turbo-16k-0613" + | "gpt-3.5-turbo" + | "gpt-3.5-turbo-0613"; +/** + * Represents an AI Assistant with specific capabilities and configurations. + * @template TManifest - The type of the AppManifest associated with the AI Assistant. + */ +export interface AIAssistant { + /** + * The name of the AI Assistant. + */ + name: string; + /** + * Optional instructions or guidelines for the AI Assistant. + */ + instructions?: string; + /** + * Optional array of prompts to provide context for the AI Assistant. + */ + prompts?: Prompt[]; + /** + * Optional welcome message to be displayed when the chat session starts. + */ + welcomeMessage?: string; + /** + * Optional list of available functions (actions or loaders) that the AI Assistant can perform. + */ + availableFunctions?: Array< + AvailableActions | AvailableLoaders + >; + /** + * Optional function to customize the handling of properties (props) passed to the AI Assistant. + * It takes a set of properties and returns a modified set of properties. + * @param {unknown} props - The properties passed to the AI Assistant. + * @returns {unknown} - The modified properties. + */ + useProps?: (props: unknown) => unknown; + /** + * Optional function to log the received messages from the user. + * @param {Log} logInfo - User message / information. + * @returns {void} - The modified properties. + */ + onMessageReceived?: (logInfo: Log) => void; + /** + * Optional function to log the received messages sent by the assistant. + * @param {Log} logInfo - Assistant message / information. + * @returns {void} - The modified properties. + */ + onMessageSent?: (logInfo: Log) => void; + /** + * The GPT model that will be used, if not specified the assistant model will be used. + */ + model?: GPTModel | { + custom: string; + }; + /** + * The Id of the assistant + */ + id?: string; + /** + * The Id of the assistant thread + */ + threadId?: string; +} +export interface Log { + assistantId: string; + threadId: string; + runId: string; + model: string; + message: object; +} +export interface Prompt { + content: string; + context: string; +} +export interface AssistantAwsProps { + assistantBucketRegion: Secret; + accessKeyId: Secret; + secretAccessKey: Secret; + assistantBucketName: Secret; +} +export interface Props extends OpenAIProps { + /** + * @description the assistant Id + */ + assistantId: string; + /** + * @description Instructions + */ + instructions?: string; + assistants?: AIAssistant[]; + assistantAwsProps?: AssistantAwsProps; + s3?: AWS.S3; +} +export interface State extends OpenAIState { + instructions?: string; + assistant: Promise; + assistants: Record>; + assistantAwsProps?: AssistantAwsProps; + s3?: AWS.S3; +} +/** + * @title Deco AI Assistant + * @description Create AI assistants on deco.cx. + * @category Tool + * @logo https://raw.githubusercontent.com/deco-cx/apps/main/ai-assistants/logo.png + */ +export default function App(state: Props): App, +]> { + const openAIApp = openai(state); + const assistantsAPI = openAIApp.state.openAI.beta.assistants; + // Sets assistantId only if state.assistants exists + const assistantId = (state.assistants?.[0] ?? null) !== null + ? state.assistantId + : ""; + return { + manifest, + state: { + ...openAIApp.state, + assistants: (state.assistants ?? []).filter(Boolean).reduce( + (acc, assistant) => { + const assistantDeferred = deferred(); + if (isDeferred(assistant)) { + const aiAssistant = assistant(); + if (isAwaitable(aiAssistant)) { + aiAssistant.then(assistantDeferred.resolve).catch( + assistantDeferred.reject, + ); + } else { + assistantDeferred.resolve(aiAssistant); + } + } else if (assistant) { + assistantDeferred.resolve(assistant); + } + return { [assistant.name]: assistantDeferred, ...acc }; + }, + {}, + ), + instructions: `${state.instructions ?? ""}`, + assistant: assistantsAPI.retrieve(assistantId), + s3: new AWS.S3({ + region: state.assistantAwsProps?.assistantBucketRegion.get?.() ?? + Deno.env.get("ASSISTANT_BUCKET_REGION"), + accessKeyId: state.assistantAwsProps?.accessKeyId.get?.() ?? + Deno.env.get("AWS_ACCESS_KEY_ID"), + secretAccessKey: state.assistantAwsProps?.secretAccessKey.get?.() ?? + Deno.env.get("AWS_SECRET_ACCESS_KEY"), + }), + assistantAwsProps: state.assistantAwsProps, + }, + dependencies: [openAIApp], + }; +} +export const onBeforeResolveProps = (props: Props) => { + if (Array.isArray(props?.assistants)) { + return { + ...props, + assistants: props.assistants.map((assistant) => + asResolved(assistant, true) + ), + }; + } + return props; +}; +export type AppContext = AC>; +export const preview = () => { + return { + Component: PreviewContainer, + props: { + name: "Deco AI Assistant", + owner: "deco.cx", + description: "Create AI assistants on deco.cx.", + logo: + "https://raw.githubusercontent.com/deco-cx/apps/main/ai-assistants/logo.png", + images: [], + tabs: [], + }, + }; +}; diff --git a/ai-assistants/runtime.ts b/ai-assistants/runtime.ts new file mode 100644 index 000000000..da42a2435 --- /dev/null +++ b/ai-assistants/runtime.ts @@ -0,0 +1,3 @@ +import { Manifest } from "./manifest.gen.ts"; +import { proxy } from "@deco/deco/web"; +export const invoke = proxy(); diff --git a/ai-assistants/schema.ts b/ai-assistants/schema.ts new file mode 100644 index 000000000..493aff815 --- /dev/null +++ b/ai-assistants/schema.ts @@ -0,0 +1,32 @@ +import { type JSONSchema7 } from "@deco/deco"; +const isJSONSchema = (v: unknown | JSONSchema7): v is JSONSchema7 & { + $ref: string; +} => { + return (typeof v === "object" && ((v as JSONSchema7)?.$ref !== undefined)); +}; +export function dereferenceJsonSchema( + schema: JSONSchema7 & { + definitions?: Record; + }, +) { + const resolveReference = ( + obj: unknown, + visited: Record, + ): JSONSchema7 => { + if (isJSONSchema(obj)) { + if (visited[obj["$ref"]]) { + return {}; + } + visited[obj["$ref"]] = true; + const [_, __, ref] = obj["$ref"].split("/"); + return resolveReference(schema?.definitions?.[ref], visited); + } else if (obj && typeof obj === "object") { + const recordObj = obj as Record; + for (const key in recordObj) { + recordObj[key] = resolveReference(recordObj[key], visited); + } + } + return obj as JSONSchema7; + }; + return resolveReference(schema, {}); +} diff --git a/ai-assistants/types.ts b/ai-assistants/types.ts new file mode 100644 index 000000000..eb8ff617e --- /dev/null +++ b/ai-assistants/types.ts @@ -0,0 +1,38 @@ +/** + * A personalized assistant configuration + */ +export interface AssistantPersonalization { + /** + * @title The assistant's name + */ + nickname?: string; + + /** + * @title The assistant's personality + */ + mood?: + | "Friendly" + | "Straight to the Point" + | "Humorous" + | "Professional" + | "Enthusiastic" + | "Informative" + | "Sarcastic" + | "Formal" + | "Energetic" + | "Curious" + | "Confident" + | "Helpful"; +} + +export interface AssistantIds { + /** + * @title The assistant's id + */ + assistantId?: string; + + /** + * @title The current thread id + */ + threadId?: string; +} diff --git a/ai-assistants/utils/blobConversion.ts b/ai-assistants/utils/blobConversion.ts new file mode 100644 index 000000000..826cba88c --- /dev/null +++ b/ai-assistants/utils/blobConversion.ts @@ -0,0 +1,51 @@ +import { AssistantIds } from "../types.ts"; +import { logger } from "@deco/deco/o11y"; +export default function base64ToBlob( + base64: string | ArrayBuffer | null, + context: string, + assistantIds?: AssistantIds, +): Blob { + let regex = + /^data:(audio\/[a-z0-9\-\+\.]+|video\/[a-z0-9\-\+\.]+);base64,(.*)$/; + if (context === "image") { + regex = /^data:(image\/[a-z]+);base64,(.*)$/; + } + // Split the base64 string into the MIME type and the base64 encoded data + if (!base64 || typeof base64 !== "string") { + logger.error(`${ + JSON.stringify({ + assistantId: assistantIds?.assistantId, + threadId: assistantIds?.threadId, + context: context, + error: "Expected a base64 string, typeof base64 is not string", + }) + }`); + throw new Error("Expected a base64 string"); + } + const parts = base64.match(regex); + if (!parts || parts.length !== 3) { + logger.error(`${ + JSON.stringify({ + assistantId: assistantIds?.assistantId, + threadId: assistantIds?.threadId, + context: context, + error: `${context} Base64 string is not properly formatted: ${base64}`, + }) + }`); + throw new Error( + `${context} Base64 string is not properly formatted: ${parts}`, + ); + } + const mimeType = parts[1]; // e.g., 'audio/png' or 'video/mp4' or 'audio/mp3' or 'image/png' + const mediaData = parts[2]; + // Convert the base64 encoded data to a binary string + const binaryStr = atob(mediaData); + // Convert the binary string to an array of bytes (Uint8Array) + const length = binaryStr.length; + const arrayBuffer = new Uint8Array(new ArrayBuffer(length)); + for (let i = 0; i < length; i++) { + arrayBuffer[i] = binaryStr.charCodeAt(i); + } + // Create and return the Blob object + return new Blob([arrayBuffer], { type: mimeType }); +} diff --git a/algolia/README.md b/algolia/README.md new file mode 100644 index 000000000..b20a965d8 --- /dev/null +++ b/algolia/README.md @@ -0,0 +1,44 @@ + +Loaders, actions and workflows for adding Agolia search, the leader in globally scalable, secure, digital search and discovery experiences that are ultrafast and reliable, to your deco.cx website. + +Algolia is a general purpose indexer. This means you can save any Json document and later retrieve it using the Search API. Although being a simpler solution than competing alternatives like Elastic Search or Solar, setting up an index on Algolia still requires some software engineering, like setting up searchable fields, facet fields and sorting indices. Hopefully, deco.cx introduces canonical types, like Product, ProductGroup, etc. These schemas allow this app to built solutions for common use-cases, like indexing Products for a Product Listing Page. + +# Installation +1. Install via decohub +2. Open your Algolia dashboard and grab your keys at settings > API Keys +3. Copy & Paste your Application ID and Admin API Key +4. Save & Publish this block +5. Click on the button below to setup the necessary indices on Algolia + +
+
+ +
+
+ + +> Use `Admin API Key` instead of `Search-Only API Key` since this app tweaks search params and create/deletes records + +## Integrating to Algolia +This App uses deco.cx canonical types in a push-based architecture. This means anyone interested in indexing any supported canonical type just need to invoke the right workflow passing the right input payload. Below you can see the schematics of how ecommerce platforms use the `workflows/index/product.ts` workflow for indexing products +Screenshot 2023-09-20 at 17 44 33 + +As you can see, this App already receives the `Product` as input parameter, so it's up to the ecommerce platform integration to invoke the workflow. +If you want to use Algolia with a supported ecommerce platform (VTEX, VNDA, Wake etc), install the ecommerce platform app and register Algolia workflow to listen to product events. +If you want to implement a new platform integration, you can base yourself on existing trigger implementation at VTEX/workflows/events.ts + +### VTEX Integration +To integrate with VTEX: + +1. Open your admin at deco.cx +2. Under Blocks > Apps, make sure both VTEX and Algolia apps are installed +3. Connect a product update event from VTEX into Algolia. For this: +0. 1. Under Blocks > Workflows -> events.ts open your VTEX-trigger block. +0. 2. Connect `Product` event from VTEX into Algolia by clicking on `+ Add Product` and selecting `Algolia Integration - Product Event` +0. 3. Save & Publish + +image + +🎉 Your setup is complete! You should now be able to see the products being indexed on Algolia. + +After indexing is complete, you can open your Pages at deco.cx and change loaders to Algolia Loaders diff --git a/algolia/actions/index/product.ts b/algolia/actions/index/product.ts new file mode 100644 index 000000000..cb266d05c --- /dev/null +++ b/algolia/actions/index/product.ts @@ -0,0 +1,43 @@ +import { ApiError } from "npm:@algolia/transporter@4.20.0"; +import { Product } from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { toIndex } from "../../utils/product.ts"; +import { INDEX_NAME } from "../../loaders/product/list.ts"; + +interface Props { + product: Product; + action: "DELETE" | "UPSERT"; + indexName?: string; +} + +// deno-lint-ignore no-explicit-any +const isAPIError = (x: any): x is ApiError => + typeof x?.status === "number" && + x.name === "ApiError"; + +const action = async (props: Props, _req: Request, ctx: AppContext) => { + const { indexName = INDEX_NAME, product, action } = props; + const { client } = ctx; + + try { + const indexProduct = toIndex(product); + + const { taskID } = action === "UPSERT" + ? await client.initIndex(indexName).saveObject(indexProduct, { + autoGenerateObjectIDIfNotExist: false, + }) + : await client.initIndex(indexName).deleteObject(indexProduct.objectID); + + return taskID; + } catch (error) { + console.error(error); + + if (isAPIError(error) && error.status === 400) { + return null; + } + + throw error; + } +}; + +export default action; diff --git a/algolia/actions/index/wait.ts b/algolia/actions/index/wait.ts new file mode 100644 index 000000000..31d4a2336 --- /dev/null +++ b/algolia/actions/index/wait.ts @@ -0,0 +1,16 @@ +import { INDEX_NAME } from "../../loaders/product/list.ts"; +import { AppContext } from "../../mod.ts"; + +interface Props { + taskID: number; + indexName?: string; +} + +const action = async (props: Props, _req: Request, ctx: AppContext) => { + const { client } = ctx; + const { indexName = INDEX_NAME } = props; + + await client.initIndex(indexName).waitTask(props.taskID); +}; + +export default action; diff --git a/algolia/actions/setup.ts b/algolia/actions/setup.ts new file mode 100644 index 000000000..f9900f5bc --- /dev/null +++ b/algolia/actions/setup.ts @@ -0,0 +1,23 @@ +import { AppContext } from "../mod.ts"; +import { setupProductsIndices } from "../utils/product.ts"; + +type Props = Partial<{ + adminApiKey: string; + applicationId: string; +}>; + +const action = async (props: Props, _req: Request, ctx: AppContext) => { + const { adminApiKey, applicationId } = props; + if (!adminApiKey || !applicationId) { + return "Missing Keys/AppId"; + } + + try { + await setupProductsIndices(applicationId, adminApiKey, ctx.client); + return "Setup finished successfuly"; + } catch (error) { + return `Setup finished with error: ${error}`; + } +}; + +export default action; diff --git a/algolia/loaders/client.ts b/algolia/loaders/client.ts new file mode 100644 index 000000000..65cef149b --- /dev/null +++ b/algolia/loaders/client.ts @@ -0,0 +1,14 @@ +import type { AppContext } from "../mod.ts"; +import type { SearchClient } from "https://esm.sh/algoliasearch@4.20.0"; + +export type AlgoliaClient = SearchClient; + +export default function loader( + _props: unknown, + _req: Request, + ctx: AppContext, +): AlgoliaClient { + const { client } = ctx; + + return client; +} diff --git a/algolia/loaders/product/list.ts b/algolia/loaders/product/list.ts new file mode 100644 index 000000000..766e706f4 --- /dev/null +++ b/algolia/loaders/product/list.ts @@ -0,0 +1,63 @@ +import { SearchResponse } from "npm:@algolia/client-search"; +import { Product } from "../../../commerce/types.ts"; + +import { AppContext } from "../../mod.ts"; +import { + IndexedProduct, + Indices, + resolveProducts, +} from "../../utils/product.ts"; + +interface Props { + /** + * @title Count + * @description Max number of products to return + */ + hitsPerPage: number; + + /** + * @title Facets + * @description Facets to filter by + */ + facetFilters?: string; + + /** @description Full text search query */ + term?: string; + indexName?: string; +} + +export const INDEX_NAME: Indices = "products"; + +/** + * @title Algolia Integration + */ +const loader = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { client } = ctx; + const { indexName = INDEX_NAME } = props; + + const { results } = await client.search([{ + indexName, + query: props.term ?? "", + params: { + hitsPerPage: props.hitsPerPage ?? 12, + facetFilters: JSON.parse(props.facetFilters ?? "[]"), + clickAnalytics: true, + }, + }]); + + const { hits: products, queryID } = results[0] as SearchResponse< + IndexedProduct + >; + + return resolveProducts(products, client, { + url: req.url, + queryID, + indexName, + }); +}; + +export default loader; diff --git a/algolia/loaders/product/listingPage.ts b/algolia/loaders/product/listingPage.ts new file mode 100644 index 000000000..30bc32186 --- /dev/null +++ b/algolia/loaders/product/listingPage.ts @@ -0,0 +1,280 @@ +import { SearchResponse } from "npm:@algolia/client-search"; +import { Filter, ProductListingPage } from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { replaceHighlight } from "../../utils/highlight.ts"; +import { + IndexedProduct, + Indices, + resolveProducts, +} from "../../utils/product.ts"; + +/** @titleBy label */ +interface Facet { + /** + * @title Facet Name + * @description These are the facet names available at Algolia dashboard > search > index */ + name: string; + + /** @description Facet label to be rendered on the site UI */ + label: string; +} + +interface Props { + /** + * @title Count + * @description Max number of products to return + */ + hitsPerPage: number; + + /** + * @title Facets + * @description List of facet names from Product to render on the website + */ + facets?: Facet[]; + + /** + * @description https://www.algolia.com/doc/api-reference/api-parameters/sortFacetValuesBy/ + */ + sortFacetValuesBy?: "count" | "alpha"; + + /** @description Full text search query */ + term?: string; + + /** @description Enable to highlight matched terms */ + highlight?: boolean; + + /** @description Hide Unavailable Items */ + hideUnavailable?: boolean; + + /** + * @description ?page search params for the first page + * @default 0 + */ + startingPage?: 0 | 1; +} + +const getPageInfo = ( + page: number, + nbPages: number, + nbHits: number, + hitsPerPage: number, + url: URL, + startingPage: number, +) => { + const next = page + 1; + const prev = page - 1; + const hasNextPage = next < nbPages; + const hasPreviousPage = prev >= 0; + const nextPage = new URLSearchParams(url.searchParams); + const previousPage = new URLSearchParams(url.searchParams); + + if (hasNextPage) { + nextPage.set("page", `${next + startingPage}`); + } + + if (hasPreviousPage) { + previousPage.set("page", `${prev + startingPage}`); + } + + return { + nextPage: hasNextPage ? `?${nextPage}` : undefined, + previousPage: hasPreviousPage ? `?${previousPage}` : undefined, + records: nbHits, + recordPerPage: hitsPerPage, + currentPage: page + startingPage, + }; +}; + +// Transforms facets and re-orders so they match what's configured on deco admin +const transformFacets = ( + facets: Record>, + options: { order: Facet[]; facetFilters: [string, string[]][]; url: URL }, +): Filter[] => { + const { facetFilters, url, order } = options; + const params = new URLSearchParams(url.searchParams); + const filters = Object.fromEntries(facetFilters); + const orderByKey = new Map( + order.map(({ name, label }, index) => [name, { label, index }]), + ); + const entries = Object.entries(facets); + + const transformed: Filter[] = new Array(entries.length); + for (let it = 0; it < entries.length; it++) { + const [key, values] = entries[it]; + const filter = filters[key] ?? []; + let index: number | undefined = it; + let label: string | undefined = key; + + // Apply sort only when user set facets on deco admin + if (orderByKey.size > 0) { + index = orderByKey.get(key)?.index; + label = orderByKey.get(key)?.label; + } + + if (index === undefined || label === undefined) continue; + + transformed[index] = { + "@type": "FilterToggle", + quantity: 0, + label, + key, + values: Object.entries(values).map(([value, quantity]) => { + const index = filter.findIndex((f) => f === value); + const selected = index > -1; + const newFilter = selected + ? { + ...filters, + [key]: [...filter].filter((f) => f !== value), + } + : { + ...filters, + [key]: [...filter, value], + }; + + if (newFilter[key].length === 0) { + delete newFilter[key]; + } + + params.set("facetFilters", JSON.stringify(Object.entries(newFilter))); + + return { + value, + quantity, + label: value, + selected, + url: `?${params}`, + }; + }), + }; + } + + return transformed.filter(Boolean); +}; + +const getIndex = (options: string | null): Indices => { + switch (options) { + case "relevance": + return "products"; + case "price_asc": + return "products_price_asc"; + case "price_desc": + return "products_price_desc"; + default: + return "products"; + } +}; + +/** + * @title Algolia Integration + */ +const loader = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const url = new URL(req.url); + const { client } = ctx; + const indexName = getIndex(url.searchParams.get("sort")); + const startingPage = props.startingPage ?? 0; + const pageIndex = Number(url.searchParams.get("page")) || startingPage; + + const facetFilters: [string, string[]][] = JSON.parse( + url.searchParams.get("facetFilters") ?? "[]", + ); + + if (props.hideUnavailable) { + facetFilters.push(["available", ["true"]]); + } + + // Creates a canonical facet representation format + // Facets on the same category are grouped by OR and facets on + // different categories are split by an AND. e.g.: + // + // (department:"man" OR department:"woman") AND (brand:"deco") AND (available:"true") + const fFilters = facetFilters.map(([key, values]) => + `(${values.map((value) => `${key}:"${value}"`).join(" OR ")})` + ).join(" AND "); + + const { results } = await client.search([ + { + indexName, + query: props.term ?? url.searchParams.get("q") ?? + url.searchParams.get("query") ?? "", + params: { + filters: fFilters, + facets: [], + hitsPerPage: props.hitsPerPage ?? 12, + page: pageIndex - startingPage, + clickAnalytics: true, + }, + }, + { + indexName, + query: props.term ?? url.searchParams.get("q") ?? + url.searchParams.get("query") ?? "", + params: { + facetingAfterDistinct: true, + facets: (props.facets?.length || 0) > 0 + ? props.facets?.map((f) => f.name) + : ["*"], + hitsPerPage: 0, + sortFacetValuesBy: props.sortFacetValuesBy, + }, + }, + ]); + + const [ + { hits, page = 0, nbPages = 1, queryID, nbHits = 12, hitsPerPage = 12 }, + { facets }, + ] = results as SearchResponse[]; + + const products = await resolveProducts( + hits.map(({ _highlightResult, ...p }) => + replaceHighlight(p, props.highlight ? _highlightResult : {}) + ), + client, + { url, queryID, indexName }, + ); + const pageInfo = getPageInfo( + page, + nbPages, + nbHits, + hitsPerPage, + url, + startingPage, + ); + const filters = transformFacets(facets ?? {}, { + order: props.facets ?? [], + facetFilters, + url, + }); + + return { + "@type": "ProductListingPage", + // TODO: Find out what's the right breadcrumb on algolia + breadcrumb: { + "@type": "BreadcrumbList", + itemListElement: [], + numberOfItems: 0, + }, + filters, + products, + pageInfo, + sortOptions: [ + { + value: "relevance", + label: "Relevance", + }, + { + value: "price_asc", + label: "Price - Lower to Higher", + }, + { + value: "price_desc", + label: "Price - Higher to Lower", + }, + ], + }; +}; + +export default loader; diff --git a/algolia/loaders/product/suggestions.ts b/algolia/loaders/product/suggestions.ts new file mode 100644 index 000000000..5bf3ce274 --- /dev/null +++ b/algolia/loaders/product/suggestions.ts @@ -0,0 +1,106 @@ +import type { SearchResponse } from "npm:@algolia/client-search"; +import { Suggestion } from "../../../commerce/types.ts"; +import type { AppContext } from "../../mod.ts"; +import { replaceHighlight } from "../../utils/highlight.ts"; +import { + type IndexedProduct, + Indices, + resolveProducts, +} from "../../utils/product.ts"; + +interface Props { + query?: string; + + /** @description number of suggested terms/products to return */ + count?: number; + + /** @description Enable to highlight matched terms */ + highlight?: boolean; + + /** @description Hide Unavailable Items */ + hideUnavailable?: boolean; +} + +interface IndexedSuggestion { + nb_words: number; + popularity: number; + products: { + exact_nb_hits: number; + facets: { + exact_matches: Record; + analytics: Record; + }; + }; + query: string; +} + +const toFacets = ( + facets: IndexedSuggestion["products"]["facets"]["exact_matches"], +) => + Object.entries(facets).map(([key, values]) => ({ + values: values.map((v) => v.value), + key, + })); + +const productsIndex = "products" satisfies Indices; + +/** + * @title Algolia Integration + */ +const loader = async ( + { query, count, highlight, hideUnavailable }: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { client } = ctx; + + const { results } = await client.search([ + { + indexName: "products_query_suggestions" satisfies Indices, + params: { hitsPerPage: count ?? 0 }, + query, + }, + { + indexName: productsIndex, + params: { + hitsPerPage: count ?? 0, + filters: hideUnavailable ? `available:true` : "", + facets: [], + clickAnalytics: true, + }, + query, + }, + ]); + + const [ + { hits: suggestions }, + { hits: indexedProducts, queryID }, + ] = results as [ + SearchResponse, + SearchResponse, + ]; + + const products = await resolveProducts( + indexedProducts.map(({ _highlightResult, ...p }) => + replaceHighlight(p, highlight ? _highlightResult : {}) + ), + client, + { url: req.url, queryID, indexName: productsIndex }, + ); + + const searches = suggestions.map((s) => ({ + term: s.query, + hits: s.products.exact_nb_hits, + facets: [ + ...toFacets(s.products.facets.exact_matches), + ...toFacets(s.products.facets.analytics), + ].filter(Boolean), + })); + + return { + searches: searches, + products, + }; +}; + +export default loader; diff --git a/algolia/logo.png b/algolia/logo.png new file mode 100644 index 000000000..25734ef10 Binary files /dev/null and b/algolia/logo.png differ diff --git a/algolia/manifest.gen.ts b/algolia/manifest.gen.ts new file mode 100644 index 000000000..10575358f --- /dev/null +++ b/algolia/manifest.gen.ts @@ -0,0 +1,39 @@ +// DO NOT EDIT. This file is generated by deco. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $$$$$$$$$0 from "./actions/index/product.ts"; +import * as $$$$$$$$$1 from "./actions/index/wait.ts"; +import * as $$$$$$$$$2 from "./actions/setup.ts"; +import * as $$$0 from "./loaders/client.ts"; +import * as $$$1 from "./loaders/product/list.ts"; +import * as $$$2 from "./loaders/product/listingPage.ts"; +import * as $$$3 from "./loaders/product/suggestions.ts"; +import * as $$$$$$0 from "./sections/Analytics/Algolia.tsx"; +import * as $$$$$$$$$$0 from "./workflows/index/product.ts"; + +const manifest = { + "loaders": { + "algolia/loaders/client.ts": $$$0, + "algolia/loaders/product/list.ts": $$$1, + "algolia/loaders/product/listingPage.ts": $$$2, + "algolia/loaders/product/suggestions.ts": $$$3, + }, + "sections": { + "algolia/sections/Analytics/Algolia.tsx": $$$$$$0, + }, + "actions": { + "algolia/actions/index/product.ts": $$$$$$$$$0, + "algolia/actions/index/wait.ts": $$$$$$$$$1, + "algolia/actions/setup.ts": $$$$$$$$$2, + }, + "workflows": { + "algolia/workflows/index/product.ts": $$$$$$$$$$0, + }, + "name": "algolia", + "baseUrl": import.meta.url, +}; + +export type Manifest = typeof manifest; + +export default manifest; diff --git a/algolia/mod.ts b/algolia/mod.ts new file mode 100644 index 000000000..e607bcef2 --- /dev/null +++ b/algolia/mod.ts @@ -0,0 +1,88 @@ +import algolia from "https://esm.sh/algoliasearch@4.20.0"; +import { createFetchRequester } from "npm:@algolia/requester-fetch@4.20.0"; +import { Markdown } from "../decohub/components/Markdown.tsx"; +import { PreviewContainer } from "../utils/preview.tsx"; +import type { Secret } from "../website/loaders/secret.ts"; +import manifest, { Manifest } from "./manifest.gen.ts"; +import { type App, type AppContext as AC } from "@deco/deco"; +export type AppContext = AC>; +export interface State { + /** + * @title Your Algolia App ID + * @description https://dashboard.algolia.com/account/api-keys/all + */ + applicationId: string; + /** + * @title Search API Key + * @description https://dashboard.algolia.com/account/api-keys/all + * @format password + */ + searchApiKey: string; + /** + * @title Admin API Key + * @description https://dashboard.algolia.com/account/api-keys/all + * @format password + */ + adminApiKey: Secret; +} +/** + * @title Algolia + * @description Product search & discovery that increases conversions at scale. + * @category Search + * @logo https://raw.githubusercontent.com/deco-cx/apps/main/algolia/logo.png + */ +export default function App(props: State) { + const { applicationId, adminApiKey, searchApiKey } = props; + if (!adminApiKey) { + throw new Error("Missing admin API key"); + } + const stringAdminApiKey = typeof adminApiKey === "string" + ? adminApiKey + : adminApiKey?.get?.() ?? ""; + const client = algolia(applicationId, stringAdminApiKey, { + requester: createFetchRequester(), // Fetch makes it perform mutch better + }); + const state = { client, applicationId, searchApiKey }; + const app: App = { + manifest: { + ...manifest, + actions: { + ...manifest.actions, + "algolia/actions/setup.ts": { + ...manifest.actions["algolia/actions/setup.ts"], + default: (p, req, ctx) => + manifest.actions["algolia/actions/setup.ts"].default( + { applicationId, adminApiKey: stringAdminApiKey, ...p }, + req, + ctx, + ), + }, + }, + }, + state, + }; + return app; +} +export const preview = async () => { + const markdownContent = await Markdown( + new URL("./README.md", import.meta.url).href, + ); + return { + Component: PreviewContainer, + props: { + name: "Algolia", + owner: "deco.cx", + description: + "Product search & discovery that increases conversions at scale.", + logo: + "https://raw.githubusercontent.com/deco-cx/apps/main/algolia/logo.png", + images: [], + tabs: [ + { + title: "About", + content: markdownContent(), + }, + ], + }, + }; +}; diff --git a/algolia/sections/Analytics/Algolia.tsx b/algolia/sections/Analytics/Algolia.tsx new file mode 100644 index 000000000..d752c3038 --- /dev/null +++ b/algolia/sections/Analytics/Algolia.tsx @@ -0,0 +1,179 @@ +import insights from "npm:search-insights@2.9.0"; +import { + AddToCartEvent, + SelectItemEvent, + ViewItemEvent, + ViewItemListEvent, +} from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { type SectionProps } from "@deco/deco"; +import { useScriptAsDataURI } from "@deco/deco/hooks"; +declare global { + interface Window { + aa: typeof insights.default; + } +} +const setupAndListen = (appId: string, apiKey: string, version: string) => { + function setupScriptTag() { + globalThis.window.AlgoliaAnalyticsObject = "aa"; + globalThis.window.aa = globalThis.window.aa || + function () { + // @ts-expect-error monkey patch before initialization + (globalThis.window.aa.queue = globalThis.window.aa.queue || []).push( + arguments, + ); + }; + globalThis.window.aa.version = version; + const script = document.createElement("script"); + script.setAttribute("async", ""); + script.setAttribute( + "src", + `https://cdn.jsdelivr.net/npm/search-insights@${version}/dist/search-insights.min.js`, + ); + document.head.appendChild(script); + } + function createUserToken() { + if ( + typeof crypto !== "undefined" && + typeof crypto.randomUUID === "function" + ) { + return crypto.randomUUID(); + } + return (Math.random() * 1e9).toFixed(); + } + function setupSession() { + globalThis.window.aa("init", { appId, apiKey }); + const userToken = localStorage.getItem("ALGOLIA_USER_TOKEN") || + createUserToken(); + localStorage.setItem("ALGOLIA_USER_TOKEN", userToken); + globalThis.window.aa("setUserToken", userToken); + } + function setupEventListeners() { + function attributesFromURL(href: string) { + const url = new URL(href); + const queryID = url.searchParams.get("algoliaQueryID"); + const indexName = url.searchParams.get("algoliaIndex"); + // Not comming from an algolia search page + if (!queryID || !indexName) { + return null; + } + return { queryID, indexName }; + } + // deno-lint-ignore no-explicit-any + function isSelectItemEvent(event: any): event is SelectItemEvent { + return event.name === "select_item"; + } + // deno-lint-ignore no-explicit-any + function isAddToCartEvent(event: any): event is AddToCartEvent { + return event.name === "add_to_cart"; + } + function isViewItem( + // deno-lint-ignore no-explicit-any + event: any, + ): event is ViewItemEvent | ViewItemListEvent { + return event.name === "view_item" || event.name === "view_item_list"; + } + type WithID = T & { + item_id: string; + }; + const hasItemId = (item: T): item is WithID => + // deno-lint-ignore no-explicit-any + typeof (item as any).item_id === "string"; + const PRODUCTS = "products"; + const MAX_BATCH_SIZE = 20; + globalThis.window.DECO.events.subscribe((event) => { + if (!event) { + return; + } + const eventName = event.name; + if (isSelectItemEvent(event)) { + const [item] = event.params.items; + if ( + !item || + !hasItemId(item) || + typeof item.index !== "number" || + typeof item.item_url !== "string" + ) { + return console.warn( + "Failed sending event to Algolia. Missing index, item_id or item_url", + JSON.stringify(event, null, 2), + ); + } + const attr = attributesFromURL(item.item_url); + if (attr) { + globalThis.window.aa("clickedObjectIDsAfterSearch", { + eventName, + index: attr.indexName, + queryID: attr.queryID, + objectIDs: [item.item_id], + positions: [item.index + 1], + }); + } else { + globalThis.window.aa("clickedObjectIDs", { + eventName, + index: PRODUCTS, + objectIDs: [item.item_id], + }); + } + } + if (isAddToCartEvent(event)) { + const [item] = event.params.items; + const attr = attributesFromURL(globalThis.window.location.href) || + attributesFromURL(item.item_url || ""); + const objectIDs = event.params.items + .filter(hasItemId) + .map((i) => i.item_id); + if (attr) { + globalThis.window.aa("convertedObjectIDsAfterSearch", { + eventName, + objectIDs, + index: attr.indexName, + queryID: attr.queryID, + }); + } else { + globalThis.window.aa("convertedObjectIDs", { + eventName, + index: PRODUCTS, + objectIDs, + }); + } + } + if (isViewItem(event)) { + const objectIDs = event.params.items + .filter(hasItemId) + .map((i) => i.item_id); + for (let it = 0; it < objectIDs.length; it += MAX_BATCH_SIZE) { + globalThis.window.aa("viewedObjectIDs", { + eventName, + index: PRODUCTS, + objectIDs: objectIDs.slice(it, (it + 1) * MAX_BATCH_SIZE), + }); + } + } + }); + } + setupScriptTag(); + setupSession(); + setupEventListeners(); +}; +function Analytics( + { applicationId, searchApiKey }: SectionProps, +) { + return ( + `; + const flagsScript = ``; + return dnsPrefetchLink + preconnectLink + plausibleScript + flagsScript; + }; + return ({ src: transformReq }); +}; +export default loader; diff --git a/analytics/logo.png b/analytics/logo.png new file mode 100644 index 000000000..a617992e1 Binary files /dev/null and b/analytics/logo.png differ diff --git a/analytics/manifest.gen.ts b/analytics/manifest.gen.ts new file mode 100644 index 000000000..779ee42c5 --- /dev/null +++ b/analytics/manifest.gen.ts @@ -0,0 +1,21 @@ +// DO NOT EDIT. This file is generated by deco. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $$$0 from "./loaders/DecoAnalyticsScript.ts"; +import * as $$$$$$0 from "./sections/Analytics/DecoAnalytics.tsx"; + +const manifest = { + "loaders": { + "analytics/loaders/DecoAnalyticsScript.ts": $$$0, + }, + "sections": { + "analytics/sections/Analytics/DecoAnalytics.tsx": $$$$$$0, + }, + "name": "analytics", + "baseUrl": import.meta.url, +}; + +export type Manifest = typeof manifest; + +export default manifest; diff --git a/analytics/mod.ts b/analytics/mod.ts new file mode 100644 index 000000000..24b6df52c --- /dev/null +++ b/analytics/mod.ts @@ -0,0 +1,30 @@ +import manifest, { Manifest } from "./manifest.gen.ts"; +import { PreviewContainer } from "../utils/preview.tsx"; +import { type App, type AppContext as AC } from "@deco/deco"; +export type AppContext = AC>; +// deno-lint-ignore no-explicit-any +export type State = any; +/** + * @title Deco Analytics + * @description Measure your site traffic at a glance in a simple and modern web analytics dashboard. + * @category Analytics + * @logo https://raw.githubusercontent.com/deco-cx/apps/main/analytics/logo.png + */ +export default function App(state: State): App { + return { manifest, state }; +} +export const preview = () => { + return { + Component: PreviewContainer, + props: { + name: "Deco Analytics", + owner: "deco.cx", + description: + "Measure your site traffic at a glance in a simple and modern web analytics dashboard.", + logo: + "https://raw.githubusercontent.com/deco-cx/apps/main/analytics/logo.png", + images: [], + tabs: [], + }, + }; +}; diff --git a/analytics/scripts/plausible_scripts.ts b/analytics/scripts/plausible_scripts.ts new file mode 100644 index 000000000..c96269fab --- /dev/null +++ b/analytics/scripts/plausible_scripts.ts @@ -0,0 +1,24 @@ +// We got these scripts from plausible. Here is the docs for every possible extension: +// https://plausible.io/docs/script-extensions + +// The scripts currently have extensions '.local', '.hash' and '.exclusions'. + +// It is necessary to change how we get data-domain: +// +// ((w,d)=>{const h=w.location.hostname;return h.replace(/^www./,"")})(window,document) +// +// We can avoid setting data-domain per site using this strategy. +// Also, when copying the script from plausible, remember to re-escape \ on regex calls. +// The difference between localAndExclusionAndHashScript and exclusionAndHashScript is the parameter '.local' +// when generating the script. + +// For localhost debug only +export const localAndExclusionAndHashScript = + '!function(){"use strict";var c=window.location,o=window.document,u=o.currentScript,s=u.getAttribute("data-api")||new URL(u.src).origin+"/api/event";function p(e,t){e&&console.warn("Ignoring Event: "+e),t&&t.callback&&t.callback()}function e(e,t){try{if("true"===window.localStorage.plausible_ignore)return p("localStorage flag",t)}catch(e){}var i=u&&u.getAttribute("data-include"),n=u&&u.getAttribute("data-exclude");if("pageview"===e){i=!i||i.split(",").some(a),n=n&&n.split(",").some(a);if(!i||n)return p("exclusion rule",t)}function a(e){var t=c.pathname;return(t+=c.hash).match(new RegExp("^"+e.trim().replace(/\\*\\*/g,".*").replace(/([^\\.])\\*/g,"$1[^\\\\s/]*")+"/?$"))}var i={},n=(i.n=e,i.u=c.href,i.d=((w,d)=>{const h=w.location.hostname;return h.replace(/^www./,"")})(window,document),i.r=o.referrer||null,t&&t.meta&&(i.m=JSON.stringify(t.meta)),t&&t.props&&(i.p=t.props),u.getAttributeNames().filter(function(e){return"event-"===e.substring(0,6)})),r=i.p||{},l=(n.forEach(function(e){var t=e.replace("event-",""),e=u.getAttribute(e);r[t]=r[t]||e}),i.p=r,i.h=1,new XMLHttpRequest);l.open("POST",s,!0),l.setRequestHeader("Content-Type","text/plain"),l.send(JSON.stringify(i)),l.onreadystatechange=function(){4===l.readyState&&t&&t.callback&&t.callback()}}var t=window.plausible&&window.plausible.q||[];window.plausible=e;for(var i,n=0;n{const h=w.location.hostname;return h.replace(/^www./,"")})(window,document),n.r=c.referrer||null,t&&t.meta&&(n.m=JSON.stringify(t.meta)),t&&t.props&&(n.p=t.props),s.getAttributeNames().filter(function(e){return"event-"===e.substring(0,6)})),r=n.p||{},o=(i.forEach(function(e){var t=e.replace("event-",""),e=s.getAttribute(e);r[t]=r[t]||e}),n.p=r,n.h=1,new XMLHttpRequest);o.open("POST",u,!0),o.setRequestHeader("Content-Type","text/plain"),o.send(JSON.stringify(n)),o.onreadystatechange=function(){4===o.readyState&&t&&t.callback&&t.callback()}}var t=window.plausible&&window.plausible.q||[];window.plausible=e;for(var n,i=0;i; +} + +export const Preview = () => ( + + + ); +} diff --git a/deco.ts b/deco.ts index 42fd76176..48947b7b6 100644 --- a/deco.ts +++ b/deco.ts @@ -1,11 +1,53 @@ -const app = (name: string) => ({ dir: `./${name.replace("apps/", "")}`, name }); +const app = (name: string) => ({ dir: name, name }); + +const compatibilityApps = [{ + dir: "./compat/$live", + name: "$live", +}, { + dir: "./compat/std", + name: "deco-sites/std", +}]; const config = { apps: [ - app("apps/vtex"), - app("apps/vnda"), - app("apps/website"), - app("apps/workflows"), + app("posthog"), + app("decopilot-app"), + app("smarthint"), + app("ra-trustvox"), + app("anthropic"), + app("resend"), + app("emailjs"), + app("konfidency"), + app("mailchimp"), + app("ai-assistants"), + app("files"), + app("openai"), + app("brand-assistant"), + app("implementation"), + app("weather"), + app("blog"), + app("analytics"), + app("sourei"), + app("typesense"), + app("algolia"), + app("vtex"), + app("vnda"), + app("wake"), + app("wap"), + app("linx"), + app("linx-impulse"), + app("shopify"), + app("nuvemshop"), + app("website"), + app("commerce"), + app("workflows"), + app("verified-reviews"), + app("power-reviews"), + app("crux"), + app("decohub"), + app("htmx"), + app("sap"), + ...compatibilityApps, ], }; diff --git a/decohub/README.md b/decohub/README.md new file mode 100644 index 000000000..229dc7ab0 --- /dev/null +++ b/decohub/README.md @@ -0,0 +1,17 @@ +

+

+ + Deco + +

+

+ +

+ + Deco Hub + +

+

+ Unlock apps and integrations on deco.cx +

+ diff --git a/decohub/apps/ai-assistants.ts b/decohub/apps/ai-assistants.ts new file mode 100644 index 000000000..02d3e519d --- /dev/null +++ b/decohub/apps/ai-assistants.ts @@ -0,0 +1 @@ +export { default } from "../../ai-assistants/mod.ts"; diff --git a/decohub/apps/algolia.ts b/decohub/apps/algolia.ts new file mode 100644 index 000000000..780f10fc6 --- /dev/null +++ b/decohub/apps/algolia.ts @@ -0,0 +1 @@ +export { default, preview } from "../../algolia/mod.ts"; diff --git a/decohub/apps/analytics.ts b/decohub/apps/analytics.ts new file mode 100644 index 000000000..5cbe95e74 --- /dev/null +++ b/decohub/apps/analytics.ts @@ -0,0 +1 @@ +export { default } from "../../analytics/mod.ts"; diff --git a/decohub/apps/anthropic.ts b/decohub/apps/anthropic.ts new file mode 100644 index 000000000..125be545f --- /dev/null +++ b/decohub/apps/anthropic.ts @@ -0,0 +1 @@ +export { default } from "../../anthropic/mod.ts"; diff --git a/decohub/apps/blog.ts b/decohub/apps/blog.ts new file mode 100644 index 000000000..94a7db900 --- /dev/null +++ b/decohub/apps/blog.ts @@ -0,0 +1 @@ +export { default } from "../../blog/mod.ts"; diff --git a/decohub/apps/brand-assistant.ts b/decohub/apps/brand-assistant.ts new file mode 100644 index 000000000..0514804b5 --- /dev/null +++ b/decohub/apps/brand-assistant.ts @@ -0,0 +1 @@ +export { default } from "../../brand-assistant/mod.ts"; diff --git a/decohub/apps/crux.ts b/decohub/apps/crux.ts new file mode 100644 index 000000000..7d4d5531f --- /dev/null +++ b/decohub/apps/crux.ts @@ -0,0 +1 @@ +export { default, Preview } from "../../crux/mod.ts"; diff --git a/decohub/apps/emailjs.ts b/decohub/apps/emailjs.ts new file mode 100644 index 000000000..e43c37d38 --- /dev/null +++ b/decohub/apps/emailjs.ts @@ -0,0 +1 @@ +export { default, preview } from "../../emailjs/mod.ts"; diff --git a/decohub/apps/htmx.ts b/decohub/apps/htmx.ts new file mode 100644 index 000000000..b0f57112b --- /dev/null +++ b/decohub/apps/htmx.ts @@ -0,0 +1 @@ +export { default, preview } from "../../htmx/mod.ts"; diff --git a/decohub/apps/implementation.ts b/decohub/apps/implementation.ts new file mode 100644 index 000000000..ee33cec16 --- /dev/null +++ b/decohub/apps/implementation.ts @@ -0,0 +1 @@ +export { default, preview } from "../../implementation/mod.ts"; diff --git a/decohub/apps/konfidency.ts b/decohub/apps/konfidency.ts new file mode 100644 index 000000000..eff6eb05a --- /dev/null +++ b/decohub/apps/konfidency.ts @@ -0,0 +1 @@ +export { default, preview } from "../../konfidency/mod.ts"; diff --git a/decohub/apps/linx-impulse.ts b/decohub/apps/linx-impulse.ts new file mode 100644 index 000000000..9b70b2888 --- /dev/null +++ b/decohub/apps/linx-impulse.ts @@ -0,0 +1 @@ +export { default } from "../../linx-impulse/mod.ts"; diff --git a/decohub/apps/linx.ts b/decohub/apps/linx.ts new file mode 100644 index 000000000..4f983ceab --- /dev/null +++ b/decohub/apps/linx.ts @@ -0,0 +1 @@ +export { default, preview } from "../../linx/mod.ts"; diff --git a/decohub/apps/mailchimp.ts b/decohub/apps/mailchimp.ts new file mode 100644 index 000000000..a470fd9a3 --- /dev/null +++ b/decohub/apps/mailchimp.ts @@ -0,0 +1 @@ +export { default, preview } from "../../mailchimp/mod.ts"; diff --git a/decohub/apps/nuvemshop.ts b/decohub/apps/nuvemshop.ts new file mode 100644 index 000000000..ed6f5b479 --- /dev/null +++ b/decohub/apps/nuvemshop.ts @@ -0,0 +1 @@ +export { default, preview } from "../../nuvemshop/mod.ts"; diff --git a/decohub/apps/posthog.ts b/decohub/apps/posthog.ts new file mode 100644 index 000000000..c4cf74803 --- /dev/null +++ b/decohub/apps/posthog.ts @@ -0,0 +1 @@ +export { default, preview } from "../../posthog/mod.ts"; diff --git a/decohub/apps/power-reviews.ts b/decohub/apps/power-reviews.ts new file mode 100644 index 000000000..c7e62728c --- /dev/null +++ b/decohub/apps/power-reviews.ts @@ -0,0 +1 @@ +export { default } from "../../power-reviews/mod.ts"; diff --git a/decohub/apps/ra-trustvox.ts b/decohub/apps/ra-trustvox.ts new file mode 100644 index 000000000..488ad8448 --- /dev/null +++ b/decohub/apps/ra-trustvox.ts @@ -0,0 +1 @@ +export { default } from "../../ra-trustvox/mod.ts"; diff --git a/decohub/apps/resend.ts b/decohub/apps/resend.ts new file mode 100644 index 000000000..b85fea41e --- /dev/null +++ b/decohub/apps/resend.ts @@ -0,0 +1 @@ +export { default, preview } from "../../resend/mod.ts"; diff --git a/decohub/apps/shopify.ts b/decohub/apps/shopify.ts new file mode 100644 index 000000000..963ab1d19 --- /dev/null +++ b/decohub/apps/shopify.ts @@ -0,0 +1 @@ +export { default, preview } from "../../shopify/mod.ts"; diff --git a/decohub/apps/smarthint.ts b/decohub/apps/smarthint.ts new file mode 100644 index 000000000..0489d6d98 --- /dev/null +++ b/decohub/apps/smarthint.ts @@ -0,0 +1 @@ +export { default, preview } from "../../smarthint/mod.ts"; diff --git a/decohub/apps/sourei.ts b/decohub/apps/sourei.ts new file mode 100644 index 000000000..2186ea074 --- /dev/null +++ b/decohub/apps/sourei.ts @@ -0,0 +1 @@ +export { default, preview } from "../../sourei/mod.ts"; diff --git a/decohub/apps/typesense.ts b/decohub/apps/typesense.ts new file mode 100644 index 000000000..738685d25 --- /dev/null +++ b/decohub/apps/typesense.ts @@ -0,0 +1 @@ +export { default, preview } from "../../typesense/mod.ts"; diff --git a/decohub/apps/verified-reviews.ts b/decohub/apps/verified-reviews.ts new file mode 100644 index 000000000..e2e9da3bf --- /dev/null +++ b/decohub/apps/verified-reviews.ts @@ -0,0 +1 @@ +export { default } from "../../verified-reviews/mod.ts"; diff --git a/decohub/apps/vnda.ts b/decohub/apps/vnda.ts new file mode 100644 index 000000000..55f90c014 --- /dev/null +++ b/decohub/apps/vnda.ts @@ -0,0 +1 @@ +export { default, preview } from "../../vnda/mod.ts"; diff --git a/decohub/apps/vtex.ts b/decohub/apps/vtex.ts new file mode 100644 index 000000000..66a661a55 --- /dev/null +++ b/decohub/apps/vtex.ts @@ -0,0 +1,16 @@ +export { default } from "../../vtex/mod.ts"; +import { PreviewVtex } from "../../vtex/preview/Preview.tsx"; +import { Markdown } from "../components/Markdown.tsx"; +import { type AppRuntime } from "@deco/deco"; +export const preview = async (props: AppRuntime) => { + const markdownContent = await Markdown( + new URL("../../vtex/README.md", import.meta.url).href, + ); + return { + Component: PreviewVtex, + props: { + ...props, + markdownContent, + }, + }; +}; diff --git a/decohub/apps/wake.ts b/decohub/apps/wake.ts new file mode 100644 index 000000000..47cabd78c --- /dev/null +++ b/decohub/apps/wake.ts @@ -0,0 +1 @@ +export { default, preview } from "../../wake/mod.ts"; diff --git a/decohub/apps/wap.ts b/decohub/apps/wap.ts new file mode 100644 index 000000000..fd842f473 --- /dev/null +++ b/decohub/apps/wap.ts @@ -0,0 +1,3 @@ +import { Markdown as _Markdown } from "../components/Markdown.tsx"; + +export { default } from "../../wap/mod.ts"; diff --git a/decohub/apps/weather.ts b/decohub/apps/weather.ts new file mode 100644 index 000000000..2f174decb --- /dev/null +++ b/decohub/apps/weather.ts @@ -0,0 +1 @@ +export { default } from "../../weather/mod.ts"; diff --git a/decohub/apps/workflows.ts b/decohub/apps/workflows.ts new file mode 100644 index 000000000..ee27ff726 --- /dev/null +++ b/decohub/apps/workflows.ts @@ -0,0 +1 @@ +export { default } from "../../workflows/mod.ts"; diff --git a/decohub/components/Markdown.tsx b/decohub/components/Markdown.tsx new file mode 100644 index 000000000..1f590df3d --- /dev/null +++ b/decohub/components/Markdown.tsx @@ -0,0 +1,53 @@ +interface DenoGfm { + render: ( + content: string, + options: { + allowIframes: boolean; + allowMath: boolean; + disableHtmlSanitization: boolean; + }, + ) => string; + KATEX_CSS: string; + CSS: string; +} + +let denoGfm: Promise | null = null; +const importDenoGfm = async (): Promise => { + const gfmVersion = `0.9.0`; + try { + const gfm = await import(`jsr:@deno/gfm@${gfmVersion}`); + return gfm; + } catch (err) { + return { + render: () => `could not dynamic load @deno/gfm@${gfmVersion} ${err}`, + KATEX_CSS: "", + CSS: "", + }; + } +}; +export const Markdown = async (path: string) => { + denoGfm ??= importDenoGfm(); + const { CSS, KATEX_CSS, render } = await denoGfm; + const content = await fetch(path) + .then((res) => res.text()) + .catch(() => `Could not fetch README.md for ${path}`); + + return () => { + return ( + <> + + + ); +} diff --git a/power-reviews/sections/WriteReviewForm.tsx b/power-reviews/sections/WriteReviewForm.tsx new file mode 100644 index 000000000..11923a935 --- /dev/null +++ b/power-reviews/sections/WriteReviewForm.tsx @@ -0,0 +1,53 @@ +export interface Props { + /** + * @title App Key + * @ignore + */ + appKey?: string; + /** + * @title Locale + * @ignore + */ + locale?: string; + /** + * @title Merchant Id + * @ignore + */ + merchantId?: string; + /** + * @title Merchant Group + * @ignore + */ + merchantGroup?: string; +} + +export default function WriteReviewForm(state: Props) { + return ( +
+
+ + +
+ ); +} diff --git a/power-reviews/utils/client.ts b/power-reviews/utils/client.ts new file mode 100644 index 000000000..cc3617314 --- /dev/null +++ b/power-reviews/utils/client.ts @@ -0,0 +1,39 @@ +import { + ContextInformation, + PageReview, + ReviewForm, + ReviewFormField, +} from "./types.ts"; + +export interface PowerReviews { + "GET /m/:merchantId/l/:locale/product/:pageId/reviews": { + response: PageReview; + searchParams: { + _noconfig: string; + image_only: boolean; + sort?: string; + filters?: string; + "paging.from": number; + "paging.size": number; + }; + }; + + "GET /war/writereview": { + response: ReviewForm; + searchParams: { + "merchant_id": string; + "page_id": string; + }; + }; + + "POST /war/writereview": { + searchParams: { + "merchant_id": string; + "page_id": string; + }; + body: { + fields: ReviewFormField[]; + context_information: ContextInformation; + }; + }; +} diff --git a/power-reviews/utils/tranform.ts b/power-reviews/utils/tranform.ts new file mode 100644 index 000000000..be4a0fba3 --- /dev/null +++ b/power-reviews/utils/tranform.ts @@ -0,0 +1,77 @@ +import { Product } from "../../commerce/types.ts"; +import { Review, Rollup } from "./types.ts"; + +export const toReview = (review: Review) => { + const date = new Date(review.details.created_date); + const formatedDate = date.getFullYear() + "-" + + (date.getMonth() + 1) + "-" + (date.getDate()); + const pros = review.details.properties.find((prop) => prop.key == "pros") + ?.value; + const cons = review.details.properties.find((prop) => prop.key == "cons") + ?.value; + return { + "@type": "Review" as const, + id: review.internal_review_id.toString(), + author: [ + { + "@type": "Author" as const, + name: `${review.details.nickname}`, + verifiedBuyer: review.badges.is_verified_buyer, + location: review.details.location, + }, + ], + datePublished: formatedDate, + itemReviewed: review.details.product_name, + negativeNotes: cons, + positiveNotes: pros, + reviewHeadline: review.details.headline, + reviewBody: review.details.comments, + reviewRating: { + "@type": "AggregateRating" as const, + ratingValue: review.metrics.rating, + }, + tags: review.details.properties.map((props) => ({ + label: props.label, + value: props.value, + })), + brand: { + name: review.details.brand_name, + logo: review.details.brand_logo_uri, + url: review.details.brand_base_url, + }, + media: review.media?.map((media) => ({ + type: media.type, + url: media.uri, + alt: media.id, + likes: media.helpful_votes, + unlikes: media.not_helpful_votes, + })), + }; +}; + +export const toAggregateRating = (rollup: Rollup) => { + if (!rollup) { + return { + "@type": "AggregateRating" as const, + ratingValue: 0, + ratingCount: 0, + }; + } + return { + "@type": "AggregateRating" as const, + ratingValue: rollup?.average_rating || 0, + ratingCount: rollup?.review_count || 0, + }; +}; + +export const toPowerReviewId = (prop: string | undefined, product: Product) => { + if (prop == "sku") { + return product.sku; + } + + if (prop == "model") { + return product.isVariantOf?.model; + } + + return product.productID; +}; diff --git a/power-reviews/utils/types.ts b/power-reviews/utils/types.ts new file mode 100644 index 000000000..dfedcd651 --- /dev/null +++ b/power-reviews/utils/types.ts @@ -0,0 +1,214 @@ +export interface PageReview { + name: string; + paging: Paging; + results: Result[]; +} + +export interface Paging { + total_results: number; + pages_total: number; + page_size: number; + current_page_number: number; + next_page_url: string; +} + +export interface Result { + page_id: string; + rollup: Rollup; + reviews: Review[]; +} + +export interface Review { + ugc_id: number; + legacy_id: number; + review_id: number; + internal_review_id: number; + details: Details; + badges: Badges; + media: Media[]; + metrics: Metrics; +} + +export interface Badges { + is_staff_reviewer: boolean; + is_verified_buyer: boolean; + is_verified_reviewer: boolean; +} + +export interface Details { + comments: string; + headline: string; + nickname: string; + properties: Property[]; + product_name: string; + location: string; + created_date: number; + updated_date: number; + product_page_id: string; + brand_base_url: string; + brand_logo_uri: string; + brand_name: string; +} + +export interface Property { + key: string; + label: string; + type: string; + value: string[]; +} + +export interface Metrics { + helpful_votes: number; + not_helpful_votes: number; + rating: number; + helpful_score: number; +} + +export interface Rollup { + properties: Property[]; + rating_histogram: number[]; + recommended_ratio: number; + average_rating: number; + review_count: number; + media: Media[]; + name: string; + native_review_count: number; + syndicated_review_count: number; +} + +export interface Media { + id: string; + review_id: string; + uri: string; + headline: string; + rating: string; + helpful_votes: number; + not_helpful_votes: number; + type: "image" | "video"; + caption: string; + nickname: string; + created_date: number; +} + +export interface Property { + display_type: string; + key: string; + name: string; + type: string; + values: Value[]; + display_values: string[]; +} + +export interface Value { + label: string; + count: number; +} + +export interface ReviewForm { + merchant_information: MerchantInformation; + product_information: ProductInformation; + fields: ReviewFormField[]; + context_information: ContextInformation; +} + +export interface ContextInformation { + review_session_id: string; + review_start_date: Date; + product_id: number; + product_template_id: number; + image_template_id: number; + context_hash: number; +} + +export interface ReviewFormField { + id: string; + field_type: string; + key: string; + label?: string; + required?: boolean; + input_type?: string; + answer_type?: string; + max_length?: number; + helper_text?: string; + choices?: Choice[]; + prompt?: string; + composite_type?: string; + count?: number; + fields?: FieldField[]; + value?: string | number; +} + +export interface Choice { + id: string; + value: string; +} + +export interface FieldField { + id: string; + field_type: string; + key: string; + label: string; + required: boolean; + helper_text: string; + input_type: string; + answer_type: string; + choices?: Choice[]; +} + +export interface MerchantInformation { + name: string; + configuration: Configuration; + return_url: string; +} + +export interface Configuration { + is_live: boolean; + collect_email: string; + allow_facebook_connect: boolean; + allow_post_to_twitter: boolean; + enable_share_to_amazon: boolean; + enable_share_to_bestbuy: boolean; + enable_share_to_coolblue: boolean; + services_must_agree_with_terms: boolean; + video_collection_type: string; + star_styles: string; + enable_front_end_iovation_validation: boolean; + enable_enhanced_content_security: boolean; + allow_post_to_pinterest_war: boolean; + enable_facebook_integration: boolean; + enable_instagram_integration: boolean; + war_minimum_required_characters: number; + war_minimum_recommended_characters: number; + ryp_sort_order: string; + disable_cd4_heading_structures: boolean; + share_to_retailer_display: string; + social_measurement_data: string; + answerbox_enable_pre_question: boolean; + answerbox_nickname_is_required: boolean; + answerbox_required_email: boolean; + enable_rating_only_collection: boolean; +} + +export interface ProductInformation { + name: string; + full_product_image_urls: FullProductImageUrls; + full_product_url: string; + page_id: string; + locale: string; + product_lookup_location: string; +} + +export interface FullProductImageUrls { + "100": string; +} + +export interface WriteReviewResponse { + fields: ReviewFormField; + context_information: ContextInformation; + status_code: string; + progressive_info?: { + is_progressive_type: boolean; + is_last_stap: false; + current_step: number; + }; +} diff --git a/ra-trustvox/components/TrustvoxCertificateFixed.tsx b/ra-trustvox/components/TrustvoxCertificateFixed.tsx new file mode 100644 index 000000000..31cc3d608 --- /dev/null +++ b/ra-trustvox/components/TrustvoxCertificateFixed.tsx @@ -0,0 +1,5 @@ +export default function TrustvoxCertificateFixed() { + return ( +
+ ); +} diff --git a/ra-trustvox/components/TrustvoxProductDetailsRate.tsx b/ra-trustvox/components/TrustvoxProductDetailsRate.tsx new file mode 100644 index 000000000..3ac819313 --- /dev/null +++ b/ra-trustvox/components/TrustvoxProductDetailsRate.tsx @@ -0,0 +1,50 @@ +interface Props { + /** + * @ignore + */ + productId: string; +} + +export default function TrustvoxProductDetailsRate({ productId }: Props) { + return ( + <> + + +
+ Clique e veja! + + + ); +} diff --git a/ra-trustvox/components/TrustvoxShelfRate.tsx b/ra-trustvox/components/TrustvoxShelfRate.tsx new file mode 100644 index 000000000..3db21cc6a --- /dev/null +++ b/ra-trustvox/components/TrustvoxShelfRate.tsx @@ -0,0 +1,31 @@ +interface Props { + /** + * @ignore + */ + productId: string; +} + +export default function TrustvoxShelfRate({ productId }: Props) { + return ( + <> + +
+ + ); +} diff --git a/ra-trustvox/manifest.gen.ts b/ra-trustvox/manifest.gen.ts new file mode 100644 index 000000000..860c4dc36 --- /dev/null +++ b/ra-trustvox/manifest.gen.ts @@ -0,0 +1,23 @@ +// DO NOT EDIT. This file is generated by deco. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $$$$$$0 from "./sections/TrustvoxCertificate.tsx"; +import * as $$$$$$1 from "./sections/TrustvoxProductReviews.tsx"; +import * as $$$$$$2 from "./sections/TrustvoxRateConfig.tsx"; +import * as $$$$$$3 from "./sections/TrustvoxStoreReviewsCarousel.tsx"; + +const manifest = { + "sections": { + "ra-trustvox/sections/TrustvoxCertificate.tsx": $$$$$$0, + "ra-trustvox/sections/TrustvoxProductReviews.tsx": $$$$$$1, + "ra-trustvox/sections/TrustvoxRateConfig.tsx": $$$$$$2, + "ra-trustvox/sections/TrustvoxStoreReviewsCarousel.tsx": $$$$$$3, + }, + "name": "ra-trustvox", + "baseUrl": import.meta.url, +}; + +export type Manifest = typeof manifest; + +export default manifest; diff --git a/ra-trustvox/mod.ts b/ra-trustvox/mod.ts new file mode 100644 index 000000000..c0a42d82b --- /dev/null +++ b/ra-trustvox/mod.ts @@ -0,0 +1,46 @@ +import { PreviewContainer } from "../utils/preview.tsx"; +import manifest, { Manifest } from "./manifest.gen.ts"; +import { type App, type AppContext as AC } from "@deco/deco"; +export interface State { + /** + * @title Store ID + * @description Store ID available on the Trustvox dashboard. + */ + storeId: string; + /** + * @title Number of reviews in store carousel. + * @description Number of reviews that should appear in the store carousel widget. + * @default 7 + */ + numberOfReviewsInStoreCarousel?: number; + /** + * @title Enable the staging environment. + * @description When enabling the testing environment, the store id must be replaced with a store id from a Trustvox testing environment store. + * @default false + */ + enableStaging?: boolean; +} +/** + * @title RA Trustvox + * @description RA trustvox reviews. + * @category Review + * @logo https://raw.githubusercontent.com/trustvox/deco-apps/enhancement/trustvox-app/ra-trustvox/ra-trustvox.png + */ +export default function RATrustvox(state: State): App { + return { manifest, state }; +} +export type AppContext = AC>; +export const preview = () => { + return { + Component: PreviewContainer, + props: { + name: "RA Trustvox", + owner: "deco.cx", + description: "RA trustvox reviews.", + logo: + "https://raw.githubusercontent.com/trustvox/deco-apps/enhancement/trustvox-app/ra-trustvox/ra-trustvox.png", + images: [], + tabs: [], + }, + }; +}; diff --git a/ra-trustvox/ra-trustvox.png b/ra-trustvox/ra-trustvox.png new file mode 100644 index 000000000..16be38dcc Binary files /dev/null and b/ra-trustvox/ra-trustvox.png differ diff --git a/ra-trustvox/sections/TrustvoxCertificate.tsx b/ra-trustvox/sections/TrustvoxCertificate.tsx new file mode 100644 index 000000000..3f6b24a62 --- /dev/null +++ b/ra-trustvox/sections/TrustvoxCertificate.tsx @@ -0,0 +1,15 @@ +import { AppContext } from "../mod.ts"; +import { type SectionProps } from "@deco/deco"; +export default function TrustvoxCertificate( + { enableStaging = false }: SectionProps, +) { + const scriptUrl = enableStaging + ? "https://storage.googleapis.com/trustvox-certificate-widget-staging/widget.js" + : "https://certificate.trustvox.com.br/widget.js"; + return + + ); +} + +export default Slider; diff --git a/utils/cookie.ts b/utils/cookie.ts new file mode 100644 index 000000000..6a37c72d9 --- /dev/null +++ b/utils/cookie.ts @@ -0,0 +1,39 @@ +import { getCookies, getSetCookies, setCookie } from "std/http/cookie.ts"; +import { DECO_SEGMENT, type Flag } from "@deco/deco"; +import { tryOrDefault } from "@deco/deco/utils"; +export const getFlagsFromRequest = (req: Request) => { + const cookies = getCookies(req.headers); + return getFlagsFromCookies(cookies); +}; +export const getFlagsFromCookies = (cookies: Record) => { + const flags: Flag[] = []; + const segment = cookies[DECO_SEGMENT] + ? tryOrDefault( + () => JSON.parse(decodeURIComponent(atob(cookies[DECO_SEGMENT]))), + {}, + ) + : {}; + segment.active?.forEach((flag: string) => + flags.push({ name: flag, value: true }) + ); + segment.inactiveDrawn?.forEach((flag: string) => + flags.push({ name: flag, value: false }) + ); + return flags; +}; +export const proxySetCookie = ( + from: Headers, + to: Headers, + toDomain?: URL | string, +) => { + const newDomain = toDomain && new URL(toDomain); + for (const cookie of getSetCookies(from)) { + const newCookie = newDomain + ? { + ...cookie, + domain: newDomain.hostname, + } + : cookie; + setCookie(to, newCookie); + } +}; diff --git a/utils/dataURI.ts b/utils/dataURI.ts new file mode 100644 index 000000000..f9760a8ff --- /dev/null +++ b/utils/dataURI.ts @@ -0,0 +1,31 @@ +let once = true; + +// Avoid throwing DOM Exception: +// The string to be encoded contains characters outside of the Latin1 range. +const btoaSafe = (x: string) => + btoa(`decodeURIComponent(escape(${unescape(encodeURIComponent(x))}))`); + +// deno-lint-ignore no-explicit-any +export const scriptAsDataURI = any>( + fn: T, + ...params: Parameters +) => { + if (once) { + once = false; + console.warn( + `scriptAsDataURI is deprecated and will soon be removed. Use import { useScriptAsDataURI } from 'deco/hooks/useScript.ts' instead.`, + ); + } + + return dataURI( + "text/javascript", + true, + `(${fn})(${params.map((p) => JSON.stringify(p)).join(", ")})`, + ); +}; + +export const dataURI = ( + contentType: "text/javascript", + base64: boolean, + content: string, +) => `data:${contentType}${base64 ? `;base64,${btoaSafe(content)}` : content}`; diff --git a/utils/defaultErrorPage.tsx b/utils/defaultErrorPage.tsx new file mode 100644 index 000000000..64c7498ad --- /dev/null +++ b/utils/defaultErrorPage.tsx @@ -0,0 +1,185 @@ +import { Head } from "$fresh/runtime.ts"; + +type Props = { + error?: string; +}; + +const Squares = () => ( + <> + + +
+
+
+
+
+ +
+

{name}

+

by {owner}

+
+
+

+ {description} +

+
+
+ {images.length > 0 && ( +
+
+ {images.length > 1 && ( +
+ + + +
+ )} + + {images.map((image, index) => { + return ( + + + + ); + })} + + {images.length > 1 && ( +
+ + + +
+ )} +
+ + +
+ )} +
+
+ {tabs?.map((tab, idx) => { + return ( +
+ + {tab.title} + +
+ {tab.content} +
+
+ ); + })} + {(tabs?.length || 0) > 0 && ( +
+
+ )} + +
+
+ +
+ ); +} + +const ArrowSvg = () => ( + + + +); diff --git a/utils/shortHash.ts b/utils/shortHash.ts new file mode 100644 index 000000000..172e4bc36 --- /dev/null +++ b/utils/shortHash.ts @@ -0,0 +1,21 @@ +export async function hashString(input: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(input); + + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + + const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); + return hashHex.slice(0, 8); +} + +export function hashStringSync(input: string): string { + let hash = 0; + for (let i = 0; i < input.length; i++) { + const char = input.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash |= 0; // Convert to 32-bit integer + } + return hash.toString(16).slice(0, 8); +} diff --git a/utils/weakcache.ts b/utils/weakcache.ts new file mode 100644 index 000000000..59ba9e877 --- /dev/null +++ b/utils/weakcache.ts @@ -0,0 +1 @@ +export * as weakcache from "npm:weak-lru-cache@1.0.0"; diff --git a/verified-reviews/loaders/productDetailsPage.ts b/verified-reviews/loaders/productDetailsPage.ts new file mode 100644 index 000000000..917afa83a --- /dev/null +++ b/verified-reviews/loaders/productDetailsPage.ts @@ -0,0 +1,45 @@ +import { AppContext } from "../mod.ts"; +import { ProductDetailsPage } from "../../commerce/types.ts"; +import { ExtensionOf } from "../../website/loaders/extension.ts"; +import { + createClient, + getProductId, + PaginationOptions, +} from "../utils/client.ts"; +export type Props = PaginationOptions; + +/** + * @title Opiniões verificadas - Full Review for Product (Ratings and Reviews) + */ +export default function productDetailsPage( + config: Props, + _req: Request, + ctx: AppContext, +): ExtensionOf { + const client = createClient({ ...ctx }); + return async (productDetailsPage: ProductDetailsPage | null) => { + if (!productDetailsPage) { + return null; + } + + if (!client) { + return null; + } + + const productId = getProductId(productDetailsPage.product); + const fullReview = await client.fullReview({ + productId, + count: config?.count, + offset: config?.offset, + order: config?.order, + }); + + return { + ...productDetailsPage, + product: { + ...productDetailsPage.product, + ...fullReview, + }, + }; + }; +} diff --git a/verified-reviews/loaders/productList.ts b/verified-reviews/loaders/productList.ts new file mode 100644 index 000000000..b576646d8 --- /dev/null +++ b/verified-reviews/loaders/productList.ts @@ -0,0 +1,39 @@ +import { AppContext } from "../mod.ts"; +import { Product } from "../../commerce/types.ts"; +import { ExtensionOf } from "../../website/loaders/extension.ts"; +import { getRatingProduct } from "../utils/transform.ts"; +import { createClient, getProductId } from "../utils/client.ts"; + +/** + * @title Opiniões verificadas - Ratings for Products[] + */ +export default function productList( + _config: unknown, + _req: Request, + ctx: AppContext, +): ExtensionOf { + const client = createClient({ ...ctx }); + + return async (products: Product[] | null) => { + if (!products) { + return null; + } + if (!client) { + return products; + } + + const productsIds = products.map(getProductId); + const ratings = await client.ratings({ productsIds: productsIds }); + + return products.map((product) => { + const productId = getProductId(product); + return { + ...product, + aggregateRating: getRatingProduct({ + ratings, + productId: productId, + }), + }; + }); + }; +} diff --git a/verified-reviews/loaders/productListingPage.ts b/verified-reviews/loaders/productListingPage.ts new file mode 100644 index 000000000..f04d81670 --- /dev/null +++ b/verified-reviews/loaders/productListingPage.ts @@ -0,0 +1,47 @@ +import { AppContext } from "../mod.ts"; +import { ProductListingPage } from "../../commerce/types.ts"; +import { ExtensionOf } from "../../website/loaders/extension.ts"; +import { + createClient, + getProductId, + PaginationOptions, +} from "../utils/client.ts"; +import { getRatingProduct } from "../utils/transform.ts"; + +export type Props = PaginationOptions; + +/** + * @title Opiniões verificadas - Full Review for Product (Ratings and Reviews) + */ +export default function productListingPage( + _config: unknown, + _req: Request, + ctx: AppContext, +): ExtensionOf { + const client = createClient({ ...ctx }); + return async (page: ProductListingPage | null) => { + if (!page) { + return null; + } + if (!client) { + return page; + } + + const productsIds = page.products.map(getProductId); + const ratings = await client.ratings({ productsIds: productsIds }); + + return { + ...page, + products: page.products.map((product) => { + const productId = getProductId(product); + return { + ...product, + aggregateRating: getRatingProduct({ + ratings, + productId: productId, + }), + }; + }), + }; + }; +} diff --git a/verified-reviews/loaders/storeReview.ts b/verified-reviews/loaders/storeReview.ts new file mode 100644 index 000000000..c2e29534a --- /dev/null +++ b/verified-reviews/loaders/storeReview.ts @@ -0,0 +1,42 @@ +import { Review } from "../../commerce/types.ts"; +import { AppContext } from "../mod.ts"; +import { createClient } from "../utils/client.ts"; +import { toReview } from "../utils/transform.ts"; + +export type Props = { + /** + * @title Number of reviews + * @default 5 + */ + limit?: number; + /** + * @title Offset + * @default 0 + */ + offset?: number; +}; + +/** + * @title Opiniões verificadas - Full Review for Store (Ratings and Reviews) + */ +export default async function storeReview( + props: Props, + _req: Request, + ctx: AppContext, +): Promise { + const { offset = 0, limit = 5 } = props; + const client = createClient({ ...ctx }); + + if (!client) { + return null; + } + + const reviews = await client.storeReview(); + + if (!reviews) { + return null; + } + + // The API does not have a pagination, so we need to do it here + return reviews.map(toReview).slice(offset, limit); +} diff --git a/verified-reviews/logo.png b/verified-reviews/logo.png new file mode 100644 index 000000000..4f19f17da Binary files /dev/null and b/verified-reviews/logo.png differ diff --git a/verified-reviews/manifest.gen.ts b/verified-reviews/manifest.gen.ts new file mode 100644 index 000000000..b6f337c9e --- /dev/null +++ b/verified-reviews/manifest.gen.ts @@ -0,0 +1,23 @@ +// DO NOT EDIT. This file is generated by deco. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $$$0 from "./loaders/productDetailsPage.ts"; +import * as $$$1 from "./loaders/productList.ts"; +import * as $$$2 from "./loaders/productListingPage.ts"; +import * as $$$3 from "./loaders/storeReview.ts"; + +const manifest = { + "loaders": { + "verified-reviews/loaders/productDetailsPage.ts": $$$0, + "verified-reviews/loaders/productList.ts": $$$1, + "verified-reviews/loaders/productListingPage.ts": $$$2, + "verified-reviews/loaders/storeReview.ts": $$$3, + }, + "name": "verified-reviews", + "baseUrl": import.meta.url, +}; + +export type Manifest = typeof manifest; + +export default manifest; diff --git a/verified-reviews/mod.ts b/verified-reviews/mod.ts new file mode 100644 index 000000000..752b0fc78 --- /dev/null +++ b/verified-reviews/mod.ts @@ -0,0 +1,36 @@ +import type { Secret } from "../website/loaders/secret.ts"; +import manifest, { Manifest } from "./manifest.gen.ts"; +import { PreviewContainer } from "../utils/preview.tsx"; +import { type App, type AppContext as AC } from "@deco/deco"; +export interface ConfigVerifiedReviews { + idWebsite: string; + secretKey?: Secret; + plateforme?: string; +} +/** + * @title Verified Reviews + * @description A specialized solution in the collection of customer reviews + * @category Review + * @logo https://raw.githubusercontent.com/deco-cx/apps/main/verified-reviews/logo.png + */ +export default function App( + state: ConfigVerifiedReviews, +): App { + return { manifest, state }; +} +export type AppContext = AC>; +export const preview = () => { + return { + Component: PreviewContainer, + props: { + name: "Verified Reviews", + owner: "deco.cx", + description: + "A specialized solution in the collection of customer reviews.", + logo: + "https://raw.githubusercontent.com/deco-cx/apps/main/verified-reviews/logo.png", + images: [], + tabs: [], + }, + }; +}; diff --git a/verified-reviews/utils/client.ts b/verified-reviews/utils/client.ts new file mode 100644 index 000000000..4da18103a --- /dev/null +++ b/verified-reviews/utils/client.ts @@ -0,0 +1,186 @@ +import { fetchAPI } from "../../utils/fetch.ts"; +import { Ratings, Reviews, VerifiedReviewsFullReview } from "./types.ts"; +import { Product } from "../../commerce/types.ts"; +import { ConfigVerifiedReviews } from "../mod.ts"; +import { context } from "@deco/deco"; +export type ClientVerifiedReviews = ReturnType; +export interface PaginationOptions { + count?: number; + offset?: number; + order?: + | "date_desc" + | "date_ASC" + | "rate_DESC" + | "rate_ASC" + | "helpfulrating_DESC"; +} +const MessageError = { + ratings: + "🔴⭐ Error on call ratings of Verified Review - probably unidentified product", + rating: + "🔴⭐ Error on call single rating of Verified Review - probably unidentified product", + fullReview: + "🔴⭐ Error on call Full Review of Verified Review - probably unidentified product", +}; +const baseUrl = "https://awsapis3.netreviews.eu/product"; +export const createClient = (params: ConfigVerifiedReviews | undefined) => { + if (!params) { + return; + } + const { idWebsite } = params; + /** @description https://documenter.getpostman.com/view/2336519/SVzw6MK5#338f8f1b-4379-40a2-8893-080fe5234679 */ + const rating = async ({ productId }: { + productId: string; + }) => { + const payload = { + query: "average", + products: [productId], + idWebsite: idWebsite, + plateforme: "br", + }; + try { + const data = await fetchAPI(`${baseUrl}`, { + method: "POST", + body: JSON.stringify(payload), + }); + return Object.keys(data).length ? data : undefined; + } catch (error) { + if (context.isDeploy) { + console.error(MessageError.rating, error); + } else { + throw new Error(`${MessageError.rating} - ${error}`); + } + return undefined; + } + }; + /** @description https://documenter.getpostman.com/view/2336519/SVzw6MK5#6d8ab05a-28b6-48b3-9e8f-6bbbc046619a */ + const ratings = async ({ productsIds }: { + productsIds: string[]; + }) => { + const payload = { + query: "average", + products: productsIds, + idWebsite: idWebsite, + plateforme: "br", + }; + try { + const data = await fetchAPI(`${baseUrl}`, { + method: "POST", + body: JSON.stringify(payload), + }); + return Object.keys(data).length ? data : undefined; + } catch (error) { + if (context.isDeploy) { + console.error(MessageError.ratings, error); + } else { + console.log(`${MessageError.ratings} - ${error}`); + return undefined; + } + return undefined; + } + }; + /** @description https://documenter.getpostman.com/view/2336519/SVzw6MK5#daf51360-c79e-451a-b627-33bdd0ef66b8 */ + const reviews = ( + { productId, count = 5, offset = 0, order = "date_desc" }: + & PaginationOptions + & { + productId: string; + }, + ) => { + const payload = { + query: "reviews", + product: productId, + idWebsite: idWebsite, + plateforme: "br", + offset: offset, + limit: count, + order: order, + }; + return fetchAPI(`${baseUrl}`, { + method: "POST", + body: JSON.stringify(payload), + }); + }; + const fullReview = async ( + { productId, count = 5, offset = 0 }: PaginationOptions & { + productId: string; + }, + ): Promise => { + try { + const response = await Promise.all([ + rating({ productId }), + reviews({ productId, count, offset }), + ]); + const [responseRating, responseReview] = response.flat() as [ + Ratings, + Reviews | null, + ]; + const currentRating = responseRating?.[productId]?.[0]; + return { + aggregateRating: currentRating + ? { + "@type": "AggregateRating", + ratingValue: Number(parseFloat(currentRating.rate).toFixed(1)), + reviewCount: Number(currentRating.count), + } + : undefined, + review: responseReview + ? responseReview.reviews?.map((item) => ({ + "@type": "Review", + author: [ + { + "@type": "Author", + name: `${item.firstname} ${item.lastname}`, + }, + ], + datePublished: item.review_date, + reviewBody: item.review, + reviewRating: { + "@type": "AggregateRating", + ratingValue: Number(item.rate), + // this api does not support multiple reviews + reviewCount: 1, + }, + })) + : [], + }; + } catch (error) { + if (context.isDeploy) { + console.error(MessageError.ratings, error); + } else { + throw new Error(`${MessageError.fullReview} - ${error}`); + } + return { + aggregateRating: undefined, + review: [], + }; + } + }; + const storeReview = async (): Promise => { + try { + const response = await fetchAPI( + `https://cl.avis-verifies.com/br/cache/8/6/a/${idWebsite}/AWS/WEBSITE_API/reviews.json`, + { + method: "GET", + }, + ); + return (response ? response : []); + } catch (error) { + if (context.isDeploy) { + console.error(MessageError.ratings, error); + } else { + throw new Error(`${MessageError.ratings} - ${error}`); + } + return null; + } + }; + return { + rating, + ratings, + reviews, + fullReview, + storeReview, + }; +}; +export const getProductId = (product: Product) => + product.isVariantOf!.productGroupID; diff --git a/verified-reviews/utils/transform.ts b/verified-reviews/utils/transform.ts new file mode 100644 index 000000000..8b8c8fee8 --- /dev/null +++ b/verified-reviews/utils/transform.ts @@ -0,0 +1,40 @@ +import { + AggregateRating, + Review as CommerceReview, +} from "../../commerce/types.ts"; +import { Ratings, Review } from "./types.ts"; + +export const getRatingProduct = ({ + ratings, + productId, +}: { + ratings: Ratings | undefined; + productId: string; +}): AggregateRating | undefined => { + const rating = ratings?.[productId]?.[0]; + if (!rating) { + return undefined; + } + + return { + "@type": "AggregateRating", + ratingCount: Number(rating.count), + ratingValue: Number(parseFloat(rating.rate).toFixed(1)), + }; +}; + +export const toReview = (review: Review): CommerceReview => ({ + "@type": "Review", + author: [ + { + "@type": "Author", + name: `${review.firstname} ${review.lastname}`, + }, + ], + datePublished: review.review_date, + reviewBody: review.review, + reviewRating: { + "@type": "AggregateRating", + ratingValue: Number(review.rate), + }, +}); diff --git a/verified-reviews/utils/types.ts b/verified-reviews/utils/types.ts new file mode 100644 index 000000000..3a47c48ba --- /dev/null +++ b/verified-reviews/utils/types.ts @@ -0,0 +1,51 @@ +import { + AggregateRating, + Review as CommerceReview, +} from "../../commerce/types.ts"; + +export interface Ratings { + [key: string]: { + "rate": string; + "count": string; + }[]; +} + +export interface Review { + count_helpful_yes: string; + firstname: string; + order_ref: string; + rate: string; + review: string; + email: string; + count_helpful_no: string; + hide_personnal_data: string; + lastname: string; + medias: string; + order_date: string; + id_review_product: string; + review_date: string; + id_product: string; + id_review: string; + sign_helpful: string; + publish_date: string; + info1: string; + info2: string; + info3: string; + info4: string; + info5: string; + info6: string; + info7: string; + info8: string; + info9: string; + info10: string; +} + +export interface Reviews { + reviews: Review[]; + status: number[]; +} + +export interface VerifiedReviewsFullReview { + aggregateRating?: AggregateRating; + review: CommerceReview[]; +} diff --git a/vnda/README.md b/vnda/README.md new file mode 100644 index 000000000..9362fe685 --- /dev/null +++ b/vnda/README.md @@ -0,0 +1,11 @@ + + +Your online store with the best e-commerce platform. + +Loaders, actions and workflows for adding VNDA Commerce Platform to your deco.cx website. + +VNDA offers a range of features and services to facilitate e-commerce operations. + +This app wrapps VNDA Commerce API into a comprehensive set of +loaders/actions/workflows empowering non technical users to interact and act +upon their headless commerce. \ No newline at end of file diff --git a/vnda/actions/cart/addItem.ts b/vnda/actions/cart/addItem.ts index 761baa6a7..2b490ae5a 100644 --- a/vnda/actions/cart/addItem.ts +++ b/vnda/actions/cart/addItem.ts @@ -1,6 +1,7 @@ -import { setCookie } from "std/http/mod.ts"; +import { HttpError } from "../../../utils/http.ts"; +import cartLoader, { Cart } from "../../loaders/cart.ts"; import { AppContext } from "../../mod.ts"; -import type { Cart } from "../../utils/client/types.ts"; +import { getCartCookie } from "../../utils/cart.ts"; export interface Props { itemId: string; @@ -13,36 +14,23 @@ const action = async ( req: Request, ctx: AppContext, ): Promise => { - const { client } = ctx; + const { api } = ctx; const { itemId, quantity, attributes } = props; - const reqCookies = req.headers.get("cookie") ?? ""; + const cartId = getCartCookie(req.headers); - const { orderForm, cookies } = await client.carrinho.adicionar({ - cookie: reqCookies, - sku: itemId, - quantity, - attributes, - }); - - // in case the cart was created, set the cookie to the browser - for (const cookie of cookies) { - setCookie(ctx.response.headers, { - ...cookie, - domain: new URL(req.url).hostname, - }); + if (!cartId) { + throw new HttpError(400, "Missing cart cookie"); } - const allCookies = [ - reqCookies, - ...cookies.map(({ name, value }) => `${name}=${value}`), - ].join("; "); - - const relatedItems = await client.carrinho.relatedItems(allCookies); + await api["POST /api/v2/carts/:cartId/items"]({ cartId }, { + body: { + sku: itemId, + quantity, + extra: attributes, + }, + }); - return { - orderForm, - relatedItems, - }; + return cartLoader({}, req, ctx); }; export default action; diff --git a/vnda/actions/cart/addItems.ts b/vnda/actions/cart/addItems.ts new file mode 100644 index 000000000..7cd31f58b --- /dev/null +++ b/vnda/actions/cart/addItems.ts @@ -0,0 +1,46 @@ +import { HttpError } from "../../../utils/http.ts"; +import cartLoader, { Cart } from "../../loaders/cart.ts"; +import { AppContext } from "../../mod.ts"; +import { getCartCookie } from "../../utils/cart.ts"; + +interface Item { + itemId: string; + quantity: number; + attributes?: Record; +} + +export interface Props { + items: Item[]; +} + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { api } = ctx; + const { items } = props; + const cartId = getCartCookie(req.headers); + + if (!cartId) { + throw new HttpError(400, "Missing cart cookie"); + } + + await api["POST /api/v2/carts/:cartId/items/bulk"]({ cartId }, { + body: { + items: items.map((item) => ({ + sku: item.itemId, + quantity: item.quantity, + customizations: item.attributes + ? Object.entries(item.attributes).map(([key, value]) => ({ + [key]: value, + })) + : undefined, + })), + }, + }); + + return cartLoader({}, req, ctx); +}; + +export default action; diff --git a/vnda/actions/cart/setShippingAddress.ts b/vnda/actions/cart/setShippingAddress.ts deleted file mode 100644 index 5bf2fce04..000000000 --- a/vnda/actions/cart/setShippingAddress.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AppContext } from "../../mod.ts"; -import type { Cart } from "../../utils/client/types.ts"; - -export interface Props { - zip: string; -} - -const action = async ( - props: Props, - req: Request, - ctx: AppContext, -): Promise => { - const { client } = ctx; - const { zip } = props; - - const cookie = req.headers.get("cookie") ?? ""; - - const shipping = await client.cep(zip, cookie); - const updated = await ctx.invoke("apps/vnda/loaders/cart.ts"); - - return { shipping, ...updated }; -}; - -export default action; diff --git a/vnda/actions/cart/simulation.ts b/vnda/actions/cart/simulation.ts new file mode 100644 index 000000000..ff7c7ee67 --- /dev/null +++ b/vnda/actions/cart/simulation.ts @@ -0,0 +1,28 @@ +import { AppContext } from "../../mod.ts"; +import type { ShippingMethod } from "../../utils/client/types.ts"; +import { badRequest } from "@deco/deco"; +export interface Props { + skuId: string; + quantity: number; + zip: string; +} +const action = async ( + props: Props, + _req: Request, + ctx: AppContext, +): Promise => { + const { api } = ctx; + const { skuId, quantity, zip } = props; + if (!skuId || !quantity || !zip) { + badRequest({ + message: "could not find some props", + }); + } + const cep = await api["GET /api/v2/variants/:sku/shipping_methods"]({ + sku: skuId, + quantity, + zip, + }); + return cep.json(); +}; +export default action; diff --git a/vnda/actions/cart/updateCart.ts b/vnda/actions/cart/updateCart.ts new file mode 100644 index 000000000..39bb91f52 --- /dev/null +++ b/vnda/actions/cart/updateCart.ts @@ -0,0 +1,31 @@ +import { HttpError } from "../../../utils/http.ts"; +import cartLoader, { Cart } from "../../loaders/cart.ts"; +import { AppContext } from "../../mod.ts"; +import { getCartCookie } from "../../utils/cart.ts"; + +export interface Props { + agent?: string; + zip?: string; + client_id?: number; + coupon_code?: string; + rebate_token?: string; +} + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { api } = ctx; + const cartId = getCartCookie(req.headers); + + if (!cartId) { + throw new HttpError(400, "Missing cart cookie"); + } + + await api["PATCH /api/v2/carts/:cartId"]({ cartId }, { body: props }); + + return cartLoader({}, req, ctx); +}; + +export default action; diff --git a/vnda/actions/cart/updateCoupon.ts b/vnda/actions/cart/updateCoupon.ts deleted file mode 100644 index f7d9a8fe4..000000000 --- a/vnda/actions/cart/updateCoupon.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AppContext } from "../../mod.ts"; -import type { Cart } from "../../utils/client/types.ts"; - -export interface Props { - code: string; -} - -const action = async ( - props: Props, - req: Request, - ctx: AppContext, -): Promise => { - const { client } = ctx; - const { code } = props; - const cookie = req.headers.get("cookie") ?? ""; - - const coupon = await client.coupon(code, cookie); - const updated = await ctx.invoke("apps/vnda/loaders/cart.ts"); - - return { coupon, ...updated }; -}; - -export default action; diff --git a/vnda/actions/cart/updateItem.ts b/vnda/actions/cart/updateItem.ts index 76e0e37c6..3cda16269 100644 --- a/vnda/actions/cart/updateItem.ts +++ b/vnda/actions/cart/updateItem.ts @@ -1,5 +1,7 @@ +import { HttpError } from "../../../utils/http.ts"; +import cartLoader, { Cart } from "../../loaders/cart.ts"; import { AppContext } from "../../mod.ts"; -import type { Cart } from "../../utils/client/types.ts"; +import { getCartCookie } from "../../utils/cart.ts"; export interface Props { itemId: number | string; @@ -11,17 +13,23 @@ const action = async ( req: Request, ctx: AppContext, ): Promise => { - const { client } = ctx; - const { itemId: item_id, quantity } = props; - const cookie = req.headers.get("cookie") ?? ""; + const { api } = ctx; + const { itemId, quantity } = props; + const cartId = getCartCookie(req.headers); + + if (!cartId) { + throw new HttpError(400, "Missing cart cookie"); + } if (quantity > 0) { - await client.carrinho.atualizar({ item_id, quantity }, cookie); + await api["PATCH /api/v2/carts/:cartId/items/:itemId"]({ cartId, itemId }, { + body: { quantity }, + }); } else { - await client.carrinho.remover(item_id, cookie); + await api["DELETE /api/v2/carts/:cartId/items/:itemId"]({ cartId, itemId }); } - return ctx.invoke("apps/vnda/loaders/cart.ts"); + return cartLoader({}, req, ctx); }; export default action; diff --git a/vnda/actions/notifyme.ts b/vnda/actions/notifyme.ts new file mode 100644 index 000000000..166a0983d --- /dev/null +++ b/vnda/actions/notifyme.ts @@ -0,0 +1,43 @@ +import { AppContext } from "../mod.ts"; + +export interface Props { + key: string; + sku: string; + email: string; +} + +const action = async ( + props: Props, + _req: Request, + ctx: AppContext, +): Promise => { + const { account } = ctx; + const { key, sku, email } = props; + + const formdata = new FormData(); + formdata.append("key", key); + formdata.append("sku", sku); + formdata.append("email", email); + + const options = { + method: "POST", + body: formdata, + }; + + try { + await Promise.all([ + fetch( + `https://${account}.cdn.vnda.com.br/lista_de_espera`, + options, + ).then((res) => res.json()), + fetch( + `https://${account}.cdn.vnda.com.br/webform`, + options, + ), + ]); + } catch (error) { + console.log(error); + } +}; + +export default action; diff --git a/vnda/doccache.zst b/vnda/doccache.zst deleted file mode 100644 index 186ebc57e..000000000 Binary files a/vnda/doccache.zst and /dev/null differ diff --git a/vnda/handlers/sitemap.ts b/vnda/handlers/sitemap.ts new file mode 100644 index 000000000..fc560a988 --- /dev/null +++ b/vnda/handlers/sitemap.ts @@ -0,0 +1,73 @@ +import { AppContext } from "../mod.ts"; + +type ConnInfo = Deno.ServeHandlerInfo; +const xmlHeader = + ''; + +const includeSiteMaps = ( + currentXML: string, + origin: string, + includes?: string[], +) => { + const siteMapIncludeTags = []; + + for (const include of (includes ?? [])) { + siteMapIncludeTags.push(` + + ${include.startsWith("/") ? `${origin}${include}` : include} + ${new Date().toISOString().substring(0, 10)} +`); + } + return siteMapIncludeTags.length > 0 + ? currentXML.replace( + xmlHeader, + `${xmlHeader}\n${siteMapIncludeTags.join("\n")}`, + ) + : currentXML; +}; + +export interface Props { + include?: string[]; +} +/** + * @title Sitemap Proxy + */ +export default function Sitemap( + { include }: Props, + appCtx: AppContext, +) { + const { publicUrl } = appCtx; + return ( + req: Request, + _ctx: ConnInfo, + ) => { + if (!publicUrl) { + throw new Error("Missing publicUrl"); + } + + const url = new URL( + publicUrl?.startsWith("http") ? publicUrl : `https://${publicUrl}`, + ); + + const headers = new Headers(); + headers.set("Content-Type", "application/xml"); + + const reqUrl = new URL(req.url); + const text = `${xmlHeader} + + ${url.host}sitemap/vnda.xml + + `; + return new Response( + includeSiteMaps( + text.replaceAll(publicUrl, `${reqUrl.origin}/`), + reqUrl.origin, + include, + ), + { + headers, + status: 200, + }, + ); + }; +} diff --git a/vnda/hooks/context.ts b/vnda/hooks/context.ts index 162c8053d..6e0ac94d0 100644 --- a/vnda/hooks/context.ts +++ b/vnda/hooks/context.ts @@ -1,9 +1,9 @@ import { IS_BROWSER } from "$fresh/runtime.ts"; import { signal } from "@preact/signals"; -import { Runtime } from "../runtime.ts"; -import { Cart } from "../utils/client/types.ts"; +import { invoke } from "../runtime.ts"; +import type { Cart } from "../loaders/cart.ts"; -interface Context { +export interface Context { cart: Cart; } @@ -47,10 +47,8 @@ const enqueue = ( }; const load = (signal: AbortSignal) => - Runtime.invoke({ - cart: { - key: "apps/vnda/loaders/cart.ts", - }, + invoke({ + cart: invoke.vnda.loaders.cart(), }, { signal }); if (IS_BROWSER) { diff --git a/vnda/hooks/useCart.ts b/vnda/hooks/useCart.ts index eda74c1b2..fd30d4e14 100644 --- a/vnda/hooks/useCart.ts +++ b/vnda/hooks/useCart.ts @@ -1,48 +1,49 @@ -import { AnalyticsItem } from "apps/commerce/types.ts"; -import { Runtime } from "../runtime.ts"; -import { Cart, Item } from "../utils/client/types.ts"; -import { state as storeState } from "./context.ts"; +// deno-lint-ignore-file no-explicit-any +import type { AnalyticsItem } from "../../commerce/types.ts"; +import type { Manifest } from "../manifest.gen.ts"; +import { invoke } from "../runtime.ts"; +import { Context, state as storeState } from "./context.ts"; const { cart, loading } = storeState; +type Item = NonNullable["items"][number]; + export const itemToAnalyticsItem = ( item: Item & { quantity: number }, index: number, -): AnalyticsItem => ({ - item_id: `${item.id}_${item.variant_sku}`, - item_name: item.product_name, - discount: item.price - item.variant_price, - item_variant: item.variant_name.slice(item.product_name.length).trim(), - // TODO: check - price: item.price, - // TODO - // item_brand: "todo", - index, - quantity: item.quantity, -}); +): AnalyticsItem => { + return { + item_id: item.variant_sku, + item_group_id: item.id, + quantity: item.quantity, + price: item.price, + index, + discount: Math.abs(item.variant_price - item.price), + item_name: item.product_name, + item_variant: item.variant_name, + }; +}; + +type EnqueuableActions< + K extends keyof Manifest["actions"], +> = Manifest["actions"][K]["default"] extends + (...args: any[]) => Promise ? K : never; -const wrap = - (action: (p: T, init?: RequestInit | undefined) => Promise) => - (p: T) => - storeState.enqueue(async (signal) => ({ - cart: await action(p, { signal }), - })); +const enqueue = < + K extends keyof Manifest["actions"], +>(key: EnqueuableActions) => +(props: Parameters[0]) => + storeState.enqueue((signal) => + invoke({ cart: { key, props } } as any, { signal }) as any + ); const state = { cart, loading, - addItem: wrap( - Runtime.create("apps/vnda/actions/cart/addItem.ts"), - ), - updateItem: wrap( - Runtime.create("apps/vnda/actions/cart/updateItem.ts"), - ), - setShippingAddress: wrap( - Runtime.create("apps/vnda/actions/cart/setShippingAddress.ts"), - ), - updateCoupon: wrap( - Runtime.create("apps/vnda/actions/cart/updateCoupon.ts"), - ), + update: enqueue("vnda/actions/cart/updateCart.ts"), + addItem: enqueue("vnda/actions/cart/addItem.ts"), + updateItem: enqueue("vnda/actions/cart/updateItem.ts"), + simulate: invoke.vnda.actions.cart.simulation, }; export const useCart = () => state; diff --git a/vnda/loaders/cart.ts b/vnda/loaders/cart.ts index c19fbbb60..74bbb0680 100644 --- a/vnda/loaders/cart.ts +++ b/vnda/loaders/cart.ts @@ -1,5 +1,11 @@ import { AppContext } from "../mod.ts"; -import type { Cart } from "../utils/client/types.ts"; +import { getAgentCookie, getCartCookie, setCartCookie } from "../utils/cart.ts"; +import { OpenAPI } from "../utils/openapi/vnda.openapi.gen.ts"; + +export type Cart = { + orderForm?: OpenAPI["POST /api/v2/carts"]["response"]; + relatedItems?: OpenAPI["POST /api/v2/carts"]["response"]["items"]; +}; /** * @title VNDA Integration @@ -10,17 +16,41 @@ const loader = async ( req: Request, ctx: AppContext, ): Promise => { - const { client } = ctx; - const cookies = req.headers.get("cookie") ?? ""; + const { api } = ctx; + const cartId = getCartCookie(req.headers); + const agent = getAgentCookie(req.headers); + + let orderForm; + + try { + orderForm = cartId + ? await api["GET /api/v2/carts/:cartId"]({ cartId }).then((res) => + res.json() + ) + : await api["POST /api/v2/carts"]({}, { body: {} }).then((res) => + res.json() + ); + } catch (_error) { + // Failed to get current cardId, creating a new orderForm + orderForm = await api["POST /api/v2/carts"]({}, { body: {} }).then((res) => + res.json() + ); + } + + const hasAgent = orderForm.agent === agent; - const [orderForm, relatedItems] = await Promise.all([ - client.carrinho.get(cookies), - client.carrinho.relatedItems(cookies), - ]); + if (!hasAgent && agent) { + const [{ id }] = await api["GET /api/v2/users"]({ external_code: agent }) + .then((res) => res.json()); + await api["PATCH /api/v2/carts/:cartId"]({ cartId: orderForm.id }, { + body: { agent, user_id: id }, + }); + } + setCartCookie(ctx.response.headers, orderForm.id.toString()); return { orderForm, - relatedItems, + relatedItems: orderForm.items ?? [], }; }; diff --git a/vnda/loaders/extensions/price/list.ts b/vnda/loaders/extensions/price/list.ts new file mode 100644 index 000000000..c8904f980 --- /dev/null +++ b/vnda/loaders/extensions/price/list.ts @@ -0,0 +1,21 @@ +import { Product } from "../../../../commerce/types.ts"; +import { ExtensionOf } from "../../../../website/loaders/extension.ts"; +import { AppContext } from "../../../mod.ts"; +import { fetchAndApplyPrices } from "../../../utils/transform.ts"; + +export interface Props { + priceCurrency: string; +} + +const loader = ( + { priceCurrency }: Props, + req: Request, + ctx: AppContext, +): ExtensionOf => +(products: Product[] | null) => { + if (!Array.isArray(products)) return products; + + return fetchAndApplyPrices(products, priceCurrency, req, ctx); +}; + +export default loader; diff --git a/vnda/loaders/extensions/price/listingPage.ts b/vnda/loaders/extensions/price/listingPage.ts new file mode 100644 index 000000000..6131c01b7 --- /dev/null +++ b/vnda/loaders/extensions/price/listingPage.ts @@ -0,0 +1,35 @@ +import { ProductListingPage } from "../../../../commerce/types.ts"; +import { ExtensionOf } from "../../../../website/loaders/extension.ts"; +import { AppContext } from "../../../mod.ts"; +import { fetchAndApplyPrices } from "../../../utils/transform.ts"; + +export interface Props { + priceCurrency: string; +} + +const loader = ( + { priceCurrency }: Props, + req: Request, + ctx: AppContext, +): ExtensionOf => +async (props: ProductListingPage | null) => { + if (!props) return props; + + const { products, ...page } = props; + + if (!Array.isArray(products)) return props; + + const extendedProducts = await fetchAndApplyPrices( + products, + priceCurrency, + req, + ctx, + ); + + return { + ...page, + products: extendedProducts, + }; +}; + +export default loader; diff --git a/vnda/loaders/productDetailsPage.ts b/vnda/loaders/productDetailsPage.ts index cdd2dd6f7..ad959a032 100644 --- a/vnda/loaders/productDetailsPage.ts +++ b/vnda/loaders/productDetailsPage.ts @@ -1,10 +1,13 @@ import type { ProductDetailsPage } from "../../commerce/types.ts"; +import { STALE } from "../../utils/fetch.ts"; import type { RequestURLParam } from "../../website/functions/requestToParam.ts"; import { AppContext } from "../mod.ts"; -import { getSEOFromTag, parseSlug, toProduct } from "../utils/transform.ts"; +import { ProductPrice } from "../utils/client/types.ts"; +import { parseSlug, toProduct } from "../utils/transform.ts"; export interface Props { slug: RequestURLParam; + priceIntl?: boolean; } /** @@ -17,43 +20,109 @@ async function loader( ctx: AppContext, ): Promise { const url = new URL(req.url); - const { slug } = props; - const { client } = ctx; + const { slug, priceIntl = false } = props; + const { api } = ctx; if (!slug) return null; const variantId = url.searchParams.get("skuId") || null; - const { id } = parseSlug(slug); + const fromSlug = parseSlug(slug); - const [maybeProduct, seo] = await Promise.all([ - client.product.get(id), - client.seo.product(id), + // 404: invalid slug + if (!fromSlug) { + return null; + } + + const { id } = fromSlug; + + const getMaybeProduct = async (id: number) => { + try { + const result = await api["GET /api/v2/products/:id"]({ + id, + include_images: "true", + }, STALE); + return result.json(); + } catch (error) { + // Make async rendering work + if (error instanceof DOMException && error.name === "AbortError") { + throw error; + } + + return null; + } + }; + + // Since the Product by ID request don't return the INTL price, is necessary to search all prices and replace them + const getProductPrice = async (id: number): Promise => { + if (!priceIntl) { + return null; + } else { + try { + const result = await api["GET /api/v2/products/:productId/price"]({ + productId: id, + }, STALE); + return result.json(); + } catch (error) { + // Make async rendering work + if (error instanceof DOMException && error.name === "AbortError") { + throw error; + } + + return null; + } + } + }; + + const [maybeProduct, productPrice] = await Promise.all([ + getMaybeProduct(id), + getProductPrice(id), ]); + const variantsLength = maybeProduct?.variants?.length ?? 0; + // 404: product not found - if (!maybeProduct) { + if (!maybeProduct || variantsLength === 0) { return null; } const product = toProduct(maybeProduct, variantId, { url, priceCurrency: "BRL", + productPrice, }); + const segments = url.pathname.slice(1).split("/"); + + let seoArray; + if (product.isVariantOf?.productGroupID) { + seoArray = await api["GET /api/v2/seo_data"]({ + resource_type: "Product", + resource_id: Number(product.isVariantOf.productGroupID), + }, STALE).then((res) => res.json()) + .catch(() => undefined); + } + + const seo = seoArray?.at(-1); + return { "@type": "ProductDetailsPage", // TODO: Find out what's the right breadcrumb on vnda breadcrumbList: { "@type": "BreadcrumbList", - itemListElement: [], - numberOfItems: 0, + itemListElement: segments.map((s, i) => ({ + "@type": "ListItem", + name: s, + position: i + 1, + item: new URL(`/${segments.slice(0, i + 1).join("/")}`, url).href, + })), + numberOfItems: segments.length, }, product, - seo: getSEOFromTag({ - title: product.name, - description: product.description || "", - ...seo?.[0], - }, req), + seo: { + title: seo?.title || (product.name ?? ""), + description: seo?.description || (product.description ?? ""), + canonical: new URL(`/${segments.join("/")}`, url).href, + }, }; } diff --git a/vnda/loaders/productDetailsPageVideo.ts b/vnda/loaders/productDetailsPageVideo.ts new file mode 100644 index 000000000..53fbb948c --- /dev/null +++ b/vnda/loaders/productDetailsPageVideo.ts @@ -0,0 +1,28 @@ +import { AppContext } from "../mod.ts"; +import { ExtensionOf } from "../../website/loaders/extension.ts"; +import { ProductDetailsPage } from "../../commerce/types.ts"; +import { addVideoToProduct } from "../utils/transform.ts"; +import { STALE } from "../../utils/fetch.ts"; + +export default function productDetailsPageVideo( + _props: unknown, + _req: Request, + ctx: AppContext, +): ExtensionOf { + const { api } = ctx; + return async (productDetailsPage: ProductDetailsPage | null) => { + if (!productDetailsPage) { + return null; + } + const { product } = productDetailsPage; + const { inProductGroupWithID } = product; + const videos = await api["GET /api/v2/products/:productId/videos"]({ + productId: inProductGroupWithID as string, + }, STALE).then((r) => r.json()).catch(() => null); + const productWithVideo = addVideoToProduct(product, videos); + return { + ...productDetailsPage, + product: productWithVideo, + }; + }; +} diff --git a/vnda/loaders/productList.ts b/vnda/loaders/productList.ts index 4acc3a49e..4ca265206 100644 --- a/vnda/loaders/productList.ts +++ b/vnda/loaders/productList.ts @@ -1,4 +1,5 @@ -import type { Product } from "apps/commerce/types.ts"; +import type { Product } from "../../commerce/types.ts"; +import { STALE } from "../../utils/fetch.ts"; import type { AppContext } from "../mod.ts"; import { toProduct } from "../utils/transform.ts"; @@ -17,6 +18,12 @@ export interface Props { /** @description search for products that have certain tag */ tags?: string[]; + + /** @description search for products that have certain type_tag */ + typeTags?: { key: string; value: string }[]; + + /** @description search for products by id */ + ids: number[]; } /** @@ -29,22 +36,39 @@ const productListLoader = async ( ctx: AppContext, ): Promise => { const url = new URL(req.url); - const { client } = ctx; - - const search = await client.product.search({ - term: props?.term, - wildcard: props?.wildcard, - sort: props?.sort, - per_page: props?.count, - tags: props?.tags, + const { api } = ctx; + + const { results: searchResults = [] } = await api + ["GET /api/v2/products/search"]({ + term: props?.term, + wildcard: props?.wildcard, + sort: props?.sort, + per_page: props?.count, + "tags[]": props?.tags, + ...Object.fromEntries( + (props.typeTags || []).map(( + { key, value }, + ) => [`type_tags[${key}][]`, value]), + ), + "ids[]": props?.ids, + }, STALE).then((res) => res.json()); + + const validProducts = searchResults.filter(({ variants }) => { + return variants.length !== 0; }); - return search.results.map((product) => - toProduct(product, null, { + if (validProducts.length === 0) return null; + + const sortedProducts = props.ids?.length > 0 + ? props.ids.map((id) => validProducts.find((product) => product.id === id)) + : validProducts; + + return sortedProducts.map((product) => { + return toProduct(product!, null, { url, priceCurrency: "BRL", - }) - ); + }); + }); }; export default productListLoader; diff --git a/vnda/loaders/productListingPage.ts b/vnda/loaders/productListingPage.ts index b030bf9ed..874da1ab6 100644 --- a/vnda/loaders/productListingPage.ts +++ b/vnda/loaders/productListingPage.ts @@ -1,14 +1,20 @@ -import { Sort } from "../utils/client/types.ts"; +import type { + BreadcrumbList, + ProductListingPage, +} from "../../commerce/types.ts"; import { SortOption } from "../../commerce/types.ts"; +import { STALE } from "../../utils/fetch.ts"; +import type { RequestURLParam } from "../../website/functions/requestToParam.ts"; +import type { AppContext } from "../mod.ts"; +import { ProductSearchResult, Sort } from "../utils/client/types.ts"; +import { Tag } from "../utils/openapi/vnda.openapi.gen.ts"; import { + canonicalFromTags, getSEOFromTag, toFilters, toProduct, typeTagExtractor, } from "../utils/transform.ts"; -import type { ProductListingPage } from "../../commerce/types.ts"; -import type { RequestURLParam } from "../../website/functions/requestToParam.ts"; -import type { AppContext } from "../mod.ts"; export const VNDA_SORT_OPTIONS: SortOption[] = [ { value: "", label: "Relevância" }, @@ -18,6 +24,15 @@ export const VNDA_SORT_OPTIONS: SortOption[] = [ { value: "highest_price", label: "Maior preço" }, ]; +type Operators = "and" | "or"; + +interface FilterOperator { + type_tags?: Operators; + property1?: Operators; + property2?: Operators; + property3?: Operators; +} + export interface Props { /** * @description overides the query term @@ -37,8 +52,38 @@ export interface Props { * Slug for category pages */ slug?: RequestURLParam; + + filterByTags?: boolean; + + /** @description if properties are empty, "typeTags" value will apply to all. Defaults to "and" */ + filterOperator?: FilterOperator; + + /** + * @hide true + * @description The URL of the page, used to override URL from request + */ + pageHref?: string; } +const getBreadcrumbList = (categories: Tag[], url: URL): BreadcrumbList => ({ + "@type": "BreadcrumbList" as const, + itemListElement: categories.map((t, index) => ({ + "@type": "ListItem" as const, + item: canonicalFromTags(categories.slice(0, index + 1), url).href, + position: index + 1, + name: t.title, + })), + numberOfItems: categories.length, +}); + +const handleOperator = ( + key: "type_tags" | "property1" | "property2" | "property3", + defaultValue: Operators, + filterOperators?: FilterOperator, +) => ({ + [`${key}_operator`]: filterOperators?.[key] ?? defaultValue ?? "and", +}); + /** * @title VNDA Integration * @description Product Listing Page loader @@ -49,76 +94,168 @@ const searchLoader = async ( ctx: AppContext, ): Promise => { // get url from params - const url = new URL(req.url); - const { client } = ctx; + const url = new URL(props.pageHref || req.url); + const { api } = ctx; const count = props.count ?? 12; - const { cleanUrl, typeTags } = typeTagExtractor(url); const sort = url.searchParams.get("sort") as Sort; const page = Number(url.searchParams.get("page")) || 1; - const isSearchPage = url.pathname === "/busca"; + const isSearchPage = ctx.searchPagePath + ? ctx.searchPagePath === url.pathname + : url.pathname === "/busca" || url.pathname === "/s"; const qQueryString = url.searchParams.get("q"); const term = props.term || props.slug || qQueryString || undefined; - const search = await client.product.search({ - term, - sort, - page, - per_page: count, - tags: props.tags, - type_tags: typeTags, - wildcard: true, - }); + const priceFilterRegex = /de-(\d+)-a-(\d+)/; + const filterMatch = url.href.match(priceFilterRegex) ?? []; + + const categoryTagName = (props.term || url.pathname.slice(1) || "").split( + "/", + ); + + const properties1 = url.searchParams.getAll("type_tags[property1][]"); + const properties2 = url.searchParams.getAll("type_tags[property2][]"); + const properties3 = url.searchParams.getAll("type_tags[property3][]"); + + const categoryTagNames = Array.from(url.searchParams.values()); + + const tags = await Promise.all([ + ...categoryTagNames, + ...categoryTagName.filter((item): item is string => + typeof item === "string" + ), + ].map((name) => + api["GET /api/v2/tags/:name"]({ name }, STALE) + .then((res) => res.json()) + .catch(() => undefined) + )); - const categoryTagName = props.term || url.pathname.split("/").pop() || ""; - const [seo, categoryTag] = await Promise.all([ - client.seo.tag(categoryTagName), - isSearchPage - ? client.tag(categoryTagName).catch(() => undefined) - : undefined, + const categories = tags + .slice(-categoryTagName.length) + .filter((tag): tag is Tag => + typeof tag !== "undefined" && typeof tag.name !== "undefined" + ); + + const filteredTags = tags + .filter((tag): tag is Tag => typeof tag !== "undefined"); + + const { cleanUrl, typeTags } = typeTagExtractor(url, filteredTags); + + const initialTags = props.tags && props.tags?.length > 0 + ? props.tags + : undefined; + + const categoryTagsToFilter = categories.length > 0 && props.filterByTags + ? categories.map((t) => t.name) + .filter((name): name is string => typeof name === "string") + : undefined; + + const defaultOperator = props.filterOperator?.type_tags ?? "and"; + + const preference = categoryTagsToFilter + ? term + : qQueryString ?? url.pathname.slice(1); + + const tag = categories.at(-1); + + const [response, seo = []] = await Promise.all([ + await api["GET /api/v2/products/search"]({ + term: term ?? preference, + sort, + page, + per_page: count, + "tags[]": initialTags ?? categoryTagsToFilter, + wildcard: true, + ...(filterMatch[1] && { min_price: Number(filterMatch[1]) }), + ...(filterMatch[2] && { max_price: Number(filterMatch[2]) }), + "property1_values[]": properties1, + "property2_values[]": properties2, + "property3_values[]": properties3, + ...handleOperator("type_tags", defaultOperator, props.filterOperator), + ...handleOperator("property1", defaultOperator, props.filterOperator), + ...handleOperator("property2", defaultOperator, props.filterOperator), + ...handleOperator("property3", defaultOperator, props.filterOperator), + ...Object.fromEntries( + typeTags.reduce]>>( + (acc, { key, value, isProperty }) => { + if (isProperty) return acc; + + const pos = acc.findIndex((item) => item[0] === key); + + if (pos !== -1) { + acc[pos] = [key, [...acc[pos][1], value]]; + return acc; + } + + return [...acc, [key, [value]]]; + }, + [], + ), + ), + }, STALE), + api["GET /api/v2/seo_data"]( + { resource_type: "Tag", code: tag?.name }, + STALE, + ).then((res) => res.json()) + .catch(() => undefined), ]); - const { results: searchResults, pagination } = search; - const products = searchResults.map((product) => - toProduct(product, null, { + const pagination = JSON.parse( + response.headers.get("x-pagination") ?? "null", + ) as ProductSearchResult["pagination"] | null; + + const search = await response.json(); + + const { results: searchResults = [] } = search; + + const validProducts = searchResults.filter(({ variants }) => { + return variants.length !== 0; + }); + + const products = validProducts.map((product) => { + return toProduct(product, null, { url, priceCurrency: "BRL", - }) - ); + }); + }); const nextPage = new URLSearchParams(url.searchParams); const previousPage = new URLSearchParams(url.searchParams); - if (pagination.next_page) { + if (pagination?.next_page) { nextPage.set("page", (page + 1).toString()); } - if (pagination.prev_page) { + if (pagination?.prev_page) { previousPage.set("page", (page - 1).toString()); } - const hasSEO = !isSearchPage && (seo?.[0] || categoryTag); + const hasTypeTags = !![ + ...typeTags, + ...properties1, + ...properties2, + ...properties3, + ].length; return { "@type": "ProductListingPage", - seo: hasSEO - ? getSEOFromTag({ ...categoryTag, ...seo?.[0] }, req) - : undefined, - // TODO: Find out what's the right breadcrumb on vnda - breadcrumb: { - "@type": "BreadcrumbList", - itemListElement: [], - numberOfItems: 0, - }, + seo: getSEOFromTag(categories, url, seo.at(-1), hasTypeTags, isSearchPage), + breadcrumb: isSearchPage + ? { + "@type": "BreadcrumbList", + itemListElement: [], + numberOfItems: 0, + } + : getBreadcrumbList(categories, url), filters: toFilters(search.aggregations, typeTags, cleanUrl), - products: products, + products, pageInfo: { - nextPage: pagination.next_page ? `?${nextPage}` : undefined, - previousPage: pagination.prev_page ? `?${previousPage}` : undefined, + nextPage: pagination?.next_page ? `?${nextPage}` : undefined, + previousPage: pagination?.prev_page ? `?${previousPage}` : undefined, currentPage: page, - records: pagination.total_count, + records: pagination?.total_count, recordPerPage: count, }, sortOptions: VNDA_SORT_OPTIONS, diff --git a/vnda/loaders/proxy.ts b/vnda/loaders/proxy.ts new file mode 100644 index 000000000..20e6a0606 --- /dev/null +++ b/vnda/loaders/proxy.ts @@ -0,0 +1,133 @@ +import { Route } from "../../website/flags/audience.ts"; +import { AppContext } from "../mod.ts"; + +const PAGE_PATHS = [ + "/admin", + "/admin/*", + "/carrinho", + "/carrinho/*", + "/cdn-cgi/*", + "/cep", + "/cep/*", + "/checkout/*", + "/common/*", + "/components/*", + "/conta", + "/conta/*", + "/cupom/ajax", + "/entrar", + "/entrar/*", + "/images/*", + "/javascripts/*", + "/loja/configuracoes", + "/pagamento/*", + "/pedido/*", + "/recaptcha", + "/recuperar_senha", + "/sair", + "/sitemap/vnda.xml", + "/stylesheets/*", + "/v/s", + "/webform", +]; + +const API_PATHS = [ + "/api/*", +]; + +const decoSiteMapUrl = "/sitemap/deco.xml"; + +const VNDA_HOST_HEADER = "X-Shop-Host"; +export interface Props { + /** @description ex: /p/fale-conosco */ + pagesToProxy?: string[]; + generateDecoSiteMap?: boolean; + /** + * @title Exclude paths from /deco-sitemap.xml + */ + excludePathsFromDecoSiteMap?: string[]; + includeSiteMap?: string[]; +} + +/** + * @title VNDA Proxy Routes + */ +function loader( + { + pagesToProxy = [], + generateDecoSiteMap, + excludePathsFromDecoSiteMap = [], + includeSiteMap, + }: Props, + _req: Request, + { publicUrl, account }: AppContext, +): Route[] { + const internalDomain = `https://${account}.cdn.vnda.com.br/`; + const url = new URL( + publicUrl?.startsWith("http") ? publicUrl : `https://${publicUrl}`, + ); + + const customHeaders = [{ key: VNDA_HOST_HEADER, value: url.hostname }]; + + const [include, routes] = generateDecoSiteMap + ? [ + [...(includeSiteMap ?? []), decoSiteMapUrl], + [{ + pathTemplate: decoSiteMapUrl, + handler: { + value: { + excludePaths: excludePathsFromDecoSiteMap, + __resolveType: "website/handlers/sitemap.ts", + }, + }, + }], + ] + : [includeSiteMap, []]; + + const internalDomainPaths = [ + ...PAGE_PATHS, + ...pagesToProxy, + ].map(( + pathTemplate, + ) => ({ + pathTemplate, + handler: { + value: { + __resolveType: "website/handlers/proxy.ts", + avoidAppendPath: pathTemplate === "/sitemap/vnda.xml", + url: pathTemplate === "/sitemap/vnda.xml" + ? `https://sitemap.vnda.com.br/preview/${publicUrl}` + : internalDomain, + host: url.hostname, + customHeaders, + }, + }, + })); + + const siteMap = { + pathTemplate: "/sitemap.xml", + handler: { + value: { + include, + __resolveType: "vnda/handlers/sitemap.ts", + customHeaders, + }, + }, + }; + + const apiDomainPaths = API_PATHS.map((pathTemplate) => ({ + pathTemplate, + handler: { + value: { + __resolveType: "website/handlers/proxy.ts", + url: `https://api.vnda.com.br/`, + host: url.hostname, + customHeaders, + }, + }, + })); + + return [...routes, ...internalDomainPaths, siteMap, ...apiDomainPaths]; +} + +export default loader; diff --git a/vnda/logo.png b/vnda/logo.png new file mode 100644 index 000000000..f78f44c57 Binary files /dev/null and b/vnda/logo.png differ diff --git a/vnda/manifest.gen.ts b/vnda/manifest.gen.ts index 64ef66c48..cd5093019 100644 --- a/vnda/manifest.gen.ts +++ b/vnda/manifest.gen.ts @@ -2,33 +2,48 @@ // This file SHOULD be checked into source version control. // This file is automatically updated during development when running `dev.ts`. -import * as $$$0 from "./loaders/productList.ts"; -import * as $$$1 from "./loaders/productDetailsPage.ts"; -import * as $$$2 from "./loaders/productListingPage.ts"; -import * as $$$3 from "./loaders/cart.ts"; -import * as $$$$$$$$$0 from "./actions/cart/updateItem.ts"; -import * as $$$$$$$$$1 from "./actions/cart/setShippingAddress.ts"; -import * as $$$$$$$$$2 from "./actions/cart/addItem.ts"; -import * as $$$$$$$$$3 from "./actions/cart/updateCoupon.ts"; -import { AppManifest } from "$live/types.ts"; +import * as $$$$$$$$$0 from "./actions/cart/addItem.ts"; +import * as $$$$$$$$$1 from "./actions/cart/addItems.ts"; +import * as $$$$$$$$$2 from "./actions/cart/simulation.ts"; +import * as $$$$$$$$$3 from "./actions/cart/updateCart.ts"; +import * as $$$$$$$$$4 from "./actions/cart/updateItem.ts"; +import * as $$$$$$$$$5 from "./actions/notifyme.ts"; +import * as $$$$0 from "./handlers/sitemap.ts"; +import * as $$$0 from "./loaders/cart.ts"; +import * as $$$1 from "./loaders/extensions/price/list.ts"; +import * as $$$2 from "./loaders/extensions/price/listingPage.ts"; +import * as $$$3 from "./loaders/productDetailsPage.ts"; +import * as $$$4 from "./loaders/productDetailsPageVideo.ts"; +import * as $$$5 from "./loaders/productList.ts"; +import * as $$$6 from "./loaders/productListingPage.ts"; +import * as $$$7 from "./loaders/proxy.ts"; const manifest = { "loaders": { - "apps/vnda/loaders/cart.ts": $$$3, - "apps/vnda/loaders/productDetailsPage.ts": $$$1, - "apps/vnda/loaders/productList.ts": $$$0, - "apps/vnda/loaders/productListingPage.ts": $$$2, + "vnda/loaders/cart.ts": $$$0, + "vnda/loaders/extensions/price/list.ts": $$$1, + "vnda/loaders/extensions/price/listingPage.ts": $$$2, + "vnda/loaders/productDetailsPage.ts": $$$3, + "vnda/loaders/productDetailsPageVideo.ts": $$$4, + "vnda/loaders/productList.ts": $$$5, + "vnda/loaders/productListingPage.ts": $$$6, + "vnda/loaders/proxy.ts": $$$7, + }, + "handlers": { + "vnda/handlers/sitemap.ts": $$$$0, }, "actions": { - "apps/vnda/actions/cart/addItem.ts": $$$$$$$$$2, - "apps/vnda/actions/cart/setShippingAddress.ts": $$$$$$$$$1, - "apps/vnda/actions/cart/updateCoupon.ts": $$$$$$$$$3, - "apps/vnda/actions/cart/updateItem.ts": $$$$$$$$$0, + "vnda/actions/cart/addItem.ts": $$$$$$$$$0, + "vnda/actions/cart/addItems.ts": $$$$$$$$$1, + "vnda/actions/cart/simulation.ts": $$$$$$$$$2, + "vnda/actions/cart/updateCart.ts": $$$$$$$$$3, + "vnda/actions/cart/updateItem.ts": $$$$$$$$$4, + "vnda/actions/notifyme.ts": $$$$$$$$$5, }, + "name": "vnda", + "baseUrl": import.meta.url, }; export type Manifest = typeof manifest; -export const name = "apps/vnda"; - -export default manifest satisfies AppManifest; +export default manifest; diff --git a/vnda/middleware.ts b/vnda/middleware.ts new file mode 100644 index 000000000..7d6f72b19 --- /dev/null +++ b/vnda/middleware.ts @@ -0,0 +1,29 @@ +import { AppMiddlewareContext } from "./mod.ts"; +import { equal } from "std/testing/asserts.ts"; +import { + buildSegmentCookie, + getSegmentFromBag, + getSegmentFromCookie, + setSegmentCookie, + setSegmentInBag, +} from "./utils/segment.ts"; + +export const middleware = ( + _props: unknown, + req: Request, + ctx: AppMiddlewareContext, +) => { + const segment = getSegmentFromBag(ctx); + if (!segment) { + const segmentFromRequest = buildSegmentCookie(req); + const segmentFromCookie = getSegmentFromCookie(req); + if ( + segmentFromRequest !== null && + !equal(segmentFromRequest, segmentFromCookie) + ) { + setSegmentInBag(ctx, segmentFromRequest); + setSegmentCookie(segmentFromRequest, ctx.response.headers); + } + } + return ctx.next!(); +}; diff --git a/vnda/mod.ts b/vnda/mod.ts index 1100ee3a1..ad4285a1a 100644 --- a/vnda/mod.ts +++ b/vnda/mod.ts @@ -1,9 +1,18 @@ -import type { App, FnContext } from "$live/mod.ts"; -import manifest, { Manifest, name } from "./manifest.gen.ts"; -import { createClient } from "./utils/client/client.ts"; - +import { Markdown } from "../decohub/components/Markdown.tsx"; +import { createHttpClient } from "../utils/http.ts"; +import { PreviewContainer } from "../utils/preview.tsx"; +import type { Secret } from "../website/loaders/secret.ts"; +import manifest, { Manifest } from "./manifest.gen.ts"; +import { middleware } from "./middleware.ts"; +import { OpenAPI } from "./utils/openapi/vnda.openapi.gen.ts"; +import { + type App, + type AppMiddlewareContext as AMC, + type FnContext, +} from "@deco/deco"; +export type AppMiddlewareContext = AMC>; export type AppContext = FnContext; - +/** @title VNDA */ export interface Props { /** * @title VNDA Account name @@ -11,40 +20,82 @@ export interface Props { * @default deco */ account: string; - /** * @title Public store URL * @description Domain that is registered on VNDA * @default www.mystore.com.br */ publicUrl: string; - /** * @description The token generated from admin panel. Read here: https://developers.vnda.com.br/docs/chave-de-acesso-e-requisicoes. Do not add any other permissions than catalog. */ - authToken: string; - + authToken: Secret; /** * @title Use Sandbox * @description Define if sandbox environment should be used */ sandbox: boolean; + /** + * @description Use VNDA as backend platform + * @hide true + */ + platform: "vnda"; + /** @description Here is to put the pathname of the Search Page. Ex: /s. We have default values: "/busca" or "/s" */ + searchPagePath?: string; } - export interface State extends Props { - client: ReturnType; + api: ReturnType>; } - +export const color = 0x0C29D0; /** * @title VNDA + * @description Loaders, actions and workflows for adding VNDA Commerce Platform to your website. + * @category Ecommmerce + * @logo https://raw.githubusercontent.com/deco-cx/apps/main/vnda/logo.png */ -export default function App(props: Props): App { +export default function VNDA(props: Props): App { + const { authToken, publicUrl, sandbox } = props; + const stringAuthToken = typeof authToken === "string" + ? authToken + : authToken?.get?.() ?? ""; + const api = createHttpClient({ + headers: new Headers({ + "User-Agent": "decocx/1.0", + "X-Shop-Host": publicUrl, + "accept": "application/json", + authorization: `Bearer ${stringAuthToken}`, + }), + base: sandbox + ? "https://api.sandbox.vnda.com.br" + : "https://api.vnda.com.br", + }); return { - name, - state: { - ...props, - client: createClient(props), - }, + state: { ...props, api }, manifest, + middleware, }; } +export const preview = async () => { + const markdownContent = await Markdown( + new URL("./README.md", import.meta.url).href, + ); + return { + Component: PreviewContainer, + props: { + name: "VNDA", + owner: "deco.cx", + description: + "Loaders, actions and workflows for adding VNDA Commerce Platform to your website.", + logo: "https://raw.githubusercontent.com/deco-cx/apps/main/vnda/logo.png", + images: [ + "https://deco-sites-assets.s3.sa-east-1.amazonaws.com/starting/8deab172-eca8-45dd-85f9-c44f66b1cfb1/Hub_de_Integracao_Tiny_91206b57b3_94dac840e3.webp", + ], + tabs: [ + { + title: "About", + content: markdownContent(), + }, + ], + }, + }; +}; diff --git a/vnda/runtime.ts b/vnda/runtime.ts index c6d780f6f..da42a2435 100644 --- a/vnda/runtime.ts +++ b/vnda/runtime.ts @@ -1,4 +1,3 @@ -import { forApp } from "$live/clients/withManifest.ts"; -import app from "./mod.ts"; - -export const Runtime = forApp>(); +import { Manifest } from "./manifest.gen.ts"; +import { proxy } from "@deco/deco/web"; +export const invoke = proxy(); diff --git a/vnda/utils/cart.ts b/vnda/utils/cart.ts new file mode 100644 index 000000000..3364e552d --- /dev/null +++ b/vnda/utils/cart.ts @@ -0,0 +1,29 @@ +import { getCookies, setCookie } from "std/http/cookie.ts"; +import { SEGMENT_COOKIE_NAME } from "./segment.ts"; + +const CART_COOKIE = "vnda_cart_id"; + +const ONE_WEEK_MS = 7 * 24 * 3600 * 1_000; + +export const getCartCookie = (headers: Headers): string | undefined => { + const cookies = getCookies(headers); + + return cookies[CART_COOKIE]; +}; + +export const getAgentCookie = (headers: Headers): string | undefined => { + const cookies = getCookies(headers); + + return cookies[SEGMENT_COOKIE_NAME]; +}; + +export const setCartCookie = (headers: Headers, cartId: string) => + setCookie(headers, { + name: CART_COOKIE, + value: cartId, + path: "/", + expires: new Date(Date.now() + ONE_WEEK_MS), + httpOnly: true, + secure: true, + sameSite: "Lax", + }); diff --git a/vnda/utils/client/client.ts b/vnda/utils/client/client.ts index 67a3a0206..85e8c2853 100644 --- a/vnda/utils/client/client.ts +++ b/vnda/utils/client/client.ts @@ -1,235 +1,129 @@ -import { getSetCookies } from "std/http/cookie.ts"; -import { FetchOptions, fetchSafe } from "../../../utils/fetch.ts"; -import { HttpError } from "../../../utils/HttpError.ts"; -import { Props } from "../../mod.ts"; -import { paramsToQueryString } from "../queryBuilder.ts"; import { - Banner, - Coupon, + Item, OrderForm, ProductGroup, - ProductSearchParams, + ProductPrice, ProductSearchResult, - RelatedItem, RelatedItemTag, SEO, - Shipping, + ShippingMethod, + Sort, TagsSearchParams, } from "./types.ts"; -export const createClient = (state: Props) => { - const { publicUrl, sandbox, authToken } = state; - const publicEndpoint = `https://${publicUrl}`; // TODO: Remove this and use only api endpoints - const baseUrl = sandbox - ? "https://api.sandbox.vnda.com.br" - : "https://api.vnda.com.br"; - - const fetcher = (path: string, init?: RequestInit & FetchOptions) => - fetchSafe(new URL(path, baseUrl), { - ...init, - headers: { - "User-Agent": "decocx/1.0", - "X-Shop-Host": publicUrl, - "accept": "application/json", - authorization: `Bearer ${authToken}`, - ...init?.headers, - }, - }); - - const getProduct = (id: string | number): Promise => - fetcher(`/api/v2/products/${id}`, { withProxyCache: true }) - .then((res) => res.json()) - .catch(() => null); - - const searchProduct = async ( - params: ProductSearchParams, - ): Promise => { - const { type_tags, ...knownParams } = params; - const typeTagsEntries = type_tags?.map((tag) => [tag.key, tag.value]) ?? []; - - const qs = paramsToQueryString({ - ...knownParams, - ...Object.fromEntries(typeTagsEntries), - }); - - const response = await fetcher(`/api/v2/products/search?${qs}`, { - withProxyCache: true, - }); +export interface API { + /** @docs https://developers.vnda.com.br/reference/get-api-v2-products-id */ + "GET /api/v2/products/:id": { + response: ProductGroup; + searchParams: { include_images: boolean }; + }; - const data = await response.json(); - const pagination = response.headers.get("x-pagination"); + /** @docs https://developers.vnda.com.br/reference/get-api-v2-products-product_id-price */ + "GET /api/v2/products/:productId/price": { + response: ProductPrice; + searchParams: { coupon_codes?: string[] }; + }; - return { - ...data, - pagination: pagination ? JSON.parse(pagination) : { - total_pages: 0, - total_count: 0, - current_page: params.page, - prev_page: false, - next_page: false, - }, + /** @docs https://developers.vnda.com.br/reference/get-api-v2-banners */ + "GET /api/v2/banners": { + searchParams: { + only_valid: boolean; + tag: "listagem-banner-principal"; }; }; - const getDefaultBanner = (): Promise => - fetcher( - `/api/v2/banners?only_valid=true&tag=listagem-banner-principal`, - { withProxyCache: true }, - ).then((res) => res.json()); - - const getSEO = (type: "Product" | "Page" | "Tag") => - ( - resourceId: string | number, - ): Promise => { - const qs = new URLSearchParams(); - qs.set("resource_type", type); - if (type !== "Tag") qs.set("resource_id", `${resourceId}`); - if (type === "Tag") qs.set(`code`, `${resourceId}`); - qs.set("type", "category"); - - return fetcher(`/api/v2/seo_data?${qs.toString()}`, { - withProxyCache: true, - }).then((res) => res.json()); + /** @docs https://developers.vnda.com.br/reference/get-api-v2-tags-name */ + "GET /api/v2/tags/:name": { + response: RelatedItemTag; }; - const getProductSEO = getSEO("Product"); - const getPageSEO = getSEO("Page"); - const getTagSEO = getSEO("Tag"); - - const getTag = (name: string): Promise => - fetcher(`/api/v2/tags/${name}`, { withProxyCache: true }) - .then((res) => res.json()); - - const getTags = (params?: TagsSearchParams): Promise => { - const qs = new URLSearchParams(); - Object.entries(params ?? {}).forEach(([key, value]) => { - qs.set(key, value); - }); - - return fetcher(`/api/v2/tags?${qs.toString()}`, { withProxyCache: true }) - .then((res) => res.json()); + /** @docs https://developers.vnda.com.br/reference/get-api-v2-tags */ + "GET /api/v2/tags": { + response: RelatedItemTag[]; + searchParams: TagsSearchParams; }; - const getCarrinho = (cookie: string): Promise => - fetcher(new URL("/carrinho", publicEndpoint).href, { - headers: { cookie }, - }).then((res) => res.json()); - - const relatedItems = (cookie: string): Promise => - fetcher( - new URL( - "/carrinho/produtos-sugeridos/relacionados-carrinho", - publicEndpoint, - ).href, - { headers: { cookie } }, - ).then((res) => res.json()).catch((error) => { - if (error instanceof HttpError && error.status === 404) { - return []; - } - - throw error; - }); + "GET /api/v2/seo_data": { + response: SEO[]; + searchParams: { + resource_type: "Product" | "Page"; + resource_id: string | number; + type: "category"; + } | { + resource_type: "Tag"; + code: string; + type: "category"; + }; + }; - const adicionar = async ({ - cookie, - sku, - quantity, - attributes, - }: { - cookie: string; - sku: string; - quantity: number; - attributes: Record; - }) => { - const form = new FormData(); - form.set("sku", sku); - form.set("quantity", `${quantity}`); + /** @docs https://developers.vnda.com.br/reference/get-api-v2-products-search */ + "GET /api/v2/products/search": { + response: Omit; + searchParams: { + term?: string | undefined; + page?: number; + "tags[]"?: string[]; + sort?: Sort; + per_page?: number; + wildcard?: boolean; + type_tags_operator?: string; + } & { [x: string]: unknown | unknown[] }; + }; - Object.entries(attributes).forEach(([name, value]) => - form.set(`attribute-${name}`, value) - ); + /** @docs https://developers.vnda.com.br/reference/get-api-v2-carts-id */ + "GET /api/v2/carts/:cartId": { + response: OrderForm; + }; - const response = await fetcher( - new URL("/carrinho/adicionar", publicEndpoint).href, - { - method: "POST", - body: form, - headers: { cookie }, - }, - ); + /** @docs https://developers.vnda.com.br/reference/post-api-v2-carts */ + "POST /api/v2/carts": { + response: OrderForm; + }; - return { - orderForm: await response.json() as OrderForm, - cookies: getSetCookies(response.headers), + /** @docs https://developers.vnda.com.br/reference/get-api-v2-carts-id */ + "PATCH /api/v2/carts/:cartId": { + response: OrderForm; + body: { + agent?: string; + zip?: string; + client_id?: number; + coupon_code?: string; + rebate_token?: string; }; }; - const cep = (zip: string, cookie: string): Promise => { - const form = new FormData(); - form.set("zip", zip); - - return fetcher(new URL("/cep", publicEndpoint).href, { - method: "POST", - body: form, - headers: { cookie }, - }) - .then((res) => res.json()); + /** @docs https://developers.vnda.com.br/reference/post-api-v2-carts-cart_id-items */ + "POST /api/v2/carts/:cartId/items": { + response: Item; + body: { + sku: string; + quantity: number; + place_id?: number; + store_coupon_code?: string; + customizations?: Record; + extra?: Record; + }; }; - const coupon = (code: string, cookie: string): Promise => { - const form = new FormData(); - form.set("code", code); - - return fetcher(new URL("/cupom/ajax", publicEndpoint).href, { - method: "POST", - body: form, - headers: { cookie }, - }).then((res) => res.json()); + /** @docs https://developers.vnda.com.br/reference/patch-api-v2-carts-cart_id-items-id */ + "PATCH /api/v2/carts/:cartId/items/:itemId": { + response: Item; + body: { + sku?: string; + quantity: number; + place_id?: number; + store_coupon_code?: string; + customizations?: Record; + extra?: Record; + }; }; - const atualizar = ( - { item_id, quantity }: { item_id: string | number; quantity: number }, - cookie: string, - ): Promise => - fetcher(new URL("/carrinho/quantidade/atualizar", publicEndpoint).href, { - method: "POST", - body: JSON.stringify({ item_id, quantity }), - headers: { cookie }, - }).then((res) => res.json()); - - const remover = ( - item_id: string | number, - cookie: string, - ): Promise => - fetcher(new URL("/carrinho/remover", publicEndpoint).href, { - method: "POST", - body: JSON.stringify({ item_id }), - headers: { cookie }, - }).then((res) => res.json()); + /** @docs https://developers.vnda.com.br/reference/delete-api-v2-carts-cart_id-items-id */ + "DELETE /api/v2/carts/:cartId/items/:itemId": undefined; - return { - product: { - search: searchProduct, - get: getProduct, - }, - banners: { - default: getDefaultBanner, - }, - seo: { - product: getProductSEO, - page: getPageSEO, - tag: getTagSEO, - }, - tag: getTag, - tags: getTags, - carrinho: { - get: getCarrinho, - relatedItems, - adicionar, - atualizar, - remover, - }, - cep, - coupon, + /** @docs https://developers.vnda.com.br/reference/get-api-v2-variants-variant_sku-shipping_methods */ + "GET /api/v2/variants/:sku/shipping_methods": { + response: ShippingMethod[]; + searchParams: { quantity: number; zip: string }; }; -}; +} diff --git a/vnda/utils/client/types.ts b/vnda/utils/client/types.ts index ca2a6cb64..5accc3d1a 100644 --- a/vnda/utils/client/types.ts +++ b/vnda/utils/client/types.ts @@ -1,3 +1,5 @@ +import { ProductPriceVariant } from "../openapi/vnda.openapi.gen.ts"; + export type Sort = "newest" | "oldest" | "lowest_price" | "highest_price"; export interface ProductSearchResult { @@ -50,6 +52,12 @@ export type ProductGroup = Partial<{ }; attributes: Record; tags: RelatedItemTag[]; + images?: { + id: number; + updated_at: string; + url: string; + variant_ids: unknown[]; + }[]; }>; export interface Attribute { @@ -106,8 +114,6 @@ export interface Property { export interface Cart { orderForm?: OrderForm; relatedItems?: RelatedItem[]; - shipping?: Shipping; - coupon?: Coupon; } export interface Installment { @@ -148,7 +154,7 @@ export interface OrderForm { channel: string; client_id: null; code: string; - coupon_code: null; + coupon_code: string | null; discount: null; discount_price: number; extra: Record; @@ -209,11 +215,15 @@ export interface ShippingMethod { description: string; name: string; price: number; - shipping_method_id: number; + shipping_method_id: number | null; value: string; - countries: string[] | null; + countries: { + country?: string; + price?: string; + }[] | null; fulfillment_company: string | null; value_needed_to_discount: number | null; + notice: string | null; } export interface Address { @@ -370,9 +380,22 @@ export interface Banner { export interface SEO { id: number; + name?: string; title?: string; description?: string | null; resource_type: string; resource_id: number; parent_id: null | number; } + +export interface ProductPrice { + available: boolean; + on_sale: boolean; + price: number; + sale_price: number; + intl_price: number; + discount_rule?: unknown; + updated_at: string; + installments: Installment[]; + variants: ProductPriceVariant[]; +} diff --git a/vnda/utils/constants.ts b/vnda/utils/constants.ts new file mode 100644 index 000000000..e69de29bb diff --git a/vnda/utils/openapi/vnda.openapi.gen.ts b/vnda/utils/openapi/vnda.openapi.gen.ts new file mode 100644 index 000000000..6bdf30999 --- /dev/null +++ b/vnda/utils/openapi/vnda.openapi.gen.ts @@ -0,0 +1,5412 @@ + +// deno-fmt-ignore-file +// deno-lint-ignore-file no-explicit-any ban-types ban-unused-ignore +// +// DO NOT EDIT. This file is generated by deco. +// This file SHOULD be checked into source version control. +// To generate this file: deno task start +// + + +export interface OpenAPI { +"GET /api/v2/products/:productId/videos": { +response: { +id?: number +url?: string +embed_url?: string +thumbnail_url?: string +updated_at?: number +variant_ids?: number +}[] +} +"GET /api/v2/seo_data": { +searchParams: { +resource_type?: string +resource_id?: number +type?: string +code?: string +} +response: { +id: number +title?: string +description?: string +resource_type: string +resource_id: number +parent_id: number +}[] +} +/** + * Permite autorizar operações usando o access_token e a senha do usuário + */ +"POST /api/v2/users/authorize": { +body: { +access_token: string +password: string +} +} +/** + * Realiza o login do usuário a partir do email e da senha + */ +"POST /api/v2/users/login": { +body: { +email: string +password: string +} +response: User +} +/** + * Realiza o logout do usuário a partir do access_token do mesmo + */ +"POST /api/v2/users/logout": { +body: { +/** + * Token de validação de usuário logado + * + * O `access_token` é gerado quando o usuário loga no Admin + */ +access_token?: string +} +} +/** + * Retorna os dados de um usuário pelo seu ID + */ +"GET /api/v2/users/:id": { +response: User +} +/** + * Atualiza um usuário + */ +"PUT /api/v2/users/:id": { +body: { +email?: string +name?: string +role_name?: ("Agente" | "Gestor" | "Local") +password?: string +password_confirmation?: string +external_code?: string +phone_area?: string +phone?: string +tags?: string[] +} +} +/** + * Retorna as versões da regra de bônus cadastrada + */ +"GET /api/v2/credits/rules/versions": { +response: { +event?: string +author?: string +created_at?: string +ip?: string +user_agent?: string +cart_id?: string +object_changes?: string +} +} +/** + * Retorna as regras de bônus cadastradas + */ +"GET /api/v2/credits/rules": { +response: { +active: boolean +minimum_subtotal: number +bonus: number +delayed_for: number +valid_for: number +maximum_usage_factor: number +} +} +/** + * Permite atualizar as regras de bônus + */ +"PUT /api/v2/credits/rules": { +body: { +/** + * Percentual em cima do total do pedido que vai ser dado de bônus para o cliente + */ +bonus: number +/** + * Número de dias em que o crédito começa a valer + */ +valid_in: number +/** + * Número de dias para a expiração do crédito + */ +valid_for: number +/** + * Valor mínimo do pedido para que o bônus possa ser transferido para o cliente + */ +minimum_subtotal?: number +/** + * Percentual do subtotal do pedido que pode ser pago com o bônus + */ +maximum_usage_factor?: number +} +} +/** + * Permite remover as regras de bônus, desativando o recurso + */ +"DELETE /api/v2/credits/rules": { + +} +/** + * Retorna os dados de um pedido usando o `code` ou `token` + */ +"GET /api/v2/orders/:code": { +searchParams: { +/** + * Retorna as formas de entrega do pedido + */ +include_shipping_address?: boolean +} +response: Order +} +/** + * Retorna uma lista de pedidos + */ +"GET /api/v2/orders": { +searchParams: { +/** + * Retorna os resultados a partir desta data, no formato 'yyyy-mm-dd' + */ +start?: string +/** + * Retorna os resultados até esta data, no formato 'yyyy-mm-dd' + */ +finish?: string +/** + * Se "true" retorna somente os pedidos que tenham nota fiscal. Se "false" retorna somente os pedidos que não tenham nota fiscal + */ +invoiced?: boolean +/** + * Número da página atual. Os dados de paginação estarão disponíveis, em formato JSON, no header X-Pagination no response da API, caso exista paginação + */ +page?: number +/** + * Número máximo de registros que deve ser retornado por página + */ +per_page?: number +/** + * Array com os códigos de cupons + */ +coupon_codes?: string[] +/** + * Se "true" inclui o preço dos produtos customizados no total do pedido. Se "false" retorna o total do pedido sem a somatória do preço de produtos customizados. + */ +include_customizations_in_total?: boolean +} +response: Order[] +} +/** + * Faz a captura do pagamento no adquirente + * Apenas para pedidos pagos com cartão de crédito + */ +"POST /api/v2/orders/:code/capture": { +response: { + +} +} +/** + * Altera o status do pedido para "confirmado" + */ +"POST /api/v2/orders/:code/confirm": { +body: { +/** + * Para cartão de crédito deve ser enviado OBRIGATORIAMENTE o retorno da requisição para "/api/v2/orders/{code}/capture" + */ +confirmation_data?: string +} +} +/** + * Faz o estorno do pagamento no adquirente + * Apenas para pedidos pagos com cartão de crédito + */ +"POST /api/v2/orders/:code/chargeback": { + +} +/** + * Altera o status do pedido para "cancelado" + */ +"POST /api/v2/orders/:code/cancel": { +body: { +/** + * Deve ser enviado algo que comprove que o pagamento foi devolvido. + * Para cartão de crédito deve ser enviado OBRIGATORIAMENTE o retorno da requisição para "/api/v2/orders/{code}/chargeback" + */ +cancelation_data?: string +} +response: { + +} +} +/** + * Altera o status do pacote para "enviado" + */ +"PATCH /api/v2/orders/:orderCode/packages/:packageCode/ship": { + +} +/** + * Altera o pacote para "entregue" + */ +"PATCH /api/v2/orders/:orderCode/packages/:packageCode/deliver": { + +} +/** + * Recebe uma lista JSON com os SKUs que devem ser atualizados. A atualização será executada em segundo plano em aproximadamente 1 minuto + */ +"POST /api/v2/variants/quantity": { +body: { +sku: string +quantity: number +/** + * Informe somente para atualizar o estoque de um local específico + */ +place_id?: number +}[] +} +/** + * Atualiza o estoque de uma variante de um produto + */ +"POST /api/v2/variants/:sku/quantity": { +searchParams: { +quantity: number +} +response: { +status?: string +} +} +/** + * Atualiza o estoque específico de um local + */ +"PATCH /api/v2/variants/:sku/inventories/:placeId": { +searchParams: { +quantity?: number +} +response: { +status?: string +} +} +/** + * Permite listar as variantes de um produto + */ +"GET /api/v2/products/:productId/variants": { +response: Variant[] +} +/** + * Permite criar uma variante + */ +"POST /api/v2/products/:productId/variants": { +body: { +sku: string +name?: string +quantity: number +main?: boolean +/** + * Largura do produto, em centímetros + */ +width?: number +/** + * Altura do produto, em centímetros + */ +height?: number +/** + * Comprimento do produito, em centímetros + */ +length?: number +/** + * Massa do produto, em gramas + */ +weight?: number +/** + * Dias de manuseio da variante + */ +handling_days?: number +price: number +/** + * Customização da variante + */ +custom_attributes?: { + +} +min_quantity?: number +norder?: number +property1?: string +property2?: string +property3?: string +barcode?: string +} +response: { +id?: number +main?: boolean +available?: boolean +sku?: string +name?: string +slug?: string +min_quantity?: number +quantity?: number +/** + * Quantidade de itens disponíveis + */ +stock?: number +/** + * Customização da variante + */ +custom_attributes?: { + +} +properties?: { + +} +/** + * Data e horário da última atualização + */ +updated_at?: string +price?: number +installments?: number[] +available_quantity?: number +/** + * Massa do produto, em gramas + */ +weight?: number +/** + * Largura do produto, em centímetros + */ +width?: number +/** + * Altura do produto, em centímetros + */ +height?: number +/** + * Comprimento do produito, em centímetros + */ +length?: number +/** + * Dias de manuseio da variante + */ +handling_days?: number +inventories?: VariantInventory[] +sale_price?: number +image_url?: string +product_id?: number +norder?: number +} +} +/** + * Permite remover uma variante + */ +"DELETE /api/v2/products/:productId/variants/:id": { + +} +/** + * @deprecated + * Permite atualizar uma variante + */ +"PATCH /api/v2/products/:productId/variants/:id": { +body: { +sku: string +name?: string +quantity: number +main?: boolean +width?: number +height?: number +length?: number +weight?: number +handling_days?: number +price: number +custom_attributes?: { + +} +min_quantity?: number +norder?: number +property1?: string +property2?: string +property3?: string +barcode?: string +quantity_sold?: number +} +} +/** + * Permite determinar a ordem das variantes dentro de cada produto + */ +"POST /api/v2/variants/reorder": { +body: { +/** + * A ordem dos elementos será replicada para as variantes + */ +ids: number[] +} +} +/** + * Retorna um template usando o path dele + */ +"GET /api/v2/templates/:path": { +response: Template +} +/** + * Remove um template usando o path dele + */ +"DELETE /api/v2/templates/:path": { + +} +/** + * Atualiza o conteúdo de um template usando o path dele + */ +"PATCH /api/v2/templates/:path": { +body: { +body?: string +} +} +/** + * Retorna uma lista de templates + */ +"GET /api/v2/templates": { +response: Template[] +} +/** + * Cria um novo template + */ +"POST /api/v2/templates": { +body: { +path: string +body?: string +} +response: Template +} +/** + * Reativa um usuário que estiver desativado + */ +"POST /api/v2/users/:id/activate": { + +} +/** + * Desativa um usuário + */ +"POST /api/v2/users/:id/deactivate": { + +} +/** + * Lista os usuários + */ +"GET /api/v2/users": { +searchParams: { +/** + * Incluir usuários desativados? + */ +include_inactive?: boolean +/** + * Incluir todas as imagens dos produtos? + */ +include_images?: boolean +/** + * Exibe somente os usuários com o código externo indicado + */ +external_code?: string +/** + * Exibe somente os usuários com a função indicada + */ +role_name?: ("Agente" | "Gestor" | "Local") +/** + * Filtra usuários que contenham o valor indicado no nome, telefone, email ou código externo + */ +term?: string +} +response: User1[] +} +/** + * Cria um usuário + */ +"POST /api/v2/users": { +body: { +email?: string +name?: string +role_name?: ("Agente" | "Gestor" | "Local") +password?: string +password_confirmation?: string +external_code?: string +phone_area?: string +phone?: string +tags?: string[] +} +response: User +} +/** + * Lista os usuários + */ +"GET /api/v2/users/tags": { +searchParams: { +/** + * Exibe somente os usuários com a função indicada + */ +role_names?: ("Agente" | "Gestor" | "Local" | "Agente Social Selling") +} +response: { +tags?: string[] +} +} +/** + * Retorna a lista de carrinhos ativos nos últimos 60 dias + */ +"GET /api/v2/carts": { +searchParams: { +/** + * Número da página + */ +page?: number +/** + * Quantidade de produtos por página + */ +per_page?: number +/** + * Inclui os carrinhos sem telefone (não enviar o campo para não incluir) + */ +without_phones?: boolean +/** + * Filtra os carrinhos que possuem tentativa de pagamento + */ +with_payments?: boolean +} +response: Cart[] +} +/** + * Permite criar um carrinho + */ +"POST /api/v2/carts": { +body: ParametrosDeCarrinhoResumido +response: Cart1 +} +/** + * Permite retornar um carrinho + */ +"GET /api/v2/carts/:id": { +response: Cart1 +} +/** + * Permite criar um carrinho + */ +"POST /api/v2/carts/:id": { +body: { +agent?: string +zip?: string +client_id?: number +coupon_code?: string +/** + * DEPRECATED: enviar o `client_id` + */ +email?: string +rebate_token?: string +} +response: Cart1 +} +/** + * Permite excluir um carrinho + */ +"DELETE /api/v2/carts/:id": { + +} +/** + * Permite atualizar os atributos de um carrinho + */ +"PATCH /api/v2/carts/:id": { +body: { +agent?: string +zip?: string +client_id?: number +/** + * DEPRECATED: enviar o `client_id` + */ +email?: string +rebate_token?: string +} +} +/** + * Permite calcular as parcelas referentes ao total do carrinho + */ +"GET /api/v2/carts/:id/installments": { +response: CartInstallment1[] +} +/** + * Lista os locais + */ +"GET /api/v2/places": { +searchParams: { +/** + * Filtra os locais for nome + */ +names?: string[] +/** + * Filtra os locais que são/não são warehouse + */ +warehouse?: boolean +/** + * Filtra os locais que contenham determinada categoria + */ +category?: string +/** + * As lojas mais próximas da coordenada informada serão exibidas primeiro + */ +coordinates?: string +/** + * As lojas mais próximas do CEP informado serão exibidas primeiro + */ +origin_zip_code?: string +} +response: Place[] +} +/** + * Cria um local + */ +"POST /api/v2/places": { +body: { +name: string +address_line_1: string +address_line_2?: string +city: string +neighborhood?: string +zip?: string +home_page?: string +latitude?: number +longitude?: number +images?: string[] +description?: string +email: string +first_phone?: string +second_phone?: string +mobile_phone?: string +only_cash?: boolean +categories?: string[] +marker_url?: string +state?: string +opening_hours?: string +warehouse?: boolean +legal_name?: string +cnpj?: string +} +response: Place +} +/** + * Remove um local + */ +"DELETE /api/v2/places/:id": { + +} +/** + * Atualiza um local + */ +"PATCH /api/v2/places/:id": { +body: { +name: string +address_line_1: string +address_line_2?: string +city: string +neighborhood?: string +zip?: string +home_page?: string +latitude?: number +longitude?: number +images?: string[] +description?: string +email: string +first_phone?: string +second_phone?: string +mobile_phone?: string +only_cash?: boolean +categories?: string[] +marker_url?: string +state?: string +opening_hours?: string +warehouse?: boolean +legal_name?: string +cnpj?: string +} +} +/** + * Lista as notas fiscais + */ +"GET /api/v2/orders/:code/packages/:packageCode/invoices": { +response: Invoice[] +} +/** + * Cria uma nota fiscal + */ +"POST /api/v2/orders/:code/packages/:packageCode/invoices": { +body: { +number: number +series?: number +issued_at?: string +key?: string +volumes?: number +} +response: Invoice +} +/** + * Remove uma nota fiscal + */ +"DELETE /api/v2/orders/:code/packages/:packageCode/invoices/:number": { + +} +/** + * Atualiza uma nota fiscal + */ +"PATCH /api/v2/orders/:code/packages/:packageCode/invoices/:number": { +body: { +number: number +series?: number +issued_at?: string +key?: string +volumes?: number +} +} +/** + * Será enviado por email um link para o cadastro da nova senha + * O link tem validade de 24 horas + */ +"POST /api/v2/users/reset_password": { +body: { +email: string +} +} +/** + * Cadastra a nova senha + */ +"PATCH /api/v2/users/reset_password": { +body: { +/** + * Token pare renovação de senha enviado por email + */ +token: string +/** + * Nova senha para o usuário + */ +password: string +/** + * Confirmação da nova senha do usuário + */ +password_confirmation: string +} +} +/** + * Retorna o endereço de entrega + */ +"GET /api/v2/orders/:code/shipping_address": { +response: { +id?: number +first_name: string +last_name: string +company_name?: string +email: string +/** + * Serão retornados apenas os campos preenchidos + */ +documents?: { +cpf?: string +cnpj?: string +ie?: string +} +street_name: string +street_number: string +complement?: string +neighborhood: string +/** + * Somente números + */ +first_phone_area: string +/** + * Somente números + */ +first_phone: string +/** + * Somente números + */ +second_phone_area?: string +/** + * Somente números + */ +second_phone?: string +reference?: string +/** + * Somente números + */ +zip: string +city: string +state: string +recipient_name?: string +} +} +/** + * Lista os recebedores + */ +"GET /api/v2/payment_recipients": { +response: PaymentRecipient[] +} +/** + * Cria um recebedor + */ +"POST /api/v2/payment_recipients": { +body: { +tag_id?: number +recipient_id?: number +percentage: number +active?: boolean +charge_processing_fee?: boolean +liable?: boolean +code?: string +place_id?: number +user_id?: number +/** + * Indica se o frete deve ser incluído no split do pagamento + */ +include_shipping?: boolean +} +response: PaymentRecipient +} +/** + * Retorna um recebedor + */ +"GET /api/v2/payment_recipients/:id": { +response: PaymentRecipient +} +/** + * Remove um recebedor + */ +"DELETE /api/v2/payment_recipients/:id": { + +} +/** + * Atualiza um recebedor + */ +"PATCH /api/v2/payment_recipients/:id": { +body: { +tag_id?: number +recipient_id?: number +percentage?: number +active?: boolean +charge_processing_fee?: boolean +liable?: boolean +code?: string +place_id?: number +user_id?: number +/** + * Indica se o frete deve ser incluído no split do pagamento + */ +include_shipping?: boolean +} +} +/** + * Permite a listagem de recebíveis do usuário + */ +"GET /api/v2/users/:userId/payables": { +response: RecebiveisDoUsuario[] +} +/** + * Lista os membros da audiência + */ +"GET /api/v2/audience_members": { +response: AudienceMember[] +} +/** + * Permite criar um membro da audiência + */ +"POST /api/v2/audience_members": { +body: { +first_name?: (null | string) +last_name?: (null | string) +email: string +phone_area?: (null | string) +phone?: (null | string) +tags?: string[] +} +response: AudienceMember +} +/** + * Permite remover um membro da audiência + */ +"DELETE /api/v2/audience_members/:id": { + +} +/** + * Permite alterar um membro da audiência + */ +"PATCH /api/v2/audience_members/:id": { +body: { +first_name?: string +last_name?: string +email?: string +phone_area?: string +phone?: string +tags?: string[] +} +} +/** + * Lista os rastreios de um pacote de um pedido + */ +"GET /api/v2/orders/:orderCode/packages/:packageCode/trackings": { +response: { +id?: number +/** + * Código de rastreio do pacote + */ +tracking_code: string +/** + * Data e horário da última atualização do código de rastreio do pacote + */ +tracked_at?: string +/** + * URL para rastreio do pedido com a transportadora + */ +url?: string +/** + * Transportadora do pacote + */ +company?: string +} +} +/** + * Adiciona um rastreio para um pacote de um pedido + */ +"POST /api/v2/orders/:orderCode/packages/:packageCode/trackings": { +body: { +/** + * Código de rastreio + */ +code: string +/** + * Transportadora + */ +company?: string +/** + * Link de rastreamento + */ +url?: string +} +response: { +/** + * Código de rastreio do pacote + */ +code: string +/** + * Transportadora + */ +company?: string +/** + * URL para rastreio do pacote na transportadora + */ +url?: string +} +} +/** + * Remove um rastreio + */ +"DELETE /api/v2/orders/:orderCode/packages/:packageCode/trackings/:id": { + +} +/** + * Lista os itens de um carrinho + */ +"GET /api/v2/carts/:cartId/items": { +response: CartItem[] +} +/** + * Permite criar um item do carrinho + */ +"POST /api/v2/carts/:cartId/items": { +body: Produto +response: CartItem +} +/** + * Remove um item do carrinho + */ +"DELETE /api/v2/carts/:cartId/items/:id": { + +} +/** + * Atualiza um item do carrinho + */ +"PATCH /api/v2/carts/:cartId/items/:id": { +body: { +quantity?: number +place_id?: number +extra?: { + +} +store_coupon_code?: string +} +} +/** + * Permite adicionar itens em bulk ao carrinho + */ +"POST /api/v2/carts/:cartId/items/bulk": { +body: { +/** + * Itens do carrinho + */ +items?: { +/** + * Código SKU da variante do produto + */ +sku: string +/** + * Unidades do produto + */ +quantity: number +/** + * [Personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) do produto + */ +customizations?: { +/** + * Adicione a customização de acordo com a [personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) incluídas no Admin da loja. + * Se por exemplo a customização do produto é a cor, o parâmetro para a requisição deve ser `Color` ao invés de `CUstomization`. + * Saiba mais sobre como utilizar esse parâmetro pelo exemplo de requsição localizado na seção de **Request Example** (ao lado do código da requisição). + */ +Customization?: string +}[] +}[] +minItems?: 0 +} +response: CartItem[] +} +/** + * Cria uma promoção + */ +"POST /api/v2/discounts": { +body: { +name: string +start_at: string +end_at?: string +valid_to?: ("store" | "cart") +description?: string +enabled?: boolean +email?: string +cpf?: string +tags?: string +} +response: Discount1 +} +/** + * Retorna uma promoção + */ +"GET /api/v2/discounts/:id": { +response: Discount1 +} +/** + * Remove uma promoção + */ +"DELETE /api/v2/discounts/:id": { + +} +/** + * Altera uma promoção + */ +"PATCH /api/v2/discounts/:id": { +body: { +name: string +start_at: string +end_at?: string +valid_to?: ("store" | "cart") +description?: string +enabled?: boolean +email?: string +cpf?: string +tags?: string +} +} +/** + * Lista as regras de desconto de uma promoção + */ +"GET /api/v2/discounts/:discountId/rules": { +response: DiscountRule[] +} +/** + * Cria uma regra de desconto + */ +"POST /api/v2/discounts/:discountId/rules": { +body: { +apply_to?: ("product" | "tag" | "subtotal" | "total" | "shipping") +amount_type?: ("R$" | "%") +amount?: number +product_id?: number +tag_name?: string +min_quantity?: number +shipping_method?: string +min_subtotal?: number +gift?: boolean +combinated_product_id?: number +client_tag?: string +shipping_rule?: string +gift_quantity?: number +agent_tag?: string +regions?: string[] +channel?: string[] +} +response: { +id?: number +amount?: number +apply_to?: ("product" | "tag" | "subtotal" | "total" | "shipping") +min_quantity?: number +type?: string +channel?: string[] +} +} +/** + * Remove uma regra de desconto + */ +"DELETE /api/v2/discounts/:discountId/rules/:id": { + +} +/** + * Altera uma regra de desconto + */ +"PATCH /api/v2/discounts/:discountId/rules/:id": { +body: { +apply_to?: ("product" | "tag" | "subtotal" | "total" | "shipping") +amount_type?: ("R$" | "%") +amount?: number +product_id?: number +tag_id?: number +min_quantity?: number +shipping_method?: string +min_subtotal?: number +gift?: boolean +combinated_product_id?: number +client_tag?: string +shipping_rule?: string +gift_quantity?: number +agent_tag?: string +regions?: string[] +channel?: string[] +} +} +/** + * Permite listar os cupons de desconto de uma promoção + */ +"GET /api/v2/discounts/:discountId/coupons": { +searchParams: { +/** + * Filtra os cupons pelo campo uses_per_code + */ +uses_per_code?: number +} +response: Coupon[] +} +/** + * Cria um cupom de desconto + */ +"POST /api/v2/discounts/:discountId/coupons": { +body: { +code?: string +uses_per_code?: number +uses_per_user?: number +referrer_email?: string +quantity?: number +user_id?: number +} +response: Coupon +} +/** + * Remove um cupom de desconto + */ +"DELETE /api/v2/discounts/:discountId/coupons:id": { + +} +/** + * Atualiza um cupom de desconto + */ +"PATCH /api/v2/discounts/:discountId/coupons:id": { +body: { +/** + * Caso deseje um uso ilimitado do cupom, o valor desse campo deverá ser 0 + */ +uses_per_code?: number +/** + * Caso deseje um uso ilimitado do cupom, o valor desse campo deverá ser 0 + */ +uses_per_user?: number +} +} +/** + * Lista os produtos + */ +"GET /api/v2/products": { +searchParams: { +/** + * Delimita a quantidade de itens retornados + */ +limit?: number +/** + * Número da página + */ +page?: number +/** + * Quantidade de produtos por página + */ +per_page?: number +/** + * Filtra pela referência + */ +reference?: string +/** + * Filtra pelo ID dos produtos + */ +ids?: string[] +/** + * Filtra produtos que coném a tag + */ +tag?: string +/** + * Filtra produtos alterados depois da data + */ +updated_after?: string +/** + * Exibe os produtos cadastrados recentemente primeiro + */ +sort?: "newest" +/** + * Inclui os produtos inativos na listagem + */ +include_inactive?: boolean +/** + * Inclui na requisição se deseja que venham todas as imagens do produto + */ +include_images?: boolean +} +response: Product[] +} +/** + * Cria um produto + */ +"POST /api/v2/products": { +body: SimpleProduct +response: { +id?: number +/** + * Indica se o produto está ativo (`true`) ou inativo (`false`) + */ +active?: (boolean & string) +/** + * Código de Referência do produto + */ +reference: string +/** + * Nome do produto + */ +name: string +/** + * Descrição do produto + */ +description?: string +/** + * Lista de tags associadas ao produto + */ +tag_list?: string[] +slug?: string +url?: string +updated_at?: string +/** + * Tipo de produto, entre: + * - `sample`: amostra + * - `subscription`: assinatura + * - `product`: produto em geral + */ +product_type?: ("product" | "sample" | "subscription") +} +} +/** + * Retorna um produto + */ +"GET /api/v2/products/:id": { +searchParams: { +/** + * Lista de cupons para calcular o desconto do produto + */ +coupon_codes?: string[] +/** + * Se "true", inclui o nome do local nos inventários das variantes + */ +include_inventory_place?: string +/** + * Se "true", inclui todas as imagens do produto + */ +include_images?: string +} +response: Product +} +/** + * Remove um produto + */ +"DELETE /api/v2/products/:id": { + +} +/** + * Atualiza um produto + */ +"PATCH /api/v2/products/:id": { +body: { +name: string +description?: string +active?: boolean +reference: string +tag_list?: string +} +} +/** + * Permite atualizar um produto pela referência + */ +"PATCH /api/v2/products/reference/:reference": { +body: { +reference: string +name: string +description?: string +active?: boolean +product_type?: ("product" | "sample" | "subscription") +} +} +/** + * Recebe uma avaliação e recalcula a pontuação atual + */ +"POST /api/v2/products/:id/rate": { +searchParams: { +/** + * Avaliação + */ +rate?: number +} +response: { +rating?: string +votes?: string +} +} +/** + * Busca os produtos de acordo com os parâmetros definidos + */ +"GET /api/v2/products/search": { +searchParams: { +/** + * Número da página + */ +page?: number +/** + * Quantidade de produtos por página + */ +per_page?: number +/** + * Filtra pelo ID dos produtos + */ +"ids[]"?: number[] +/** + * Filtra produtos que contenham o termo + */ +term?: string +/** + * Permite que o filtro 'term' realize filtragem de produtos por termo parcial + */ +wildcard?: boolean +/** + * Filtra pelo nome da tag dentro de um tipo de tag. Exemplo, type_tags[cor]=verde + */ +"type_tags[]"?: { + +} +/** + * Operador lógico para o filtro de tag + */ +type_tags_operator?: ("and" | "or") +/** + * Filtra pelo valor da propriedade 1 + */ +"property1_values[]"?: string[] +/** + * Operador lógico para o filtro de valor da propriedade 1 + */ +property1_operator?: ("and" | "or") +/** + * Filtra pelo valor da propriedade 2 + */ +"property2_values[]"?: string[] +/** + * Operador lógico para o filtro de valor da propriedade 2 + */ +property2_operator?: ("and" | "or") +/** + * Filtra pelo valor da propriedade 3 + */ +"property3_values[]"?: string[] +/** + * Operador lógico para o filtro de valor da propriedade 3 + */ +property3_operator?: ("and" | "or") +/** + * Filtra pelo preço de venda mínimo do produto + */ +min_price?: number +/** + * Filtra pelo preço de venda máximo do produto + */ +max_price?: number +/** + * Filtra pelo nome das tags, independente do tipo + */ +"tags[]"?: string[] +/** + * Filtra pelo nome das tags, independente do tipo + */ +parent_tags?: string[] +/** + * Filtra por produtos disponíveis + */ +show_only_available?: boolean +/** + * Ordena o resultado da busca de produtos conforme a opção escolhida + */ +sort?: ("newest" | "oldest" | "lowest_price" | "highest_price") +} +response: { +results?: ProductSearch[] +aggregations?: { +min_price?: number +max_price?: number +types?: { + +} +properties?: { +property1?: { +value?: string +count?: number +}[] +property2?: { +value?: string +count?: number +}[] +property3?: { +value?: string +count?: number +}[] +} +} +} +} +/** + * Retorna uma variante pelo SKU + */ +"GET /api/v2/variants/:sku": { +response: Variant +} +/** + * Permite atualizar uma variante pelo SKU + */ +"PATCH /api/v2/variants/:sku": { +body: { +sku: string +name?: string +quantity: number +main?: boolean +/** + * Largura do produto, em centímetros + */ +width?: number +/** + * Altura do produto, em centímetros + */ +height?: number +/** + * Comprimento do produito, em centímetros + */ +length?: number +/** + * Massa do produto, em gramas + */ +weight?: number +/** + * Dias de manuseio da variante + */ +handling_days?: number +/** + * Preço do item + */ +price: number +/** + * Customização da variante + */ +custom_attributes?: { + +} +min_quantity?: number +norder?: number +property1?: VariantProperty1 +property2?: VariantProperty1 +property3?: VariantProperty1 +barcode?: string +/** + * Quantidade de itens vendidos + */ +quantity_sold?: number +} +} +/** + * Lista as imagens de uma variante passando o SKU da mesma na URL + */ +"GET /api/v2/products/:productId/variants/:sku/images": { +response: { +url: string +/** + * Data e horário da última atualização da imagem do produto + */ +updated_at: string +}[] +} +/** + * Lista as imagens de uma variante passando o SKU da mesma nos parâmetros + */ +"GET /api/v2/products/:productId/variants/images": { +response: { +url: string +/** + * Data e horário da última atualização da imagem do produto + */ +updated_at: string +}[] +} +/** + * Calcula o frete para uma determinada variante + */ +"GET /api/v2/variants/:sku/shipping_methods": { +searchParams: { +quantity: number +zip: string +} +response: { +name: string +value: string +price: number +description: string +delivery_days: number +value_needed_to_discount: (null | number) +shipping_method_id: (null | number) +notice: (null | string) +fulfillment_company: (null | string) +countries: (null | { +country?: string +price?: string +}[]) +}[] +} +/** + * Permite listar as tags + */ +"GET /api/v2/tags": { +searchParams: { +/** + * Indica a quantidade de tags que devem ser listadas (page será ignorado) + */ +limit?: number +/** + * Número da página + */ +page?: number +/** + * Quantidade de resultados por página + */ +per_page?: number +/** + * Exibe somente as tags com o tipo indicado + */ +type?: string +/** + * Exibe somente as tags com um dos tipos indicados + */ +types?: string[] +/** + * Exibe somente as tags com um dos nomes indicados + */ +names?: string[] +/** + * Quando passado qualquer valor filtra as tags que contenham imagens + */ +images?: string +/** + * Quando passado qualquer valor filtra as tags marcadas para serem exibidas no carrinho + */ +show_in_carts?: string +/** + * Exibe somente as tags do produto indicado + */ +product_id?: number +/** + * Texto livre que permite filtrar as tags pelo nome + */ +name?: string +/** + * String no formato , que determina o campo a ser ordenado e qual a ordem (asc,desc) + */ +sort?: ("name,asc" | "name,desc" | "type,asc" | "type,desc" | "title,asc" | "title,desc" | "products_count,asc" | "products_count,desc") +} +response: Tag[] +} +/** + * Cria uma tag + */ +"POST /api/v2/tags": { +body: { +name: string +title?: string +/** + * Equivalente ao subtítulo + */ +blurb?: string +description?: string +tag_type?: string +show_in_carts?: boolean +} +response: Tag +} +/** + * Lista os tipos de tags usados em alguma tag + */ +"GET /api/v2/tags/types": { +searchParams: { +/** + * Número da página + */ +page?: number +/** + * Quantidade de resultados por página + */ +per_page?: number +} +response: string[] +} +/** + * Retorna uma tag + */ +"GET /api/v2/tags/:name": { +response: Tag +} +/** + * Remove uma tag + */ +"DELETE /api/v2/tags/:name": { + +} +/** + * Permite atualizar uma tag + */ +"PATCH /api/v2/tags/:name": { + +} +/** + * Retorna os dados de um cupom usando o seu código + */ +"GET /api/v2/coupon_codes/:code": { +response: { +id: number +code: string +discount_id: number +updated_at: string +} +} +/** + * Cria um pedido no Paypal para que posteriormente possa receber um pagamento + */ +"POST /api/v2/carts/:cartId/payment/paypal": { +response: { +status?: string +id?: string +links?: { +href?: string +rel?: string +method?: string +}[] +} +} +/** + * Retorna uma lista de clientes. Caso seja informado o parâmetro "email", então apenas o cliente com esse email será retornado + */ +"GET /api/v2/clients": { +searchParams: { +/** + * Retorna somente o cliente com o email informado + */ +email?: string +/** + * Número da página + */ +page?: number +/** + * Registros por página + */ +per_page?: number +/** + * Filtra os clientes pela menor data de atualização + */ +min_updated_at?: string +/** + * Filtra os clientes pela maior data de atualização + */ +max_updated_at?: string +/** + * Data de inicío da filtragem de clientes pela data de aniversário + */ +birthday_start?: string +/** + * Data final da filtragem de clientes pela data de aniversário + */ +birthday_end?: string +/** + * Filtra os clientes que possuem telefone + */ +has_phone?: string +/** + * Filtra os clientes que possuem first name + */ +has_first_name?: string +/** + * Filtra os clientes por vendedor + */ +user_id?: number +/** + * Filtra os clientes que possuem o termo em alguns dos campos + */ +term?: string +/** + * Ordena o resultado da busca de clientes conforme a opção escolhida + */ +sort?: ("name" | "birthdate") +} +response: Client[] +} +/** + * Permite criar um cliente + */ +"POST /api/v2/clients": { +body: { +email?: string +first_name?: string +last_name?: string +birthdate?: string +gender?: ("M" | "F") +/** + * separado por vírgula + */ +tags?: string +lists?: string[] +password?: string +password_confirmation?: string +terms?: boolean +} +response: { +id?: number +first_name?: string +last_name?: string +email?: string +gender?: string +phone_area?: string +phone?: string +cpf?: string +cnpj?: string +ie?: string +tags?: string +lists?: string[] +facebook_uid?: string +liked_facebook_page?: boolean +updated_at?: string +birthdate?: string +recent_address?: { +id?: string +first_name?: string +last_name?: string +company_name?: string +street_name?: string +street_number?: string +neighborhood?: string +complement?: string +reference?: string +city?: string +state?: string +zip?: string +first_phone_area?: string +first_phone?: string +second_phone_area?: string +second_phone?: string +email?: string +documents?: { +cpf?: string +cnpj?: string +} +}[] +} +} +/** + * Permite retornar as informações do cliente + * O auth_token do cliente pode ser informado no lugar do ID na URL + */ +"GET /api/v2/clients/:id": { +response: Client +} +/** + * Permite remover um cliente + */ +"DELETE /api/v2/clients/:id": { + +} +/** + * Permite atualizar as informações do cliente + */ +"PATCH /api/v2/clients/:id": { +body: { +email?: string +first_name?: string +last_name?: string +birthdate?: string +gender?: ("M" | "F") +/** + * separado por vírgula + */ +tags?: string +lists?: string[] +password?: string +password_confirmation?: string +terms?: boolean +} +} +/** + * Retorna a lista de pedidos do cliente + */ +"GET /api/v2/clients/:id/orders": { +response: Order[] +} +/** + * Lista os endereços do cliente utilizados nos pedidos que foram confirmados + */ +"GET /api/v2/clients/:id/addresses": { +searchParams: { +status?: string +} +response: Address +} +/** + * Lista os endereços cadastrados pelo cliente + */ +"GET /api/v2/clients/:clientId/registered_addresses": { +response: Client1 +} +/** + * Permite criar um endereço do cliente + */ +"POST /api/v2/clients/:clientId/registered_addresses": { +body: { +street_name?: string +street_number?: string +complement?: string +neighborhood?: string +label?: string +zip?: string +reference?: string +} +response: Client1 +} +/** + * Delete o endereço cadastrado pelo cliente + */ +"DELETE /api/v2/clients/:clientId/registered_addresses/:id": { + +} +/** + * Permite atualizar um endereço do cliente + */ +"PATCH /api/v2/clients/:clientId/registered_addresses/:id": { +body: { +street_name?: string +street_number?: string +complement?: string +neighborhood?: string +label?: string +zip?: string +reference?: string +} +response: Client1 +} +/** + * Cria uma senha para o cliente e envia por email + */ +"POST /api/v2/clients/recover_password": { +searchParams: { +/** + * Email do cliente + */ +email: string +/** + * Preencher para pular o envio do email de senha para o cliente + */ +no_send?: string +} +response: Client +} +/** + * Retorna o saldo de crétitos do cliente + */ +"GET /api/v2/clients/:id/credits": { +response: { +balance?: number +} +} +/** + * Retorna as transfertências de crétidos realizadas + */ +"GET /api/v2/clients/:id/credits/transfers": { +response: { +from?: { +account?: string +amount?: number +} +to?: { +account?: string +amount?: number +} +}[] +} +/** + * Lista os bônus do cliente que ainda não foram utilizados + */ +"GET /api/v2/clients/:id/bonuses": { +searchParams: { +/** + * Número da página + */ +page?: string +/** + * Registros por página + */ +per_page?: string +} +response: Bonus[] +} +/** + * Solicita a remoção (esquecimento) dos dados pessoais de um cliente, de acordo com a LGPD + */ +"PATCH /api/v2/clients/:id/remove_personal_data": { + +} +/** + * Faz o login do cliente pelo token salvo no campo auth_token + */ +"GET /api/v2/auth/email/:token": { +response: { +id: number +token: string +} +} +/** + * Faz o login do cliente por usuário e senha + */ +"POST /api/v2/auth/client": { +body: { +email: string +password: string +} +response: { +id: number +auth_token: string +} +} +/** + * Faz o pagamento do carrinho usando a forma de pagamento informada + */ +"POST /api/v2/carts/:cartId/payment": { +body: { +/** + * Meio de pagamento + */ +payment_method: "pix" +/** + * Canal de venda do carrinho + */ +channel?: ("ecommerce" | "direct") +} +} +/** + * Lista os menus + */ +"GET /api/v2/menus": { +searchParams: { +parent_id?: number +position?: string +} +response: Menu[] +} +/** + * Cria um menu + */ +"POST /api/v2/menus": { +body: { +label: string +tooltip?: string +description?: string +type: string +url?: string +page_id?: number +parent_id?: number +position: string +new_position?: string +external?: boolean +tag_id?: number +} +response: Menu +} +/** + * Retorna um menu + */ +"GET /api/v2/menus/:id": { +response: Menu +} +/** + * Remove um menu + */ +"DELETE /api/v2/menus/:id": { + +} +/** + * Atualiza um menu + */ +"PATCH /api/v2/menus/:id": { +body: { +label: string +tooltip?: string +description?: string +type: string +url?: string +page_id?: number +parent_id?: number +position: string +new_position?: string +external?: boolean +tag_id?: number +} +} +/** + * Lista as posições dos menus + */ +"GET /api/v2/menus/positions": { +response: string[] +} +/** + * Reordena os menus na ordem em que seus ids são listados no request + */ +"POST /api/v2/menus/reorder": { +body: { +/** + * A ordem dos elementos será replicada para os menus + */ +ids: number[] +} +} +/** + * Retorna os menus em árvore, organizados pela posição + */ +"GET /api/v2/menus/tree": { +response: { +/** + * Posição + */ +[k: string]: MenuTree[] +} +} +/** + * Retorna uma mensagem do site + */ +"GET /api/v2/site_message": { +response: SiteMessage +} +/** + * Remove uma mensagem do site + */ +"DELETE /api/v2/site_message": { + +} +/** + * Cria ou atualiza uma mensagem do site + */ +"PATCH /api/v2/site_message": { +body: { +title?: string +description?: string +call_to_action?: string +} +} +/** + * Lista as imagens associadas a loja + */ +"GET /api/v2/shop/images": { +searchParams: { +/** + * Número da página atual. Os dados de paginação estarão disponíveis, em formato JSON, no header X-Pagination no response da API, caso exista paginação + */ +page?: number +/** + * Número máximo de registros que deve ser retornado por página + */ +per_page?: number +/** + * Ordena o resultado da busca de produtos em ordem crescente de cadastro + */ +sort?: "newest" +} +response: ShopAsset[] +} +/** + * Permite cadastrar uma imagem + */ +"POST /api/v2/shop/images": { +body: { +position?: string +file_uid?: string +} +response: ShopAsset +} +/** + * Permite remover uma imagem da loja + */ +"DELETE /api/v2/shop/images/:id": { + +} +/** + * Permite adicionar um atributo customizado de produto + */ +"POST /api/v2/shop/product_attributes": { +body: { +index: number +name: string +mandatory: boolean +} +response: ProductsAttributes +} +/** + * Permite listar as personalizações + */ +"GET /api/v2/customizations": { +searchParams: { +/** + * Filtra por produto + */ +product_id?: number +} +response: Customization[] +} +/** + * Permite criar uma personalização + */ +"POST /api/v2/customizations": { +body: { +group_name: string +group_type: string +name: string +label?: string +image_uid?: string +image_name?: string +price?: number +quantity?: number +handling_days?: number +tag_id: number +sku?: string +pattern?: string +} +response: Customization +} +/** + * Permite retornar uma personalização + */ +"GET /api/v2/customizations/:id": { +response: Customization +} +/** + * Permite remover uma personalização + */ +"DELETE /api/v2/customizations/:id": { + +} +/** + * Permite alterar uma personalização + */ +"PATCH /api/v2/customizations/:id": { +body: { +group_name?: string +group_type?: string +name?: string +label?: string +image_uid?: string +image_name?: string +price?: string +quantity?: string +handling_days?: string +tag_id?: string +sku?: string +pattern?: string +} +} +/** + * Permite listar os itens do pedido + */ +"GET /api/v2/orders/:orderId/items": { +response: OrderItems[] +} +/** + * Permite listar as personalizações de cada item do pedido + */ +"GET /api/v2/orders/:orderId/items/:itemId/customizations": { +response: OrderItemCustomization[] +} +/** + * Permite listar as personalizações de cada item do carrinho + */ +"GET /api/v2/carts/:cartId/items/:itemId/customizations": { +response: { +[k: string]: CartItemCustomization[] +} +} +/** + * Permite remover uma customização do item do carrinho + */ +"DELETE /api/v2/carts/:cartId/items/:itemId/customizations": { + +} +/** + * Lista os mapeamentos + */ +"GET /api/v2/mappings": { +searchParams: { +/** + * Número da página atual. Os dados de paginação estarão disponíveis, em formato JSON, no header X-Pagination no response da API, caso exista paginação + */ +page?: number +/** + * Número máximo de registros que deve ser retornado por página + */ +per_page?: number +} +response: Mapping[] +} +/** + * Cria um mapeamento + */ +"POST /api/v2/mappings": { +body: { +key: string +from?: string[] +to?: string +} +response: Mapping +} +/** + * Retorna os dados de um mapeamento + */ +"GET /api/v2/mappings/:id": { +response: Mapping +} +/** + * Remove um mapeamento + */ +"DELETE /api/v2/mappings/:id": { + +} +/** + * Atualiza um mapeamento + */ +"PATCH /api/v2/mappings/:id": { +body: { +key: string +from?: string[] +to?: string +} +} +/** + * Retorna a lista de banners + */ +"GET /api/v2/banners": { +searchParams: { +/** + * Booleano indicando para filtrar banners fora do prazo de validade + */ +only_valid?: string +/** + * Booleano indicando para filtrar banners com prazo de validade expirados + */ +only_expired?: string +/** + * Booleano indicando para filtrar banners agendados + */ +only_scheduled?: string +/** + * Lista separada por vírgula com nomes de tags + */ +tag?: string +/** + * Texto livre que permite filtrar os banners pelo título + */ +title?: string +/** + * Booleano indicando para não fazer paginação dos resultados + */ +no_paginate?: string +/** + * Número da página atual. Os dados de paginação estarão disponíveis, em formato JSON, no header X-Pagination no response da API, caso exista paginação + */ +page?: number +/** + * Número máximo de registros que deve ser retornado por página + */ +per_page?: number +} +response: Banner[] +} +/** + * Retorna os dados de um banner + */ +"GET /api/v2/banners/:id": { +response: Banner +} +/** + * Retorna todos os banners disponíveis agrupados por tag + */ +"GET /api/v2/banners/all": { +response: { +[k: string]: SlimBanner[] +} +} +/** + * Permite calcular o frete para pedidos internacionais + */ +"GET /api/v2/carts/:cartId/shipping_methods/intl": { +searchParams: { +/** + * Código do país de destino + */ +country: string +} +response: { +"{package_label}"?: ShippingMethods1[] +} +} +/** + * Lista as amostras disponíveis para determinado carrinho + */ +"GET /api/v2/carts/:cartId/samples": { +response: { +id: number +image_url: (null | string) +name: string +reference: string +updated_at: string +url: string +variants: { +id: number +main: boolean +sku: string +name: string +updated_at: string +image_url: (null | string) +product_id: number +norder: number +}[] +} +} +/** + * Retorna o endereço de entrega + */ +"GET /api/v2/carts/:cartId/shipping_address": { +response: EnderecoDeEnvio +} +/** + * Adiciona um endereço de entrega no carrinho + */ +"POST /api/v2/carts/:cartId/shipping_address": { +body: EnderecoDeEnvio1 +response: CartItem +} +/** + * Associa um código de cupom ao carrinho + */ +"POST /api/v2/carts/:cartId/coupon_code": { +body: { +/** + * Código do cupom + */ +code: string +} +response: { +/** + * Código do cupom + */ +code: string +discount: number +rebate_token: string +rebate_discount: number +} +} +/** + * Lista todos os channels usados nos pedidos criados + */ +"GET /api/v2/orders/channels": { +response: string[] +} +/** + * Lista todos os estados usados nos pedidos criados + */ +"GET /api/v2/orders/states": { +response: string[] +} +/** + * Retorna o preço do produto e das variantes + */ +"GET /api/v2/products/:productId/price": { +searchParams: { +/** + * Array com os códigos de cupons + */ +coupon_codes?: string[] +} +response: { +available: boolean +on_sale: boolean +price: number +sale_price: number +intl_price: number +discount_rule?: any +/** + * @minItems 1 + */ +installments: [ProductInstallment, ...(ProductInstallment)[]] +/** + * Data e horário da última atualização + */ +updated_at: string +/** + * @minItems 1 + */ +variants: [ProductPriceVariant, ...(ProductPriceVariant)[]] +} +} +/** + * Lista as imagens do produto + */ +"GET /api/v2/products/:productId/images": { +response: ProductImage[] +} +/** + * Cria uma imagem do produto + */ +"POST /api/v2/products/:productId/images": { +body: { +file_url: string +/** + * IDs da variantes associadas a imagem + */ +variant_ids?: number[] +} +response: ProductImage[] +} +/** + * Deleta uma imagem do produto + */ +"DELETE /api/v2/products/:productId/images/:id": { + +} +/** + * Reordena as imagens do produto + */ +"POST /api/v2/products/:productId/images/reorder": { +body: { +ids: number[] +} +} +/** + * Associa a imagem com uma variante + */ +"POST /api/v2/products/:productId/images/:id/add_variant": { +body: { +variant_id: number +} +} +/** + * Remove a associação da imagem com uma variante + */ +"POST /api/v2/products/:productId/images/:id/remove_variant": { +body: { +variant_id: number +} +} +/** + * Retorna uma lista de pacotes de um pedido + */ +"GET /api/v2/orders/:orderCode/packages": { +response: Package[] +} +/** + * Indica para a API que dererminado evento aconteceu e que ela deve disparar as ações relacionadas + */ +"POST /api/v2/events": { +searchParams: { +/** + * Evento que ocorreu + */ +event_type: string +/** + * ID do recurso selacionado ao evento + */ +id: string +/** + * IP do usuário + */ +browser_ip?: string +/** + * User agent do usuário + */ +user_agent?: string +} +} +/** + * Permite a listagem de recebíveis (comissão) de um usuário vendedor da loja, quando ocorre split de pagamentos via Pagarme + */ +"GET /api/v2/users/:id/payables": { +response: RecebiveisDoUsuario[] +} +/** + * Retorna um produto pelo código identificador (`product_id`) + */ +"GET /api/v2/products/:productId": { +searchParams: { +/** + * Array com os códigos de cupons + */ +coupon_codes?: string[] +/** + * Selecione `true` para incluir o nome do local de armazenamento no retorno da requisição + */ +include_inventory_place?: string +/** + * Selecione `true` para incluir todas as imagens do produto + */ +include_images?: string +} +response: Product +} +/** + * Remove um produto do catálogo pelo código indentificador (`product_id`) + */ +"DELETE /api/v2/products/:productId": { + +} +/** + * Atualiza informações de um produto no catálogo pelo código identificador (`product_id`) + */ +"PATCH /api/v2/products/:productId": { +body: SimpleProduct1 +} +/** + * Recebe uma avaliação e recalcula a pontuação atual + */ +"POST /api/v2/products/:productId/rate": { +searchParams: { +/** + * Avaliação + */ +rate?: number +} +response: { +/** + * Média das avaliações + */ +rating?: string +/** + * Número de avaliações recebidas + */ +votes?: string +} +} +/** + * Permite remover uma variante + */ +"DELETE /api/v2/products/:productId/variants/:variantId": { + +} +/** + * @deprecated + * Atualiza as informações de um variante + */ +"PATCH /api/v2/products/:productId/variants/:variantId": { +body: { +sku: string +name?: string +quantity: number +main?: boolean +/** + * Massa do produto, em gramas + */ +weight?: number +/** + * Largura do produto, em centímetros + */ +width?: number +/** + * Altura do produto, em centímetros + */ +height?: number +/** + * Comprimento do produito, em centímetros + */ +length?: number +/** + * Dias de manuseio da variante + */ +handling_days?: number +price: number +/** + * Customização da variante + */ +custom_attributes?: { + +} +min_quantity?: number +norder?: number +property1?: { +name?: string +value?: string +defining?: boolean +} +property2?: { +name?: string +value?: string +defining?: boolean +} +property3?: { +name?: string +value?: string +defining?: boolean +} +barcode?: string +/** + * Quantidade de itens vendidos + */ +quantity_sold?: number +} +} +/** + * Deleta uma imagem do produto + */ +"DELETE /api/v2/products/:productId/images/:imageId": { + +} +/** + * Associa a imagem com uma variante + */ +"POST /api/v2/products/:productId/images/:imageId/add_variant": { +body: { +variant_id: number +} +} +/** + * Remove a associação da imagem com uma variante + */ +"POST /api/v2/products/:productId/images/:imageId/remove_variant": { +body: { +variant_id: number +} +} +/** + * Retorna as informações de um carrinho pelo seu `id` ou `token` + */ +"GET /api/v2/carts/:cartId": { +response: Cart1 +} +/** + * Permite excluir um carrinho + */ +"DELETE /api/v2/carts/:cartId": { + +} +/** + * Permite atualizar os atributos de um carrinho + */ +"PATCH /api/v2/carts/:cartId": { +body: ParametrosDeCarrinhoResumido +} +/** + * Remove um item do carrinho + */ +"DELETE /api/v2/carts/:cartId/items/:itemId": { + +} +/** + * Atualiza um item do carrinho + */ +"PATCH /api/v2/carts/:cartId/items/:itemId": { +body: Produto1 +} +/** + * Atualiza o método para o envio dos itens do carrinho + */ +"PATCH /api/v2/carts/:cartId/shipping_methods/:valueMethod": { +body: ShippingMethods1 +} +/** + * Calculo os método de envio disponíveis para o carrinho + */ +"GET /api/v2/carts/:cartId/shipping_methods": { +response: { +"{package_label}"?: ShippingMethods1[] +} +} +/** + * Calcula as parcelas de pagamento para valor total do carrinho + */ +"GET /api/v2/carts/:cartId/installments": { +response: CartInstallment1[] +} +/** + * Retorna os dados de um pedido pelo `code` ou `token` do pedido + */ +"GET /api/v2/orders/:orderCode": { +searchParams: { +/** + * Inclui as formas de entrega do pedido + */ +include_shipping_address?: boolean +} +response: Order +} +/** + * Atualiza o campo de dados extras de um pedido pelo `code` do pedido + */ +"PATCH /api/v2/orders/:orderCode": { +body: { +/** + * Campo para registro de observações, chave ou valores necessários + */ +extra?: { + +} +} +} +/** + * Retorna a *timeline* de eventos ocorridos em um pedido + */ +"GET /api/v2/orders/:orderCode/events": { +response: { + +}[] +} +/** + * Retorna a avaliação que o cliente fez em um pedido + */ +"GET /api/v2/orders/:orderCode/reviews": { + +} +/** + * Retorna os descontos de um pedido pelo `code` ou `token` do pedido + */ +"GET /api/v2/orders/:orderCode/discounts": { +response: { + +}[] +} +/** + * Retorna o endereço de envio pelo `code` do pedido + */ +"GET /api/v2/orders/:orderCode/shipping_address": { +response: EnderecoDeEnvio1 +} +/** + * Atualiza dados de endereço do pedido + */ +"PATCH /api/v2/orders/:orderCode/shipping_address": { +body: EnderecoDeEnvio1 +} +/** + * Captura o pagamento no adquirente para pedidos com pagamento por cartão de crédito. + */ +"POST /api/v2/orders/:orderCode/capture": { +response: { + +} +} +/** + * Confirma um pedido + */ +"POST /api/v2/orders/:orderCode/confirm": { +body: { +/** + * Parâmetro para incluir o retorno [da requisição de captura do pagamento](https://developers.vnda.com.br/reference/post-api-v2-orders-capture). + * Esse parâmetro é **obrigatório** para pedidos com pagamento por cartão de crédito. + */ +confirmation_data?: string +} +} +/** + * Faz o estorno do pagamento no adquirente do cartão de crédito + * Operação válida para pedidos pagos com cartão de crédito + */ +"POST /api/v2/orders/:orderCode/chargeback": { + +} +/** + * Altera o status do pedido para `cancelado` + */ +"POST /api/v2/orders/:orderCode/cancel": { +body: { +/** + * Parâmetro para incluir uma confirmação de estorno de pagamento para o cliente. + * Para pedidos com pagamento via cartão de crédito, é obrigatório que nesse campo seja incluído no parâmetro o retorno [da requisição de estorno de pagamento](https://developers.vnda.com.br/reference/post-api-v2-orders-order-code-chargeback). + */ +cancelation_data?: string +} +response: { + +} +} +/** + * Retorna os itens de um pedido pelo código do pedido + */ +"GET /api/v2/orders/:orderCode/items": { +response: ProdutoEmUmPedido1[][] +} +/** + * Lista as personalizações de um item do pedido pelos códigos do item e do pedido + */ +"GET /api/v2/orders/:orderCode/items/:itemId/customizations": { +response: OrderItemCustomization[] +} +/** + * Retorna os pedidos de um cliente pelo seu `id_client` + */ +"GET /api/v2/clients/:idClient/orders": { +response: Order[] +} +/** + * Retorna as notas fisicais de um pacote do pedido + */ +"GET /api/v2/orders/:orderCode/packages/:packageCode/invoices": { +response: Invoice[] +} +/** + * Inclui nota fiscal no pacote de um pedido + */ +"POST /api/v2/orders/:orderCode/packages/:packageCode/invoices": { +body: Invoice +response: Invoice +} +/** + * Remove uma nota fiscal + */ +"DELETE /api/v2/orders/:orderCode/packages/:packageCode/invoices/:number": { + +} +/** + * Atualiza uma nota fiscal + */ +"PATCH /api/v2/orders/:orderCode/packages/:packageCode/invoices/:number": { +body: Invoice +} +/** + * Permite listar os pedidos pendentes do feed + */ +"GET /api/feed/orders": { +searchParams: { +/** + * Selecione `true` para incluir o endereço na resposta + */ +include_shipping_address?: true +/** + * Filtra os pedidos por status + */ +status?: ("received" | "confirmed" | "canceled") +} +response: Order[] +} +/** + * Permite marcar os pedidos para que eles sejam filtrados da listagem do feed + */ +"POST /api/feed/orders": { +body: { +orders?: { +/** + * Código do pedido + */ +code: string +}[] +} +} +} +/** + * Modelo que representa um usuário na API + */ +export interface User { +/** + * Código identificador do usuário + */ +id?: number +/** + * Email do usuário + */ +email: string +/** + * Token de validação de usuário logado (`access_token`) + * + * O `access_token` é gerado quando o usuário loga no Admin + */ +access_token?: string +/** + * Nome do usuário + */ +name?: string +/** + * Identificador de usuários administradores + * + * Esse atributo retorna `true` para um usuário administrador do ambiente de loja + */ +admin?: boolean +/** + * Identificador de usuários que atualizaram a senha inicial + * + * Esse atributo retorna `true` para um usuário que já redefiniu sua senha pelo menos uma vez + */ +renew_password?: boolean +/** + * Código da função do usuário na loja: + * + * - Agente: `0`; + * - Gestor: `1`; + * - Local: `2`; + * - Agente Social Selling: `3`. + */ +role?: number +/** + * Tags para agrupamento de usuários + * As tags podem ser são utilizadas para direcionar promoções para determinados usuários, organizar os recebedores em uma divisão de pagamentos, definir regras de comissão + */ +tags?: string[] +/** + * Código externo do Vendedor. Esse campo é destinado para cadastrar um código de vendedor já existente em outro sistema. + */ +external_code?: string +/** + * Código de Discagem Direta a Distância (DDD) do telefone do usuário + */ +phone_area?: string +/** + * Número de telefone do usuário + */ +phone?: string +/** + * Data de inclusão do usuário no Admin + */ +created_at?: string +/** + * Data de atualização das informações do usuário + */ +updated_at?: string +} +/** + * Modelo que representa um pedido na API + */ +export interface Order { +/** + * Desconto por bônus do cliente + */ +rebate_discount: number +/** + * Código identificador `ID` do desconto por bônus + */ +rebate_token?: string +/** + * Código identificador `ID` do cliente + */ +user_id?: number +/** + * Data da última atualização do pedido + */ +updated_at: string +/** + * Lista com os códigos de rastreio dos pacotes do pedido + */ +tracking_code_list?: string[] +/** + * Código de rastreio do pacote + */ +tracking_code?: string +/** + * Valor final do pedido + */ +total: number +token: string +taxes: number +/** + * Valor da soma dos itens do pedido, desconsiderando descontos e frete. + */ +subtotal: number +/** + * Status do pedido + */ +status: ("received" | "confirmed" | "canceled") +payment_due_date?: string +slip_url?: string +slip_token?: string +slip_due_date?: string +slip: boolean +shipping_tracked_at?: string +shipping_price?: number +shipping_label?: string +/** + * Data e horário de envio do pedido + */ +shipped_at: string +/** + * Data e horário de recebimento do pedido + */ +received_at: string +payment_tid?: string +/** + * Método de pagamento do pedido + */ +payment_method: string +payment_gateway: string +payment_authorization: string +/** + * Data e horário do pagamento do pedido + */ +paid_at: string +items?: ProdutoEmUmPedido[] +/** + * Parcelas do pagamento parcelado + */ +installments?: number +/** + * Código identificador do pedido + */ +id?: number +/** + * Campo de observações do pedido + */ +extra?: { + +} +expected_delivery_date?: string +/** + * Email do cliente + */ +email: string +/** + * Valor do desconto aplicado no pedido + */ +discount_price: number +deposit: boolean +delivery_type?: string +delivery_message?: string +/** + * Dias para entrega + */ +delivery_days?: number +/** + * Data de entrega do pedido + */ +delivered_at: string +/** + * Código de cupom do pedido + */ +coupon_code: string +/** + * Data e horário de confirmação do pedido + */ +confirmed_at: string +/** + * Código do pedido + */ +code: string +/** + * Código identificador (`ID`) do cliente + */ +client_id: number +/** + * Canal de venda que originou o pedido + */ +channel: ("ecommerce" | "direct") +/** + * Código identificador do carrinho que originou o pedido + */ +cart_id: number +/** + * Data de validade do cartão de crédito + */ +card_validity: string +/** + * Número do cartão de crédito + */ +card_number: string +/** + * Retorna `true` se o método de pagamento do pedido é por cartão de crédito. + */ +card: boolean +/** + * Data e horário do cancelamento do pedido + */ +canceled_at?: string +/** + * Endereço IP de origem do pedido + */ +browser_ip: string +/** + * Agente do pedido + */ +agent?: string +affiliate_tag?: string +pix_qr_code?: string +/** + * Código de autorização do pagamento + */ +payment_authorization_code?: string +/** + * Indica se o pedido gerou bônus + */ +bonus_granted?: boolean +has_split?: boolean +/** + * Indica se o pedido foi pago usando o Pix + */ +pix: boolean +ame_qr_code?: string +/** + * Indica se o pedido foi pago usando o Ame + */ +ame: boolean +antifraud_assurance?: string +minItems?: 0 +} +/** + * Modelo de produto em um pedido + */ +export interface ProdutoEmUmPedido { +extra: { + +} +height?: number +id?: number +length?: number +original_price?: number +package?: string +picture_url?: string +place_city?: string +place_id?: number +place_name?: string +price?: number +product_id: number +product_name: string +quantity: number +reference: string +sku: string +total: number +variant_id: number +variant_name: string +weight: number +width: number +barcode?: string +} +/** + * Modelo que representa uma variante na API + */ +export interface Variant { +/** + * Código identificador da variante + */ +id?: number +/** + * Identifica se é a variante principal do produto. Para `true` a variante é principal e `false` a variante é secundária + */ +main?: boolean +/** + * Identifica se a variante está ativa em `true` e desativa em `false` + */ +available?: boolean +/** + * Código SKU da variante + */ +sku?: string +/** + * Nome da variante + */ +name?: string +/** + * Slug da URL da variante + */ +slug?: string +/** + * Quantidade mínima para venda + */ +min_quantity?: number +/** + * Quantidade física + */ +quantity?: number +quantity_sold?: number +/** + * Quantidade disponível + */ +stock?: number +/** + * Customização da variante + */ +custom_attributes?: { + +} +/** + * [Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante + */ +properties?: { +property1?: VariantProperty +property2?: VariantProperty +property3?: VariantProperty +} +/** + * Data e horário da última atualização da variante + */ +updated_at?: string +/** + * Preço do item + */ +price?: number +/** + * Relação das parcelas para pagamento do item parcelado + */ +installments?: number[] +/** + * Unidades reservadas e não reservadas do item + */ +available_quantity?: number +/** + * Massa do produto, em gramas + */ +weight?: number +/** + * Largura do produto, em centímetros + */ +width?: number +/** + * Altura do produto, em centímetros + */ +height?: number +/** + * Comprimento do produito, em centímetros + */ +length?: number +/** + * Dias de manuseio da variante + */ +handling_days?: number +/** + * Relação de itens por estoque (armazém) + */ +inventories?: VariantInventory[] +/** + * Preço promocional + */ +sale_price?: number +/** + * Preço internacional + */ +intl_price?: number +/** + * URL da imagem da variante + */ +image_url?: string +/** + * Código identificador `ID` do produto + */ +product_id?: number +/** + * Código de barra da variante + */ +barcode?: string +norder?: number +required?: [] +additionalProperties?: never +} +/** + * Modelo que representa uma propriedade customizada na API + */ +export interface VariantProperty { +/** + * Indica se a variante possui uma definição (`true`) ou se a variante não possui (`false`) + */ +defining: boolean +/** + * Nome da propriedade + */ +name: string +/** + * Valor da propriedade + */ +value?: string +} +/** + * Model que representa um inventory da variante + */ +export interface VariantInventory { +id: number +name: string +place_id: number +/** + * Preço do item + */ +price: number +quantity: number +/** + * Quantidade de itens vendidos + */ +quantity_sold: number +/** + * Preço promocional + */ +sale_price: number +slug: string +} +/** + * Modelo que representa um template na API + */ +export interface Template { +path: string +body?: string +updated_at: string +} +/** + * Modelo que representa um usuário na API + */ +export interface User1 { +/** + * Código identificador do usuário + */ +id?: number +/** + * Email do usuário + */ +email: string +/** + * Token de validação de usuário logado (`access_token`) + * + * O `access_token` é gerado quando o usuário loga no Admin + */ +access_token?: string +/** + * Nome do usuário + */ +name?: string +/** + * Identificador de usuários administradores + * + * Esse atributo retorna `true` para um usuário administrador do ambiente de loja + */ +admin?: boolean +/** + * Identificador de usuários que atualizaram a senha inicial + * + * Esse atributo retorna `true` para um usuário que já redefiniu sua senha pelo menos uma vez + */ +renew_password?: boolean +/** + * Código da função do usuário na loja: + * + * - Agente: `0`; + * - Gestor: `1`; + * - Local: `2`; + * - Agente Social Selling: `3`. + */ +role?: number +/** + * Tags para agrupamento de usuários + * As tags podem ser são utilizadas para direcionar promoções para determinados usuários, organizar os recebedores em uma divisão de pagamentos, definir regras de comissão + */ +tags?: string[] +/** + * Código externo do Vendedor. Esse campo é destinado para cadastrar um código de vendedor já existente em outro sistema. + */ +external_code?: string +/** + * Código de Discagem Direta a Distância (DDD) do telefone do usuário + */ +phone_area?: string +/** + * Número de telefone do usuário + */ +phone?: string +/** + * Data de inclusão do usuário no Admin + */ +created_at?: string +/** + * Data de atualização das informações do usuário + */ +updated_at?: string +} +/** + * Modelo que representa um carrinho na API + */ +export interface Cart { +id: number +email: string +shipping_method: string +items_count: number +quotation_responses_count: number +payment_responses_count: number +has_payment_responses: boolean +has_phone: boolean +updated_at: string +} +/** + * Parâmetros criação e atualização de carrinho + */ +export interface ParametrosDeCarrinhoResumido { +/** + * Agente que criou o carrinho + */ +agent?: string +/** + * Código de Endereçamento Postal (CEP) do destinatário do pedido + */ +zip?: string +/** + * Código identificador `ID` do cliente + */ +client_id?: number +/** + * Código identificador `ID` do desconto do carrinho + */ +coupon_code?: string +/** + * @deprecated + * Email do cliente + */ +email?: string +/** + * Token do desconto + */ +rebate_token?: string +/** + * Id do agente + */ +user_id?: number +} +/** + * Modelo que representa um carrinho na API + */ +export interface Cart1 { +/** + * Agente que criou o carrinho + */ +agent: string +/** + * Código identificador `ID` do endereço de cobrança do carrinho + */ +billing_address_id: number +/** + * Canal de venda que originou o carrinho + */ +channel: string +/** + * Código identificador `ID` do cliente + */ +client_id: number +/** + * Código identificador `ID` do carrinho + */ +code: string +/** + * Código de cupom de desconto utilizado no carrinho + */ +coupon_code: string +discount: Discount +/** + * @deprecated + * Valor do desconto + */ +discount_price: number +/** + * Campo para registro de observações, chave ou valores necessários + */ +extra: { + +} +/** + * Código identificador `ID` do carrinho + */ +id: number +/** + * Itens do carrinho + */ +items: CartItem[] +/** + * Unidades do item no carrinho + */ +items_count: number +/** + * Código identificador `ID` do endereço de entrega do carrinho + */ +shipping_address_id: number +/** + * Método de envio selecionado para o carrinho, como por exemplo: normal, expressa e agendada. + */ +shipping_method: string +/** + * Lista com as entregas disponíveis para os itens do carrinho de acordo com o endereço de envio + */ +shipping_methods: ShippingMethods[] +/** + * Preço de envio + */ +shipping_price: number +/** + * Valor da soma dos itens do carrinho, sem considerar descontos de cupom, carrinho e frete. + */ +subtotal: number +/** + * Token do carrinho + */ +token: string +/** + * Valor final do carrinho + */ +total: number +/** + * Valor total do carrinho para pagamento por depósito + */ +total_for_deposit: number +/** + * Valor total do carrinho para pagamento por boleto + */ +total_for_slip: number +/** + * Valor do carrinho para pagamento por PIX + */ +total_for_pix: number +/** + * Data da última atualização do carrinho + */ +updated_at: string +/** + * Código identificador `ID` do desconto por bônus + */ +rebate_token: string +/** + * Desconto por bônus do cliente + */ +rebate_discount: number +/** + * Número de dias para manuseio dos itens + */ +handling_days: number +/** + * Valor de desconto de promoções aplicadas ao subtotal do carrinho + */ +subtotal_discount: number +/** + * Valor de desconto de promoções aplicadas ao valor total do carrinho + */ +total_discount: number +installments?: CartInstallment +/** + * Código identificador `ID` do cliente + */ +user_id?: string +minItems?: 0 +} +/** + * Promoção aplicada no carrinho + */ +export interface Discount { +id: number +name: string +description: string +facebook: boolean +valid_to: ("store" | "cart") +/** + * DEPRECATED + */ +seal_uid: string +/** + * DEPRECATED + */ +seal_url: string +start_at: string +end_at: string +email: string +cpf: string +tags: string +} +/** + * Modelo que representa um item no carrinho na API + */ +export interface CartItem { +/** + * Unidades disponíveis do produto + */ +available_quantity: number +/** + * Número de dias para a entrega + */ +delivery_days: number +/** + * Campo para registro de observações, chave ou valores necessários + */ +extra: { + +} +/** + * Código identificador do local do produto + */ +place_id: number +/** + * Preço do produto + */ +price: number +/** + * Preço internacional + */ +intl_price: number +/** + * Código identificador `ID` do produto + */ +product_id: number +/** + * Nome do produto + */ +product_name: string +/** + * Código de referência do produto + */ +product_reference: string +/** + * URL do produto no e-commerce + */ +product_url: string +/** + * Unidades do produto no carrinho + */ +quantity: number +/** + * Identificador do seller + */ +seller: string +/** + * Nome do seller + */ +seller_name: string +/** + * Valor do produto sem descontos e promoções + */ +subtotal: number +/** + * Valor total do produto + */ +total: number +/** + * Data da última atualização do carrinho + */ +updated_at: string +/** + * Atributos da variante + */ +variant_attributes: { + +} +/** + * Quantidade miníma de variantes para compra + */ +variant_min_quantity: number +/** + * Nome da variante + */ +variant_name: string +/** + * Preço da variante + */ +variant_price: number +/** + * Preço internacional da variante + */ +variant_intl_price: number +variant_properties: Variant1 +/** + * Código SKU da [Variante](https://developers.vnda.com.br/docs/cat%C3%A1logo-de-produtos#produto-atributo-e-variante) + */ +variant_sku: string +/** + * Código identificador do item no carrinho + */ +id?: string +/** + * Tipo de produto + */ +product_type?: string +/** + * URL da imagem da variante + */ +image_url?: string +} +/** + * Modelo que representa uma variante na API + */ +export interface Variant1 { +/** + * Código identificador da variante + */ +id?: number +/** + * Identifica se é a variante principal do produto. Para `true` a variante é principal e `false` a variante é secundária + */ +main?: boolean +/** + * Identifica se a variante está ativa em `true` e desativa em `false` + */ +available?: boolean +/** + * Código SKU da variante + */ +sku?: string +/** + * Nome da variante + */ +name?: string +/** + * Slug da URL da variante + */ +slug?: string +/** + * Quantidade mínima para venda + */ +min_quantity?: number +/** + * Quantidade física + */ +quantity?: number +quantity_sold?: number +/** + * Quantidade disponível + */ +stock?: number +/** + * Customização da variante + */ +custom_attributes?: { + +} +/** + * [Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante + */ +properties?: { +property1?: VariantProperty +property2?: VariantProperty +property3?: VariantProperty +} +/** + * Data e horário da última atualização da variante + */ +updated_at?: string +/** + * Preço do item + */ +price?: number +/** + * Relação das parcelas para pagamento do item parcelado + */ +installments?: number[] +/** + * Unidades reservadas e não reservadas do item + */ +available_quantity?: number +/** + * Massa do produto, em gramas + */ +weight?: number +/** + * Largura do produto, em centímetros + */ +width?: number +/** + * Altura do produto, em centímetros + */ +height?: number +/** + * Comprimento do produito, em centímetros + */ +length?: number +/** + * Dias de manuseio da variante + */ +handling_days?: number +/** + * Relação de itens por estoque (armazém) + */ +inventories?: VariantInventory[] +/** + * Preço promocional + */ +sale_price?: number +/** + * Preço internacional + */ +intl_price?: number +/** + * URL da imagem da variante + */ +image_url?: string +/** + * Código identificador `ID` do produto + */ +product_id?: number +/** + * Código de barra da variante + */ +barcode?: string +norder?: number +required?: [] +additionalProperties?: never +} +/** + * Modelo que representa as formas de entrega na API + */ +export interface ShippingMethods { +package: string +name: string +label: string +price: string +delivery_days: string +delivery_type: string +description: string +short_description: string +fulfillment_company: string +} +/** + * Parcelas para pagamento parcelado + */ +export interface CartInstallment { +/** + * Identifica se há (`true`) ou não (`false`) juros no parcelamento + */ +interest: boolean +/** + * Taxa de juros do parcelamento + */ +interest_rate: number +/** + * Número de parcelas + */ +number: number +/** + * Valor de cada parcela + */ +price: number +/** + * Valor total das parcelas + */ +total: number +} +/** + * Modelo que representa uma parcela do total de um carrinho + */ +export interface CartInstallment1 { +/** + * Identifica se há (`true`) ou não (`false`) juros no parcelamento + */ +interest: boolean +/** + * Taxa de juros do parcelamento + */ +interest_rate: number +/** + * Número de parcelas + */ +number: number +/** + * Valor de cada parcela + */ +price: number +/** + * Valor total das parcelas + */ +total: number +} +/** + * Modelo que representa um local na API + */ +export interface Place { +id?: number +name: string +address_line_1: string +address_line_2?: string +city: string +neighborhood?: string +zip?: string +home_page?: string +latitude?: number +longitude?: number +images?: string[] +description?: string +email: string +first_phone?: string +second_phone?: string +mobile_phone?: string +only_cash?: boolean +categories?: string[] +marker_url?: string +state?: string +created_at?: string +updated_at?: string +opening_hours?: string +warehouse?: boolean +legal_name?: string +cnpj?: string +} +/** + * Modelo que representa uma nota fiscal na API + */ +export interface Invoice { +/** + * Número da nota fiscal + */ +number: number +/** + * Número de série da nota fiscal + */ +series?: number +/** + * Data e horário da criação da nota fiscal + */ +issued_at?: string +/** + * Chave da nota fiscal + */ +key?: string +volumes?: number +} +/** + * Modelo que representa um recebedor na API + */ +export interface PaymentRecipient { +id: number +percentage: number +active?: boolean +charge_processing_fee?: boolean +liable?: boolean +code?: string +name?: string +tag_name?: string +place_id?: number +recipient_id: number +tag_id?: number +user_id?: number +/** + * Indica se o frete deve ser incluído no split do pagamento + */ +include_shipping?: boolean +} +/** + * Valores que o usuário possui a receber + */ +export interface RecebiveisDoUsuario { +type?: string +status?: string +amount?: number +fee?: number +installment?: number +credit_date?: string +order_date?: string +transaction_id?: number +} +/** + * Modelo que representa um membro do público + */ +export interface AudienceMember { +id?: number +first_name?: string +last_name?: string +email: string +phone_area?: string +phone?: string +tags?: string[] +} +/** + * Modelo de carcaterística de produto para item no carrinho + */ +export interface Produto { +sku: string +quantity: number +extra?: { + +} +place_id?: number +store_coupon_code?: string +customizations?: any[] +} +/** + * Modelo que representa uma promoção na API + */ +export interface Discount1 { +/** + * Código identificador `ID` do desconto + */ +id?: number +/** + * Nome do desconto ou promoção + */ +name: string +/** + * Descrição do desconto + */ +description?: string +/** + * Data de início da regra do desconto + */ +start_at: string +/** + * Data de fim da regra do desconto + */ +end_at?: string +/** + * Indica se o desconto está habilitado (`true`) ou desabilitado (`false`) + */ +enabled: boolean +/** + * Em desuso + */ +facebook?: boolean +/** + * Indica a regra da promoção: se o desconto é aplicado na vitrine ou no carrinho da loja + */ +valid_to?: string +/** + * Email do cliente, no caso de promoções direcionadas para clientes específicos + */ +email?: string +/** + * Cadastro de Pessoa Física (CPF) do cliente, no caso de promoções direcionadas para clientes específicos + */ +cpf?: string +/** + * Tag de agrupamento de promoção + */ +tags?: string +} +/** + * Modelo que representa uma regra de desconto na API + */ +export interface DiscountRule { +id?: number +amount: number +type: ("fixed" | "percentage") +apply_to: string +min_quantity: number +product?: { +id?: number +reference?: string +name?: string +} +tag?: { +name?: string +} +combined_product?: { +id?: number +reference?: string +name?: string +} +min_subtotal: number +shipping_method?: string +shipping_rule?: ("any" | "all") +regions?: string[] +agent_tag?: string +channel?: string[] +} +/** + * Modelo que representa um cupom de desconto + */ +export interface Coupon { +id?: number +code?: string +uses_per_code?: number +uses_per_user?: number +referrer_email?: string +user_id?: number +updated_at?: string +orders_count?: number +} +/** + * Modelo que representa um produto na API + */ +export interface Product { +/** + * Código identificador `ID` do priduto + */ +id?: number +/** + * Indica se o produto está ativo (`true`) ou invativo (`false`) + */ +active?: boolean +/** + * Indica se o produto está disponível (`true`) ou indisponível (`false`) + */ +available?: boolean +category_tags?: { +/** + * Tipo de tag + */ +tag_type?: string +/** + * Nome da tag + */ +name?: string +/** + * Título da tag + */ +title?: string +}[] +/** + * Descrição do produto + */ +description?: string +/** + * Código de desconto + */ +discount_id?: number +/** + * Descrição do produto em HTML + */ +html_description?: string +/** + * URL da imagem do produto + */ +image_url?: string +/** + * Relação das parcelas para pagamento parcelado + */ +installments?: number[] +/** + * Quantidade mínima para venda do produto + */ +min_quantity?: string +/** + * Nome do produto + */ +name?: string +/** + * Indica se o produto está em promoção (`true`) ou não (`false`) + */ +on_sale?: boolean +/** + * Descrição simplificada + */ +plain_description?: string +/** + * Preço do item + */ +price?: number +/** + * Média de avaliação do produto + */ +rating?: { +rating?: number +votes?: number +} +/** + * Código de referência do produto + */ +reference?: string +/** + * Preço promocional + */ +sale_price?: number +/** + * slug do produto + */ +slug?: string +/** + * Lista de tags que o produto é associado + */ +tag_names?: string[] +/** + * Data e horário da última atualização do produto + */ +updated_at?: string +/** + * URL do produto + */ +url?: string +/** + * Variantes do produto + */ +variants?: { +"{id}"?: ProductVariant +}[] +/** + * Regras de desconto de uma promoção + */ +discount_rule: { +type: ("fixed" | "percentage") +amount: number +} +/** + * Imagens do produto + */ +images?: { +/** + * id do produto + */ +id?: number +/** + * Url do produto + */ +url?: string +/** + * Data e horário da última atualização do produto + */ +updated_at?: string +variant_ids?: { + +}[] +}[] +} +/** + * Modelo que representa uma variante na API + */ +export interface ProductVariant { +available: boolean +available_quantity: number +/** + * Customização da variante + */ +custom_attributes: { + +} +/** + * Dias de manuseio da variante + */ +handling_days: number +height: number +id?: number +/** + * URL da imagem da variante + */ +image_url: string +installments: number[] +inventories?: ProductVariantInventory[] +length: number +main: boolean +/** + * Quantidade mínima para venda + */ +min_quantity: number +/** + * Nome da variante + */ +name: string +norder: number +/** + * Preço do item + */ +price: number +product_id: number +/** + * [Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante + */ +properties: { +property1?: VariantProperty +property2?: VariantProperty +property3?: VariantProperty +} +quantity: number +/** + * Quantidade de itens vendidos + */ +quantity_sold?: number +/** + * Preço promocional + */ +sale_price: number +sku: string +slug: string +/** + * Quantidade de itens disponíveis + */ +stock: number +/** + * Data e horário da última atualização da variante + */ +updated_at: string +/** + * Massa do produto, em gramas + */ +weight: number +/** + * Largura do produto, em centímetros + */ +width: number +required?: [] +additionalProperties?: never +} +/** + * Modelo que representa um inventory da variante na API + */ +export interface ProductVariantInventory { +/** + * Código identificador `ID` do inventário + */ +id: number +/** + * Nome do inventário + */ +name?: string +/** + * Código identificador do local + */ +place_id: number +/** + * Nome do local + */ +place_name?: string +/** + * Preço do item + */ +price: number +/** + * Quantidade de itens no inventário + */ +quantity: number +/** + * Quantidade de itens vendidos + */ +quantity_sold: number +/** + * Preço promocional + */ +sale_price: number +/** + * Slug do inventário + */ +slug: string +/** + * Data e horário da última atualização da variante no inventário + */ +updated_at: string +/** + * Código da variante + */ +variant_id: number +/** + * Data de criação do inventário + */ +created_at: string +} +/** + * Modelo simplificado de um produto para atualização e criação + */ +export interface SimpleProduct { +name: string +description?: string +active?: boolean +reference: string +tag_list?: string +} +/** + * Modelo que representa um produto retornado via busca no Elasticsearch + */ +export interface ProductSearch { +id: number +active: boolean +available: boolean +subscription: boolean +slug: string +reference: string +reference_lowercase: string +name: string +description: string +image_url: string +url: string +tags: { +name: string +title: string +subtitle: string +description: string +importance: number +type: string +image_url: string +}[] +/** + * Preço do item + */ +price: number +on_sale: boolean +/** + * Preço promocional + */ +sale_price: number +intl_price: number +discount_id: number +discount_rule: { +type: ("fixed" | "percentage") +amount: number +} +discount: { +name: string +description: string +/** + * Em desuso + */ +facebook: boolean +valid_to: string +} +images: { +sku: string +url: string +}[] +variants: VariantProductSearch[] +installments: ProductInstallment[] +created_at: string +/** + * Data e horário da última atualização do produto + */ +updated_at: string +} +/** + * Modelo que representa uma variante retornada via busca no Elasticsearch + */ +export interface VariantProductSearch { +id: number +sku: string +sku_lowercase: string +name: string +full_name: string +main: boolean +available: boolean +image_url: string +/** + * Preço do item + */ +price: number +/** + * Preço promocional + */ +sale_price: number +intl_price: number +installments: ProductInstallment[] +/** + * Quantidade de itens disponíveis + */ +stock: number +quantity: number +/** + * Quantidade de itens vendidos + */ +quantity_sold: number +/** + * Quantidade mínima para venda + */ +min_quantity: number +available_quantity: number +/** + * Customização da variante + */ +custom_attributes: { + +} +/** + * [Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante + */ +properties: { +property1?: VariantPropertyProductSearch +property2?: VariantPropertyProductSearch +property3?: VariantPropertyProductSearch +} +inventories: { +name: string +slug: string +available: boolean +/** + * Preço do item + */ +price: number +/** + * Preço promocional + */ +sale_price: number +quantity: number +/** + * Quantidade de itens vendidos + */ +quantity_sold: number +place: { +id: number +name: string +} +}[] +/** + * Dias de manuseio da variante + */ +handling_days: number +barcode: string +/** + * Massa do produto, em gramas + */ +weight: number +/** + * Largura do produto, em centímetros + */ +width: number +/** + * Altura do produto, em centímetros + */ +height: number +/** + * Comprimento do produito, em centímetros + */ +length: number +required?: [] +additionalProperties?: never +} +/** + * Modelo que representa uma parcela + */ +export interface ProductInstallment { +number: number +/** + * Preço do item + */ +price: number +interest: boolean +interest_rate: number +total: number +} +/** + * Modelo que representa uma propriedade de uma variante quando retornada via Elasticsearch + */ +export interface VariantPropertyProductSearch { +name: string +value: string +defining: boolean +} +/** + * Modelo que representa uma propriedade de uma variante + */ +export interface VariantProperty1 { +/** + * Nome da propriedade + */ +name: string +/** + * Valor da propriedade + */ +value: string +/** + * Indica se a variante possui uma definição (`true`) ou se a variante não possui (`false`) + */ +defining: boolean +} +/** + * Modelo que representa uma tag na API + */ +export interface Tag { +name: string +title?: string +subtitle?: string +description?: string +type?: string +products_count?: number +image_url?: string +updated_at?: string +} +/** + * Modelo que representa um cliente na API + */ +export interface Client { +id?: number +first_name?: string +last_name?: string +email?: string +gender?: string +phone_area?: string +phone?: string +document_type?: ("CPF" | "CNPJ") +/** + * Número de documento cadastrado pelo cliente + */ +document_number?: string +cpf?: string +cnpj?: string +ie?: string +tags?: string +lists?: string[] +facebook_uid?: string +liked_facebook_page?: boolean +updated_at?: string +birthdate?: string +recent_address?: { +id?: string +first_name?: string +last_name?: string +company_name?: string +street_name?: string +street_number?: string +neighborhood?: string +complement?: string +reference?: string +city?: string +state?: string +zip?: string +first_phone_area?: string +first_phone?: string +second_phone_area?: string +second_phone?: string +email?: string +documents?: { +cpf?: string +cnpj?: string +} +}[] +auth_token?: string +last_confirmed_order_at?: string +received_orders_count?: number +confirmed_orders_count?: number +canceled_orders_count?: number +renew_password?: boolean +} +/** + * Modelo que representa um endereço na API + */ +export interface Address { +id?: number +first_name?: string +last_name?: string +company_name?: string +email?: string +documents?: { +cpf?: string +cnpj?: string +} +street_name?: string +street_number?: string +complement?: string +neighborhood?: string +first_phone_area?: string +first_phone?: string +second_phone_area?: string +second_phone?: string +reference?: string +zip?: string +city?: string +state?: string +recipient_name?: string +} +/** + * Modelo que representa os endereços cadastrados pelo cliente na API + */ +export interface Client1 { +id?: number +street_name?: string +street_number?: string +complement?: string +neighborhood?: string +label?: string +zip?: string +city?: string +state?: string +reference?: string +client_id?: number +} +/** + * Modelo que representa um bônus na API + */ +export interface Bonus { +amount?: number +token?: string +valid_from?: string +valid_thru?: string +created_at?: string +updated_at?: string +} +/** + * Modelo que representa um menu na API + */ +export interface Menu { +id?: number +label?: string +title?: string +description?: string +url?: string +external?: boolean +parent_id?: number +tag_id?: number +tag_name?: string +page_id?: number +page_slug?: string +items_count?: number +updated_at?: string +tooltip?: string +children?: Menu[] +image_url?: string +simple_url?: string +position?: string +norder?: number +type?: string +} +/** + * Modelo que representa um menu na API quando retornado pela ação de menu em árvore + */ +export interface MenuTree { +id?: number +title?: string +description?: string +external?: boolean +url?: string +tag_id?: number +page_id?: number +items_count?: number +children?: MenuTree[] +updated_at?: string +tooltip?: string +image_url?: string +simple_url?: string +norder?: number +} +/** + * Modelo que representa as mensagens do site na API + */ +export interface SiteMessage { +id?: number +title?: string +description?: string +call_to_action?: string +created_at?: string +updated_at?: string +} +/** + * Modelo que representa as imagens da loja na API + */ +export interface ShopAsset { +id?: number +position?: string +file_uid?: string +file_name?: string +updated_at?: string +} +/** + * Modelo que representa um atributo customizado de produto + */ +export interface ProductsAttributes { +index?: number +name?: string +mandatory?: boolean +updated_at?: string +} +/** + * Modelo que representa uma customização + */ +export interface Customization { +id?: number +group_name?: string +name?: string +label?: string +image_uid?: string +image_name?: string +price?: number +intl_price?: number +quantity?: number +handling_days?: number +tag_id?: number +sku?: string +pattern?: string +} +/** + * Modelo que representa a lista de itens do pedido + */ +export interface OrderItems { +id?: number +variant_id?: number +product_id?: number +quantity?: number +price?: number +weight?: number +width?: number +height?: number +length?: number +extra?: { +customization?: string +} +picture_url?: string +reference?: string +sku?: string +product_name?: string +variant_name?: string +original_price?: string +place_id?: string +place_name?: number +place_city?: number +total?: number +package?: number +has_customizations?: number +barcode?: number +} +/** + * Modelo que representa uma personalização de item do pedido na API + */ +export interface OrderItemCustomization { +/** + * Código identificador `ID` da personalização + */ +id: number +/** + * Número de tipos diferentes de personalizações em produtos do pedido + */ +number: number +/** + * Grupo em que se enquadra a personalização + */ +group_name: string +/** + * Nome do produto + */ +name: string +/** + * Preço do produto + */ +price: number +/** + * Preço internacional + */ +intl_price: number +/** + * Dias de manuseio do produto + */ +handling_days: number +/** + * Código SKU da variante de produto + */ +sku: string +} +/** + * Modelo que representa uma personalização de item do carrinho na API + */ +export interface CartItemCustomization { +/** + * Código identificador do produto + */ +id: number +group_name: string +name: string +number: number +/** + * Preço unitário + */ +price: number +/** + * Preço internacional + */ +intl_price: number +/** + * Número de dias para manuseio + */ +handling_days: number +/** + * Código SKU do produto + */ +sku: string +} +/** + * Modelo que representa um mapeamento na API + */ +export interface Mapping { +id?: number +key: string +from?: string[] +to?: string +created_at?: string +updated_at?: string +} +/** + * Modelo que representa um banner na API + */ +export interface Banner { +big_thumb: string +color: string +description: string +end_at: string +external: boolean +file_name: string +file_uid: string +html_description: string +id: number +norder: number +plain_description: string +small_thumb: string +start_at: string +subtitle: string +tag: string +title: string +updated_at: string +url: string +} +/** + * Modelo que representa um banner simplificado na API + */ +export interface SlimBanner { +id: number +tag: string +title: string +subtitle: string +description: string +url: string +external: boolean +start_at: string +end_at: string +file_url: string +norder: number +color: string +updated_at: string +} +/** + * Modelo que representa as formas de entrega na API + */ +export interface ShippingMethods1 { +/** + * Nome do tipo de entrega, como por exemplo Normal, Expressa e Agendada + */ +name: string +/** + * Identificador do método de envio + */ +value: string +/** + * Preço de envio + */ +price: number +/** + * Descrição do tipo de envio e prazo + */ +description: string +/** + * Número em dias do prazo de envio + */ +delivery_days: number +/** + * Valor restante da compra para que o carrinho fique elegível para frete grátis + */ +value_needed_to_discount?: number +/** + * Código identificador `ID` do tipo de envio + */ +shipping_method_id: number +/** + * Mensagem ou observação sobre a forma de envio + */ +notice?: string +/** + * Empresa responsável pelo envio + */ +fulfillment_company: string +} +/** + * Modelo de endereço de envio para carrinho e pedido + */ +export interface EnderecoDeEnvio { +id?: number +first_name: string +last_name: string +company_name?: string +email: string +/** + * Serão retornados apenas os campos preenchidos + */ +documents?: { +cpf?: string +cnpj?: string +ie?: string +} +street_name: string +street_number: string +complement?: string +neighborhood: string +/** + * Somente números + */ +first_phone_area: string +/** + * Somente números + */ +first_phone: string +/** + * Somente números + */ +second_phone_area?: string +/** + * Somente números + */ +second_phone?: string +reference?: string +/** + * Somente números + */ +zip: string +city: string +state: string +recipient_name?: string +} +/** + * Modelo de endereço de envio para carrinho e pedido + */ +export interface EnderecoDeEnvio1 { +/** + * Nome do cliente + */ +first_name?: string +/** + * Sobrenome do cliente + */ +last_name?: string +/** + * Nome da empresa (para clientes jurídicos) + */ +company_name?: string +/** + * Email do cliente + */ +email?: string +/** + * Código de Discagem Direta à Distância (DDD) + */ +first_phone_area?: string +/** + * Telefone do cliente + */ +first_phone?: string +/** + * Código de Discagem Direta à Distância (DDD) + */ +second_phone_area?: string +/** + * Telefone do cliente + */ +second_phone?: string +/** + * Nome do recebedor + */ +recipient_name?: { +[k: string]: any +} +/** + * Logradouro + */ +street_name?: string +/** + * Número + */ +street_number?: string +/** + * Complemento + */ +complement?: string +/** + * Bairro + */ +neighborhood?: string +/** + * Ponto de referência + */ +reference?: string +/** + * Código de Endereçamento Postal (CEP) + */ +zip: string +documents?: { +/** + * Cadastro de Pessoa Física + */ +cpf?: string +/** + * Registro Geral + */ +rg?: string +/** + * Cadastro Nacional de Pessoas Jurídicas + */ +cnpj?: string +/** + * Inscrição Estadual + */ +ie?: string +}[] +} +/** + * Modelo que representa os preços de uma variante + */ +export interface ProductPriceVariant { +/** + * Define se a variante do produto é a principal + */ +main: boolean +/** + * Código SKU da variante + */ +sku: string +/** + * Preço do item + */ +price: number +on_sale: boolean +/** + * Preço promocional + */ +sale_price: number +intl_price: number +available: boolean +/** + * [Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante + */ +properties: { +property1?: VariantProperty1 +property2?: VariantProperty1 +property3?: VariantProperty1 +} +/** + * Quantidade de itens disponíveis + */ +stock: number +installments: ProductInstallment[] +required?: [] +additionalProperties?: never +} +/** + * Modelo que representa uma imagem de um produto + */ +export interface ProductImage { +/** + * Código identificador `ID` da imagem + */ +id: number +/** + * URL da imagem + */ +url: string +/** + * Data e horário da última atualização da imagem do produto + */ +updated_at: string +/** + * Códigos das variantes que utilizam a imagem + */ +variant_ids: number[] +} +/** + * Modelo que representa um pacote na API + */ +export interface Package { +actual_shipping_method: string +/** + * Código identificador do pacote + */ +code: string +/** + * Data de entrega do pacote + */ +delivered_at: string +delivered_email_sent_at: string +/** + * Número de dias para entrega + */ +delivery_days: number +/** + * Tipo de envio do pacote + */ +delivery_type: string +/** + * Quantidade de dias úteis para entrega + */ +delivery_work_days: number +/** + * Transportadora + */ +fulfillment_company: string +/** + * Status de envio + */ +fulfillment_status: ("waiting" | "shipped" | "delivered") +integrated: boolean +invoiced: boolean +label: string +properties: { + +} +quoted_shipping_price: number +shipped_at: string +shipped_email_sent_at: string +shipping_label: string +shipping_name: string +shipping_price: number +total: number +/** + * Data e horário da última atualização do código de rastreio do pacote + */ +tracked_at: string +/** + * Código de rastreio do pacote + */ +tracking_code: string +required?: [] +additionalProperties?: never +} +/** + * Modelo simplificado de um produto para atualização e criação + */ +export interface SimpleProduct1 { +/** + * Código de Referência do produto + */ +reference: string +/** + * Nome do produto + */ +name: string +/** + * Descrição do produto + */ +description?: string +/** + * Indica se o produto está ativo (`true`) ou invativo (`false`) + */ +active?: boolean +/** + * Tags associadas ao produto + */ +tag_list?: string +/** + * Tipo de produto + */ +product_type?: ("product" | "sample" | "subscription") +} +/** + * Modelo de carcaterística de produto para item no carrinho + */ +export interface Produto1 { +/** + * Código SKU da variante do produto + */ +sku?: string +/** + * Unidades do produto disponíveis fisicamente + */ +quantity?: number +/** + * Campo para registro de observações, chave ou valores necessários + */ +extra?: { + +} +/** + * Código identificador do local do produto + */ +place_id?: number +/** + * Código de cupom + */ +store_coupon_code?: string +/** + * [Personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) do produto + */ +customizations?: { +/** + * [Personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) incluídas no Admin da loja. + * Se por exemplo a customização do produto é a cor, o parâmetro para a requisição deve ser `Color` ao invés de `CUstomization`. + */ +Customization?: string +}[] +} +/** + * Modelo de produto em um pedido + */ +export interface ProdutoEmUmPedido1 { +/** + * Dados extra do produto + */ +extra: { + +} +/** + * Altura do produto, em centímetros. + */ +height?: number +/** + * código identificador do produto + */ +id?: number +/** + * Comprimento do produito, em centímetros. + */ +length?: number +/** + * Preço original + */ +original_price?: number +/** + * Pacote do produto + */ +package?: string +/** + * URL da imagem do produto + */ +picture_url?: string +/** + * Cidade que o produto está + */ +place_city?: string +/** + * Código identificador do local do produto + */ +place_id?: number +/** + * Nome do local do produto + */ +place_name?: string +/** + * Preço do produto + */ +price?: number +product_id: number +product_name: string +/** + * Unidades do produto + */ +quantity: number +/** + * Código de referência do produto + */ +reference: string +/** + * Código SKU da variante do produto + */ +sku: string +/** + * Valor total do produto + */ +total: number +/** + * Código identificador da variante do produto + */ +variant_id: number +/** + * Nome da variante do produto + */ +variant_name: string +/** + * Massa do produto, em gramas + */ +weight: number +/** + * Largura do produto, em centímetros + */ +width: number +/** + * Código de barras do produto + */ +barcode?: string +/** + * Indica se o produto possui customização. + */ +has_customizations?: boolean +} diff --git a/vnda/utils/openapi/vnda.openapi.json b/vnda/utils/openapi/vnda.openapi.json new file mode 100644 index 000000000..cb8536cc3 --- /dev/null +++ b/vnda/utils/openapi/vnda.openapi.json @@ -0,0 +1,19471 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "API", + "version": "v2", + "contact": { + "name": "Dúvidas e suporte para API, envie um e-mail para", + "email": "produto@vnda.com.br" + }, + "description": "API versão 2 da Vnda E-commerce.\nSaiba mais no nosso [Guia de API](https://developers.vnda.com.br/docs/chave-de-acesso-e-requisicoes)", + "license": { + "name": "API Vnda", + "url": "https://www.vnda.com.br/" + } + }, + "servers": [ + { + "url": "https://api.vnda.com.br", + "description": "Servidor do ambiente de produção" + }, + { + "url": "https://api.sandbox.vnda.com.br", + "description": "Servidor do ambiente de testes" + } + ], + "paths": { + "/api/v2/products/{product_id}/videos": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "product_id", + "in": "path", + "required": true + } + ], + "get": { + "responses": { + "200": { + "description": "Retorna os videos do produto", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "url": { + "type": "string" + }, + "embed_url": { + "type": "string" + }, + "thumbnail_url": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "variant_ids": { + "type": "number" + } + } + } + } + } + } + } + } + } + }, + "/api/v2/seo_data": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "resource_id": { + "type": "integer" + }, + "parent_id": { + "type": "number" + } + }, + "required": [ + "id", + "resource_type", + "resource_id", + "parent_id" + ] + } + } + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "resource_type", + "in": "query" + }, + { + "schema": { + "type": "integer" + }, + "name": "resource_id", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "name": "type", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "query" + } + ] + } + }, + "/api/v2/users/authorize": { + "post": { + "summary": "User authorize", + "responses": { + "200": { + "description": "Retornado quando o access token do usuário ainda é válido e a senha está correta" + }, + "401": { + "description": "Retornado quando o access_token não é mais válido e/ou a senha está incorreta" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "access_token": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "type": "object", + "required": [ + "access_token", + "password" + ] + } + } + }, + "description": "" + }, + "description": "Permite autorizar operações usando o access_token e a senha do usuário", + "parameters": [], + "operationId": "post-api-v2-users-authorize", + "tags": [ + "Usuários" + ] + } + }, + "/api/v2/users/login": { + "post": { + "summary": "Faz o login do usuário", + "responses": { + "200": { + "description": "Quando o usuário e a senha são válidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User.v1" + } + } + } + }, + "401": { + "description": "Quando o usuário e/ou a senha não são válidos ou não foram passados" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + } + }, + "type": "object", + "required": [ + "email", + "password" + ] + } + } + }, + "description": "" + }, + "description": "Realiza o login do usuário a partir do email e da senha", + "parameters": [], + "operationId": "post-api-v2-users-login", + "tags": [ + "Usuários" + ] + } + }, + "/api/v2/users/logout": { + "post": { + "summary": "Faz o logout do usuário", + "responses": { + "200": { + "description": "Quando o usuário atual existe" + }, + "404": { + "description": "Quando o usuário atual não existe" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "access_token": { + "type": "string", + "description": "Token de validação de usuário logado\n\nO `access_token` é gerado quando o usuário loga no Admin " + } + }, + "type": "object" + } + } + }, + "description": "" + }, + "description": "Realiza o logout do usuário a partir do access_token do mesmo", + "parameters": [], + "operationId": "post-api-v2-users-logout", + "tags": [ + "Usuários" + ] + } + }, + "/api/v2/users/{id}": { + "get": { + "summary": "User", + "responses": { + "200": { + "description": "Retorna os dados do usuário", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User.v1" + }, + "examples": { + "Usuário": { + "value": { + "id": 1, + "email": "foo@vnda.com.br", + "name": null, + "admin": false, + "renew_password": false, + "role": 1, + "access_token": "706a99d0706a99d070006a99d0706a99d0706a99d0706a99d0706a99d0706a99d0", + "tags": [], + "external_code": null, + "created_at": "2019-11-06T08:50:37.130-03:00", + "updated_at": "2020-03-26T10:40:33.730-03:00" + } + } + } + } + } + } + }, + "parameters": [], + "tags": [ + "Usuários" + ], + "operationId": "get-api-v2-users-id", + "description": "Retorna os dados de um usuário pelo seu ID" + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "put": { + "summary": "Atualiza um usuário", + "operationId": "put-api-v2-users-id", + "responses": { + "204": { + "description": "Quando o usuário é atualizado" + }, + "404": { + "description": "Quando o usuário não existe", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Usuários" + ], + "description": "Atualiza um usuário", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "role_name": { + "type": "string", + "enum": [ + "Agente", + "Gestor", + "Local" + ] + }, + "password": { + "type": "string" + }, + "password_confirmation": { + "type": "string" + }, + "external_code": { + "type": "string" + }, + "phone_area": { + "type": "string", + "maxLength": 2 + }, + "phone": { + "type": "string", + "maxLength": 9 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/v2/credits/rules/versions": { + "get": { + "summary": "Lista as versões da regra de bônus", + "tags": [ + "Créditos" + ], + "responses": { + "200": { + "description": "Quando as versões da regra são retornadas", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "event": { + "type": "string" + }, + "author": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "user_agent": { + "type": "string" + }, + "cart_id": { + "type": "string" + }, + "object_changes": { + "type": "string" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-credits-rules-versions", + "parameters": [], + "description": "Retorna as versões da regra de bônus cadastrada" + } + }, + "/api/v2/credits/rules": { + "get": { + "summary": "Regras de bônus", + "tags": [ + "Créditos" + ], + "responses": { + "200": { + "description": "Quando as regras são retornadas", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "minimum_subtotal": { + "type": "number", + "minimum": 0 + }, + "bonus": { + "type": "number", + "minimum": 1 + }, + "delayed_for": { + "type": "number", + "minimum": 0 + }, + "valid_for": { + "type": "number", + "minimum": 1, + "exclusiveMinimum": false + }, + "maximum_usage_factor": { + "type": "number", + "minimum": 0, + "maximum": 1, + "exclusiveMaximum": false, + "exclusiveMinimum": true + } + }, + "required": [ + "active", + "minimum_subtotal", + "bonus", + "delayed_for", + "valid_for", + "maximum_usage_factor" + ] + }, + "examples": { + "example-1": { + "value": { + "active": true, + "minimum_subtotal": 100, + "bonus": 10, + "delayed_for": 5, + "valid_for": 30, + "maximum_usage_factor": 0.3 + } + } + } + } + } + } + }, + "operationId": "get-api-v2-credits-rules", + "parameters": [], + "description": "Retorna as regras de bônus cadastradas" + }, + "put": { + "summary": "Atualiza a regras de bônus", + "operationId": "put-api-v2-credits-rules", + "responses": { + "204": { + "description": "Quando a regra é atualizada" + } + }, + "description": "Permite atualizar as regras de bônus", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bonus": { + "type": "number", + "format": "float", + "minimum": 1, + "exclusiveMinimum": false, + "description": "Percentual em cima do total do pedido que vai ser dado de bônus para o cliente" + }, + "valid_in": { + "type": "integer", + "description": "Número de dias em que o crédito começa a valer", + "minimum": 0 + }, + "valid_for": { + "type": "integer", + "description": "Número de dias para a expiração do crédito", + "minimum": 1 + }, + "minimum_subtotal": { + "type": "number", + "description": "Valor mínimo do pedido para que o bônus possa ser transferido para o cliente", + "format": "float", + "minimum": 1, + "exclusiveMinimum": false + }, + "maximum_usage_factor": { + "type": "number", + "default": 1, + "minimum": 0, + "exclusiveMinimum": true, + "maximum": 1, + "description": "Percentual do subtotal do pedido que pode ser pago com o bônus" + } + }, + "required": [ + "bonus", + "valid_in", + "valid_for" + ] + } + } + }, + "description": "Parâmetros" + }, + "parameters": [], + "tags": [ + "Créditos" + ] + }, + "delete": { + "summary": "Remove as regras de bônus", + "operationId": "delete-api-v2-credits-rules", + "responses": { + "204": { + "description": "Quando o bônus é removido" + } + }, + "tags": [ + "Créditos" + ], + "description": "Permite remover as regras de bônus, desativando o recurso" + } + }, + "/api/v2/orders/{code}": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 10, + "maxLength": 64 + }, + "name": "code", + "in": "path", + "required": true, + "description": "O \"code\" do pedido ou o \"token\"" + }, + { + "schema": { + "type": "boolean", + "default": false + }, + "in": "query", + "name": "include_customizations_in_total", + "description": "Inclui o preço dos produtos customizados no total do pedido" + } + ], + "get": { + "summary": "Retorna um pedido", + "tags": [ + "Pedidos" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order.v1" + } + } + } + } + }, + "operationId": "get-api-v2-orders-code", + "description": "Retorna os dados de um pedido usando o `code` ou `token`", + "parameters": [ + { + "schema": { + "type": "boolean", + "default": false + }, + "in": "query", + "name": "include_shipping_address", + "description": "Retorna as formas de entrega do pedido" + } + ] + } + }, + "/api/v2/orders": { + "get": { + "summary": "Lista os pedidos", + "tags": [ + "Pedidos" + ], + "responses": { + "200": { + "description": "Quando os pedidos são retornados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order.v1" + } + } + } + } + }, + "404": { + "description": "Domínio de loja não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-orders", + "description": "Retorna uma lista de pedidos", + "parameters": [ + { + "$ref": "#/components/parameters/start" + }, + { + "$ref": "#/components/parameters/finish" + }, + { + "$ref": "#/components/parameters/invoiced" + }, + { + "$ref": "#/components/parameters/page" + }, + { + "$ref": "#/components/parameters/per_page" + }, + { + "$ref": "#/components/parameters/coupon_codes" + }, + { + "$ref": "#/components/parameters/include_customizations_in_total" + } + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/start" + }, + { + "$ref": "#/components/parameters/finish" + }, + { + "$ref": "#/components/parameters/invoiced" + }, + { + "$ref": "#/components/parameters/page" + }, + { + "$ref": "#/components/parameters/per_page" + }, + { + "$ref": "#/components/parameters/coupon_codes" + }, + { + "$ref": "#/components/parameters/include_customizations_in_total" + } + ] + }, + "/api/v2/orders/{code}/capture": { + "post": { + "summary": "Captura", + "operationId": "post-api-v2-orders-capture", + "responses": { + "200": { + "description": "Quando a captura foi realizada com sucesso", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + }, + "examples": { + "Pagar.me": { + "value": { + "object": "transaction", + "status": "paid", + "refuse_reason": null, + "status_reason": "acquirer", + "acquirer_response_code": "0000", + "acquirer_name": "pagarme", + "acquirer_id": "5eab10915eab10915eab1091", + "authorization_code": "123456", + "soft_descriptor": "", + "tid": 1234567, + "nsu": 1234567, + "date_created": "2020-05-14T19:14:50.322Z", + "date_updated": "2020-05-15T14:19:34.699Z", + "amount": 1400, + "authorized_amount": 1400, + "paid_amount": 1400, + "refunded_amount": 0, + "installments": 1, + "id": 1234567, + "cost": 120, + "card_holder_name": "John Doe", + "card_last_digits": "6565", + "card_first_digits": "470373", + "card_brand": "visa", + "card_pin_mode": null, + "card_magstripe_fallback": false, + "cvm_pin": false, + "postback_url": "https://demo.vnda.com.br/v2/payments/pagarme/notifications", + "payment_method": "credit_card", + "capture_method": "ecommerce", + "antifraud_score": null, + "boleto_url": null, + "boleto_barcode": null, + "boleto_expiration_date": null, + "referer": "api_key", + "ip": "127.0.0.1", + "subscription_id": null, + "phone": null, + "address": null, + "customer": { + "object": "customer", + "id": 2954669, + "external_id": "example@vnda.com.br", + "type": "individual", + "country": "br", + "document_number": null, + "document_type": "cpf", + "name": "John Doe", + "email": "example@vnda.com.br", + "phone_numbers": [ + "+5511111111111" + ], + "born_at": null, + "birthday": null, + "gender": null, + "date_created": "2020-05-14T19:14:50.248Z", + "documents": [ + { + "object": "document", + "id": "doc_cka75cka75cka75cka75cka75", + "type": "cpf", + "number": 191 + } + ] + }, + "billing": { + "object": "billing", + "id": 1255695, + "name": "John Doe", + "address": { + "object": "address", + "street": "Rua João Neves da Fontoura", + "complementary": null, + "street_number": "1", + "neighborhood": "Azenha", + "city": "Porto Alegre", + "state": "RS", + "zipcode": "90050030", + "country": "br", + "id": 2808888 + } + }, + "shipping": null, + "items": [ + { + "object": "item", + "id": "05.01.4.1.006", + "title": "Aceto Balsâmico Di Modena IGP 500ml Aceto Balsamico Di Modena IGP 500ml", + "unit_price": 1400, + "quantity": 1, + "category": null, + "tangible": true, + "venue": null, + "date": null + } + ], + "card": { + "object": "card", + "id": "card_cka75cka75cka75cka75cka75", + "date_created": "2020-05-14T19:14:50.307Z", + "date_updated": "2020-05-14T19:14:50.717Z", + "brand": "visa", + "holder_name": "f dc", + "first_digits": "470373", + "last_digits": "6565", + "country": "RUSSIA", + "fingerprint": "cka75cka75cka75cka75cka75", + "valid": true, + "expiration_date": "0423" + }, + "split_rules": null, + "metadata": { + "order": "7A4F490570", + "seller-1": { + "name": "default", + "package": "7A4F490570-01" + } + }, + "antifraud_metadata": {}, + "reference_key": null, + "device": null, + "local_transaction_id": null, + "local_time": null, + "fraud_covered": false, + "fraud_reimbursed": null, + "order_id": null, + "risk_level": "very_low", + "receipt_url": null, + "payment": null, + "addition": null, + "discount": null, + "private_label": null + } + } + } + } + } + }, + "422": { + "description": "Quando não foi possível realizar a captura junto ao adquirente", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "failure": { + "value": { + "error": "Capture was unsuccessful" + } + } + } + } + } + } + }, + "description": "Faz a captura do pagamento no adquirente\nApenas para pedidos pagos com cartão de crédito", + "tags": [ + "Pedidos" + ] + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "path", + "required": true + } + ] + }, + "/api/v2/orders/{code}/confirm": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Confirma", + "operationId": "post-api-v2-orders-code-confirm", + "responses": { + "200": { + "description": "OK" + } + }, + "description": "Altera o status do pedido para \"confirmado\"", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "confirmation_data": { + "type": "string", + "description": "Para cartão de crédito deve ser enviado OBRIGATORIAMENTE o retorno da requisição para \"/api/v2/orders/{code}/capture\"" + } + } + }, + "examples": { + "Depósito": { + "value": { + "banco": "Banco do Brasil", + "data_credito": "2020-03-26", + "conferido_por": "Nome do usuário do financeiro" + } + }, + "Cartão de crédito via Pagar.me": { + "value": { + "object": "transaction", + "status": "paid", + "refuse_reason": null, + "status_reason": "acquirer", + "acquirer_response_code": "0000", + "acquirer_name": "pagarme", + "acquirer_id": "5eab10915eab10915eab1091", + "authorization_code": "123456", + "soft_descriptor": "", + "tid": 1234567, + "nsu": 1234567, + "date_created": "2020-05-14T19:14:50.322Z", + "date_updated": "2020-05-15T14:19:34.699Z", + "amount": 1400, + "authorized_amount": 1400, + "paid_amount": 1400, + "refunded_amount": 0, + "installments": 1, + "id": 1234567, + "cost": 120, + "card_holder_name": "John Doe", + "card_last_digits": "6565", + "card_first_digits": "470373", + "card_brand": "visa", + "card_pin_mode": null, + "card_magstripe_fallback": false, + "cvm_pin": false, + "postback_url": "https://demo.vnda.com.br/v2/payments/pagarme/notifications", + "payment_method": "credit_card", + "capture_method": "ecommerce", + "antifraud_score": null, + "boleto_url": null, + "boleto_barcode": null, + "boleto_expiration_date": null, + "referer": "api_key", + "ip": "127.0.0.1", + "subscription_id": null, + "phone": null, + "address": null, + "customer": { + "object": "customer", + "id": 2954669, + "external_id": "example@vnda.com.br", + "type": "individual", + "country": "br", + "document_number": null, + "document_type": "cpf", + "name": "John Doe", + "email": "example@vnda.com.br", + "phone_numbers": [ + "+5511111111111" + ], + "born_at": null, + "birthday": null, + "gender": null, + "date_created": "2020-05-14T19:14:50.248Z", + "documents": [ + { + "object": "document", + "id": "doc_cka75cka75cka75cka75cka75", + "type": "cpf", + "number": 191 + } + ] + }, + "billing": { + "object": "billing", + "id": 1255695, + "name": "John Doe", + "address": { + "object": "address", + "street": "Rua João Neves da Fontoura", + "complementary": null, + "street_number": "1", + "neighborhood": "Azenha", + "city": "Porto Alegre", + "state": "RS", + "zipcode": "90050030", + "country": "br", + "id": 2808888 + } + }, + "shipping": null, + "items": [ + { + "object": "item", + "id": "05.01.4.1.006", + "title": "Aceto Balsâmico Di Modena IGP 500ml Aceto Balsamico Di Modena IGP 500ml", + "unit_price": 1400, + "quantity": 1, + "category": null, + "tangible": true, + "venue": null, + "date": null + } + ], + "card": { + "object": "card", + "id": "card_cka75cka75cka75cka75cka75", + "date_created": "2020-05-14T19:14:50.307Z", + "date_updated": "2020-05-14T19:14:50.717Z", + "brand": "visa", + "holder_name": "f dc", + "first_digits": "470373", + "last_digits": "6565", + "country": "RUSSIA", + "fingerprint": "cka75cka75cka75cka75cka75", + "valid": true, + "expiration_date": "0423" + }, + "split_rules": null, + "metadata": { + "order": "7A4F490570", + "seller-1": { + "name": "default", + "package": "7A4F490570-01" + } + }, + "antifraud_metadata": {}, + "reference_key": null, + "device": null, + "local_transaction_id": null, + "local_time": null, + "fraud_covered": false, + "fraud_reimbursed": null, + "order_id": null, + "risk_level": "very_low", + "receipt_url": null, + "payment": null, + "addition": null, + "discount": null, + "private_label": null + } + } + } + } + }, + "description": "" + }, + "tags": [ + "Pedidos" + ] + } + }, + "/api/v2/orders/{code}/chargeback": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Estorna", + "operationId": "post-api-v2-orders-code-chargeback", + "responses": { + "200": { + "description": "OK" + }, + "422": { + "description": "Unprocessable Entity" + } + }, + "description": "Faz o estorno do pagamento no adquirente\nApenas para pedidos pagos com cartão de crédito", + "tags": [ + "Pedidos" + ] + } + }, + "/api/v2/orders/{code}/cancel": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Cancela", + "operationId": "post-api-v2-orders-code-cancel", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "description": "Altera o status do pedido para \"cancelado\"", + "tags": [ + "Pedidos" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cancelation_data": { + "type": "string", + "description": "Deve ser enviado algo que comprove que o pagamento foi devolvido.\nPara cartão de crédito deve ser enviado OBRIGATORIAMENTE o retorno da requisição para \"/api/v2/orders/{code}/chargeback\"" + } + } + } + } + } + } + } + }, + "/api/v2/orders/{order_code}/packages/{package_code}/ship": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "order_code", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "package_code", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Altera para enviado", + "operationId": "patch-api-v2-orders-code-packages-code-ship", + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Pedido ou rastreio não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "description": "Altera o status do pacote para \"enviado\"", + "tags": [ + "Pacotes" + ], + "parameters": [] + } + }, + "/api/v2/orders/{order_code}/packages/{package_code}/deliver": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "order_code", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "package_code", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Altera para entregue", + "operationId": "patch-api-v2-orders-code-packages-code-deliver", + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Pedido ou rastreio não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "description": "Altera o pacote para \"entregue\"", + "tags": [ + "Pacotes" + ] + } + }, + "/api/v2/variants/quantity": { + "post": { + "summary": "Atualiza em lote", + "operationId": "post-api-v2-variants-quantity", + "responses": { + "200": { + "description": "OK" + } + }, + "tags": [ + "Estoque" + ], + "description": "Recebe uma lista JSON com os SKUs que devem ser atualizados. A atualização será executada em segundo plano em aproximadamente 1 minuto ", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "place_id": { + "type": "integer", + "description": "Informe somente para atualizar o estoque de um local específico" + } + }, + "required": [ + "sku", + "quantity" + ] + } + }, + "examples": { + "Exemplo": { + "value": [ + { + "sku": "21390", + "quantity": 12, + "place_id": 1 + }, + { + "sku": "21827", + "quantity": 12 + } + ] + } + } + } + }, + "description": "" + } + } + }, + "/api/v2/variants/{sku}/quantity": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "sku", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Atualiza", + "operationId": "post-api-v2-variants-sku-quantity", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "examples": { + "Exemplo": { + "value": { + "status": "ok" + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "examples": { + "Exemplo": { + "value": { + "status": "error", + "message": "A quantity must be provided to update stock" + } + } + } + } + } + } + }, + "tags": [ + "Estoque" + ], + "description": "Atualiza o estoque de uma variante de um produto", + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "quantity", + "required": true + } + ] + } + }, + "/api/v2/variants/{sku}/inventories/{place_id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "sku", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "place_id", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Atualiza um local", + "operationId": "patch-api-v2-variants-sku-inventories-place_id", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "examples": { + "Exemplo": { + "value": { + "status": "ok" + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "examples": { + "Exemplo": { + "value": { + "status": "error", + "message": "Quantity or price must be provided to update stock" + } + } + } + } + } + } + }, + "tags": [ + "Estoque" + ], + "description": "Atualiza o estoque específico de um local", + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "quantity" + } + ] + } + }, + "/api/v2/products/{product_id}/variants": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "product_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista as variantes", + "tags": [ + "Variantes" + ], + "responses": { + "200": { + "description": "Quando as variantes são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Variant" + } + }, + "examples": { + "example-1": { + "value": [ + { + "id": 95, + "main": true, + "available": true, + "sku": "123", + "name": "Variation", + "slug": "variation", + "min_quantity": 1, + "quantity": 1, + "quantity_sold": 0, + "stock": 1, + "custom_attributes": {}, + "properties": {}, + "updated_at": "2020-10-27T11:54:32.018-03:00", + "price": 10, + "installments": [ + 10 + ], + "available_quantity": 1, + "weight": 0.001, + "width": 1, + "height": 1, + "length": 1, + "handling_days": 0, + "inventories": [], + "sale_price": 10, + "image_url": "//b0.vnda.com.br/x120/shop/2014/07/08/variation.jpg", + "product_id": 6, + "barcode": null, + "norder": 1 + }, + { + "id": 27, + "main": false, + "available": true, + "sku": "13001", + "name": "Tamanho: PP | Cor: Branca", + "slug": "camiseta", + "min_quantity": 1, + "quantity": 85, + "stock": 83, + "custom_attributes": { + "size": "PP", + "color": "#FFFFFF" + }, + "properties": {}, + "updated_at": "2019-08-01T18:36:52.718-03:00", + "price": 169.9, + "installments": [ + 169.9 + ], + "available_quantity": 83, + "weight": 0.1, + "width": 11, + "height": 2, + "length": 16, + "handling_days": 0, + "inventories": [], + "sale_price": 169.9, + "image_url": "//b0.vnda.com.br/x120/shop/2014/07/08/camiseta.jpg", + "product_id": 6, + "barcode": null, + "norder": 1 + } + ] + } + } + } + } + }, + "400": { + "description": "Parâmetros enviados inválidos", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "example-1": { + "value": { + "error": "invalid rate value" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-products-product_id-variants", + "description": "Permite listar as variantes de um produto" + }, + "post": { + "summary": "Cria uma variante", + "operationId": "post-api-v2-products-product_id-variants", + "responses": { + "201": { + "description": "Quando a variante é criada", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "main": { + "type": "boolean" + }, + "available": { + "type": "boolean" + }, + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "min_quantity": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "stock": { + "type": "integer", + "description": "Quantidade de itens disponíveis" + }, + "custom_attributes": { + "type": "object", + "description": "Customização da variante" + }, + "properties": { + "type": "object" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização" + }, + "price": { + "type": "integer" + }, + "installments": { + "type": "array", + "items": { + "type": "integer" + } + }, + "available_quantity": { + "type": "integer" + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + }, + "height": { + "type": "number", + "description": "Altura do produto, em centímetros" + }, + "length": { + "type": "number", + "description": "Comprimento do produito, em centímetros" + }, + "handling_days": { + "type": "number", + "description": "Dias de manuseio da variante" + }, + "inventories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Variant_inventory.v1" + } + }, + "sale_price": { + "type": "number" + }, + "image_url": { + "type": "string" + }, + "product_id": { + "type": "integer" + }, + "norder": { + "type": "integer" + } + } + }, + "examples": { + "example-1": { + "value": { + "id": 95, + "main": false, + "available": true, + "sku": "SHOP0001", + "name": "Variation", + "slug": "variation", + "min_quantity": 1, + "quantity": 1, + "stock": 1, + "custom_attributes": {}, + "properties": {}, + "updated_at": "2020-10-27T11:54:32.018-03:00", + "price": 10, + "installments": [ + 10 + ], + "available_quantity": 1, + "weight": 0.001, + "width": 1, + "height": 1, + "length": 1, + "handling_days": 0, + "inventories": [], + "sale_price": 10, + "image_url": "//b0.vnda.com.br/x120/shop/2014/07/08/variation.jpg", + "product_id": 6, + "norder": 1 + } + } + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Variantes" + ], + "description": "Permite criar uma variante", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "main": { + "type": "boolean" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + }, + "height": { + "type": "number", + "description": "Altura do produto, em centímetros" + }, + "length": { + "type": "number", + "description": "Comprimento do produito, em centímetros" + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "handling_days": { + "type": "integer", + "description": "Dias de manuseio da variante" + }, + "price": { + "type": "number" + }, + "custom_attributes": { + "type": "object", + "description": "Customização da variante" + }, + "min_quantity": { + "type": "integer" + }, + "norder": { + "type": "integer" + }, + "property1": { + "type": "string", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "defining": { + "type": "boolean" + } + } + }, + "property2": { + "type": "string", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "defining": { + "type": "boolean" + } + } + }, + "property3": { + "type": "string", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "defining": { + "type": "boolean" + } + } + }, + "barcode": { + "type": "string" + } + }, + "required": [ + "sku", + "quantity", + "price" + ] + } + } + } + } + } + }, + "/api/v2/products/{product_id}/variants/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "product_id", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Atualiza uma variante", + "operationId": "patch-api-v2-products-product_id-variants-id", + "responses": { + "204": { + "description": "Quando a variante é atualizada" + }, + "404": { + "description": "Quando a variante não existe", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Variantes" + ], + "description": "Permite atualizar uma variante", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "main": { + "type": "boolean" + }, + "width": { + "type": "number" + }, + "height": { + "type": "number" + }, + "length": { + "type": "number" + }, + "weight": { + "type": "number" + }, + "handling_days": { + "type": "integer" + }, + "price": { + "type": "number" + }, + "custom_attributes": { + "type": "object" + }, + "min_quantity": { + "type": "integer" + }, + "norder": { + "type": "integer" + }, + "property1": { + "type": "string" + }, + "property2": { + "type": "string" + }, + "property3": { + "type": "string" + }, + "barcode": { + "type": "string" + }, + "quantity_sold": { + "type": "integer" + } + }, + "required": [ + "sku", + "quantity", + "price" + ] + } + } + } + }, + "deprecated": true + }, + "delete": { + "summary": "Remove uma variante", + "operationId": "delete-api-v2-products-product_id-variants-id", + "responses": { + "204": { + "description": "Quando a variante é removida" + }, + "404": { + "description": "Quando a variante não existe", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Variantes" + ], + "description": "Permite remover uma variante" + } + }, + "/api/v2/variants/reorder": { + "post": { + "summary": "Reordena as variantes", + "operationId": "post-api-v2-variants-reorder", + "responses": { + "200": { + "description": "Quando as variantes são reordenadas" + } + }, + "tags": [ + "Variantes" + ], + "description": "Permite determinar a ordem das variantes dentro de cada produto", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "description": "A ordem dos elementos será replicada para as variantes", + "items": { + "type": "integer" + } + } + }, + "required": [ + "ids" + ] + }, + "examples": { + "example-1": { + "value": { + "ids": [ + 32, + 29, + 28, + 31, + 30, + 27 + ] + } + } + } + } + } + } + } + }, + "/api/v2/templates/{path}": { + "get": { + "summary": "Retorna um template", + "tags": [ + "Templates" + ], + "responses": { + "200": { + "description": "Quando um template é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Template.v1" + }, + "examples": { + "Template": { + "value": { + "path": "home.liquid", + "body": "

shop site!

", + "updated_at": "2020-05-17T21:37:38.000-03:00" + } + }, + "Partial": { + "value": { + "path": "partials/components/product_block/_images_by_gender.liquid", + "body": "

partial template

", + "updated_at": "2020-05-17T21:37:38.000-03:00" + } + } + } + } + } + }, + "404": { + "description": "Quando um template não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-templates-path", + "description": "Retorna um template usando o path dele", + "parameters": [] + }, + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "[0-9A-Za-z_\\.\\/]+" + }, + "name": "path", + "in": "path", + "required": true, + "description": "Caminho relativo do template" + } + ], + "patch": { + "summary": "Atualiza um template", + "operationId": "patch-api-v2-templates-path", + "responses": { + "204": { + "description": "Quando um template é atualizado com sucesso" + }, + "404": { + "description": "Quando um template não é contrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "tags": [ + "Templates" + ], + "parameters": [], + "description": "Atualiza o conteúdo de um template usando o path dele", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + } + }, + "examples": { + "Template": { + "value": { + "body": "

shop site!

" + } + }, + "Partial": { + "value": { + "body": "

partial template

" + } + } + } + } + } + } + }, + "delete": { + "summary": "Remove um template", + "operationId": "delete-api-v2-templates-path", + "responses": { + "204": { + "description": "Quando um template é excluído com sucesso" + }, + "404": { + "description": "Quando um template não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "description": "Remove um template usando o path dele", + "tags": [ + "Templates" + ], + "parameters": [] + } + }, + "/api/v2/templates": { + "post": { + "summary": "Cria um template", + "operationId": "post-api-v2-templates", + "responses": { + "201": { + "description": "Quando um template é criado com sucesso", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Template.v1" + }, + "examples": { + "Template": { + "value": { + "path": "home.liquid", + "body": "

shop site!

", + "updated_at": "2020-05-17T21:37:38.000-03:00" + } + }, + "Partial": { + "value": { + "path": "partials/components/product_block/_images_by_gender.liquid", + "body": "

partial template

", + "updated_at": "2020-05-17T21:37:38.000-03:00" + } + } + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + }, + "examples": { + "Parâmetro `path` em branco": { + "value": { + "errors": { + "path": [ + "não pode ficar em branco" + ] + } + } + } + } + } + } + } + }, + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "body": { + "type": "string" + } + }, + "required": [ + "path" + ] + }, + "examples": { + "Template": { + "value": { + "path": "home.liquid", + "body": "

shop site!

" + } + }, + "Partial": { + "value": { + "path": "partials/components/product_block/_images_by_gender.liquid", + "body": "

partial template

" + } + } + } + } + } + }, + "tags": [ + "Templates" + ], + "description": "Cria um novo template" + }, + "get": { + "summary": "Lista os templates", + "operationId": "get-api-v2-templates", + "responses": { + "200": { + "description": "Quando os templates são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Template.v1" + } + }, + "examples": { + "Template": { + "value": [ + { + "path": "home.liquid", + "body": "

shop site!

", + "updated_at": "2020-05-17T21:37:38.000-03:00" + } + ] + }, + "Partial": { + "value": [ + { + "path": "partials/components/product_block/_images_by_gender.liquid", + "body": "

partial template

", + "updated_at": "2020-05-17T21:37:38.000-03:00" + } + ] + }, + "Sem templates": { + "value": [] + } + } + } + } + } + }, + "description": "Retorna uma lista de templates", + "parameters": [], + "tags": [ + "Templates" + ] + } + }, + "/api/v2/users/{id}/activate": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Ativar", + "operationId": "post-api-v2-users-id-activate", + "responses": { + "200": { + "description": "Quando a ativação foi realizada com sucesso" + }, + "404": { + "description": "Quando o usuário não é encontrado" + } + }, + "description": "Reativa um usuário que estiver desativado", + "tags": [ + "Usuários" + ] + } + }, + "/api/v2/users/{id}/deactivate": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Desativar", + "operationId": "post-api-v2-users-id-deactivate", + "responses": { + "200": { + "description": "Quando a desativação foi realizada com sucesso" + }, + "404": { + "description": "Quando o usuário não é encontrado" + } + }, + "tags": [ + "Usuários" + ], + "description": "Desativa um usuário" + } + }, + "/api/v2/users": { + "get": { + "summary": "Lista os usuários", + "operationId": "get-api-v2-users", + "responses": { + "200": { + "description": "Quando os usuários são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User.v1", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Código identificador do usuário" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email do usuário" + }, + "access_token": { + "type": "string", + "description": "Token de validação de usuário logado (`access_token`)\n \nO `access_token` é gerado quando o usuário loga no Admin" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Nome do usuário" + }, + "admin": { + "type": "boolean", + "description": "Identificador de usuários administradores\n\nEsse atributo retorna `true` para um usuário administrador do ambiente de loja" + }, + "renew_password": { + "type": "boolean", + "description": "Identificador de usuários que atualizaram a senha inicial\n\nEsse atributo retorna `true` para um usuário que já redefiniu sua senha pelo menos uma vez" + }, + "role": { + "type": "integer", + "description": "Código da função do usuário na loja:\n\n - Agente: `0`;\n - Gestor: `1`;\n - Local: `2`;\n - Agente Social Selling: `3`." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags para agrupamento de usuários\nAs tags podem ser são utilizadas para direcionar promoções para determinados usuários, organizar os recebedores em uma divisão de pagamentos, definir regras de comissão" + }, + "external_code": { + "type": "string", + "nullable": true, + "description": "Código externo do Vendedor. Esse campo é destinado para cadastrar um código de vendedor já existente em outro sistema." + }, + "phone_area": { + "type": "string", + "maxLength": 2, + "minLength": 2, + "description": "Código de Discagem Direta a Distância (DDD) do telefone do usuário" + }, + "phone": { + "type": "string", + "maxLength": 9, + "minLength": 8, + "description": "Número de telefone do usuário" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Data de inclusão do usuário no Admin" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data de atualização das informações do usuário" + } + } + } + }, + "examples": { + "Usuários": { + "value": [ + { + "id": 1, + "email": "foo@vnda.com.br", + "name": null, + "admin": false, + "renew_password": false, + "role": 1, + "access_token": "706a99d0706a99d0706a99d0706a99d0706a99d0706a99d0706a99d0706a99d0", + "tags": [], + "external_code": null, + "created_at": "2019-11-06T08:50:37.130-03:00", + "updated_at": "2020-03-26T10:40:33.730-03:00" + } + ] + } + }, + "example": [ + { + "id": 1, + "email": "foo@vnda.com.br", + "name": "User 1", + "admin": true, + "renew_password": true, + "role": 1, + "access_token": "706a99d0706a99d070006a99d0706a99d0706a99d0706a99d0706a99d0706a99d0", + "tags": [], + "external_code": null, + "created_at": "2019-11-06T08:50:37.130-03:00", + "updated_at": "2020-03-26T10:40:33.730-03:00" + }, + { + "id": 1, + "email": "test@vnda.com.br", + "name": "User 2", + "admin": false, + "renew_password": false, + "role": 2, + "access_token": "706a99d0706a99dhgs070006a99d0706a99d0706a99d0706a99d0706a99d0706a99d0", + "tags": [], + "external_code": null, + "created_at": "2019-12-06T08:50:37.130-03:00", + "updated_at": "2020-04-26T10:40:33.730-03:00" + } + ] + } + } + }, + "401": { + "description": "Token de acesso inválido" + }, + "404": { + "description": "Domínio da loja inválido", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Usuários" + ], + "description": "Lista os usuários", + "parameters": [ + { + "schema": { + "type": "boolean", + "default": false + }, + "in": "query", + "name": "include_inactive", + "description": "Incluir usuários desativados?" + }, + { + "schema": { + "type": "boolean", + "default": false + }, + "in": "query", + "name": "include_images", + "description": "Incluir todas as imagens dos produtos?" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "external_code", + "description": "Exibe somente os usuários com o código externo indicado" + }, + { + "schema": { + "type": "string", + "enum": [ + "Agente", + "Gestor", + "Local" + ] + }, + "in": "query", + "name": "role_name", + "description": "Exibe somente os usuários com a função indicada" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "term", + "description": "Filtra usuários que contenham o valor indicado no nome, telefone, email ou código externo" + } + ] + }, + "post": { + "summary": "Cria um usuário", + "operationId": "post-api-v2-users", + "responses": { + "200": { + "description": "Quando o usuário é criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User.v1" + } + } + } + }, + "201": { + "description": "Usuário criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Usuários" + ], + "description": "Cria um usuário", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "role_name": { + "type": "string", + "enum": [ + "Agente", + "Gestor", + "Local" + ] + }, + "password": { + "type": "string" + }, + "password_confirmation": { + "type": "string" + }, + "external_code": { + "type": "string" + }, + "phone_area": { + "type": "string", + "maxLength": 2 + }, + "phone": { + "type": "string", + "maxLength": 9 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/v2/users/tags": { + "get": { + "summary": "Lista as tags dos usuários a partir das funções", + "operationId": "get-api-v2-users-tags", + "responses": { + "200": { + "description": "Quando as tags são retornadas", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "tags": [ + "Usuários" + ], + "description": "Lista os usuários", + "parameters": [ + { + "schema": { + "type": "string", + "enum": [ + "Agente", + "Gestor", + "Local", + "Agente Social Selling" + ] + }, + "in": "query", + "name": "role_names", + "description": "Exibe somente os usuários com a função indicada" + } + ] + } + }, + "/api/v2/carts": { + "get": { + "summary": "Lista os carrinhos", + "operationId": "get-api-v2-carts", + "responses": { + "200": { + "$ref": "#/components/responses/Carts" + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "tags": [ + "Carrinhos" + ], + "description": "Retorna a lista de carrinhos ativos nos últimos 60 dias", + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "page", + "description": "Número da página" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "per_page", + "description": "Quantidade de produtos por página" + }, + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "without_phones", + "description": "Inclui os carrinhos sem telefone (não enviar o campo para não incluir)" + }, + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "with_payments", + "description": "Filtra os carrinhos que possuem tentativa de pagamento" + } + ] + }, + "post": { + "summary": "Cria um carrinho", + "operationId": "post-api-v2-carts", + "description": "Permite criar um carrinho", + "tags": [ + "Carrinhos da loja" + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "ipv4" + }, + "in": "header", + "name": "X-Browser-Ip", + "description": "Internet Protocol (IP) da máquina de onde é criado o carrinho", + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "header", + "name": "X-User-Agent", + "description": "Identificador da origem do carrinho na loja (como navegador ou dispositivo)", + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart.simple" + } + } + }, + "description": "" + }, + "responses": { + "201": { + "description": "Carrinho criado com sucesso", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/carts/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true, + "description": "Pode ser o id ou o token do carrinho" + } + ], + "patch": { + "summary": "Atualiza um carrinho", + "operationId": "patch-api-v2-carts-id", + "responses": { + "204": { + "description": "Quando o carrinho é atualizado com sucesso" + }, + "404": { + "description": "Quando o carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + }, + "examples": { + "Com um email inválido": { + "value": { + "errors": { + "client": [ + "não é válido" + ] + } + } + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "client_id": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "email": { + "type": "string", + "description": "DEPRECATED: enviar o `client_id`", + "format": "email" + }, + "rebate_token": { + "type": "string" + } + } + } + } + } + }, + "tags": [ + "Carrinhos" + ], + "description": "Permite atualizar os atributos de um carrinho", + "parameters": [] + }, + "delete": { + "summary": "Exclui um carrinho", + "operationId": "delete-api-v2-carts-id", + "responses": { + "204": { + "description": "Quando um carrinho é excluído com sucesso" + }, + "404": { + "description": "Quando um carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "tags": [ + "Carrinhos" + ], + "description": "Permite excluir um carrinho", + "parameters": [] + }, + "get": { + "summary": "Retorna um carrinho", + "operationId": "get-api-v2-carts-id", + "responses": { + "200": { + "description": "Quando o carrinho é encontrado", + "headers": { + "X-Attempt-Count": { + "schema": { + "type": "integer" + }, + "description": "O número de tentativas de pagamento" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart.v1" + } + } + } + }, + "404": { + "description": "Quando um carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "tags": [ + "Carrinhos" + ], + "description": "Permite retornar um carrinho", + "parameters": [] + }, + "post": { + "summary": "Cria um carrinho", + "operationId": "post-api-v2-carts-id", + "responses": { + "201": { + "description": "Quando um carrinho é criado com sucesso", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + }, + "examples": { + "Sem os parâmetros obrigatórios": { + "value": { + "errors": { + "browser_ip": [ + "não pode ficar em branco" + ], + "user_agent": [ + "não pode ficar em branco" + ] + } + } + } + } + } + } + } + }, + "description": "Permite criar um carrinho", + "tags": [ + "Carrinhos" + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "ipv4" + }, + "in": "header", + "name": "X-Browser-Ip", + "description": "IP do usuário que está criando o carrinho na loja", + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "header", + "name": "X-User-Agent", + "description": "User-Agent do navegador do usuário criando o carrinho na loja", + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "client_id": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "coupon_code": { + "type": "string" + }, + "email": { + "type": "string", + "description": "DEPRECATED: enviar o `client_id`", + "format": "email" + }, + "rebate_token": { + "type": "string" + } + } + } + } + }, + "description": "" + } + } + }, + "/api/v2/carts/{id}/installments": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Retorna as parcelas do total de um carrinho", + "tags": [ + "Carrinhos" + ], + "responses": { + "200": { + "description": "Quando o carrinho é encontrado", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Cart_installment.v1" + } + } + } + } + }, + "404": { + "description": "Quando um carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "Não encontrado": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-carts-id-installments", + "description": "Permite calcular as parcelas referentes ao total do carrinho", + "parameters": [] + } + }, + "/api/v2/places": { + "get": { + "summary": "Lista os locais", + "tags": [ + "Locais" + ], + "responses": { + "200": { + "description": "Quando os locais são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Place.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-places", + "description": "Lista os locais", + "parameters": [ + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "names", + "description": "Filtra os locais for nome" + }, + { + "schema": { + "type": "boolean", + "default": false + }, + "in": "query", + "name": "warehouse", + "description": "Filtra os locais que são/não são warehouse" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "category", + "description": "Filtra os locais que contenham determinada categoria" + }, + { + "schema": { + "type": "string", + "example": "-30.1087957,-51.3172282" + }, + "in": "query", + "name": "coordinates", + "description": "As lojas mais próximas da coordenada informada serão exibidas primeiro" + }, + { + "schema": { + "type": "string", + "pattern": "^[0-9]{8}$" + }, + "in": "query", + "name": "origin_zip_code", + "description": "As lojas mais próximas do CEP informado serão exibidas primeiro" + } + ] + }, + "post": { + "summary": "Cria um local", + "operationId": "post-api-v2-places", + "responses": { + "201": { + "description": "Quando o local é criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Place.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos" + } + }, + "tags": [ + "Locais" + ], + "description": "Cria um local", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "address_line_1": { + "type": "string", + "maxLength": 80 + }, + "address_line_2": { + "type": "string", + "maxLength": 80 + }, + "city": { + "type": "string", + "maxLength": 80 + }, + "neighborhood": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "home_page": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "email": { + "type": "string" + }, + "first_phone": { + "type": "string" + }, + "second_phone": { + "type": "string" + }, + "mobile_phone": { + "type": "string", + "default": "false" + }, + "only_cash": { + "type": "boolean" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "marker_url": { + "type": "string" + }, + "state": { + "type": "string" + }, + "opening_hours": { + "type": "string" + }, + "warehouse": { + "type": "boolean" + }, + "legal_name": { + "type": "string" + }, + "cnpj": { + "type": "string" + } + }, + "required": [ + "name", + "address_line_1", + "city", + "email" + ] + } + } + } + } + } + }, + "/api/v2/places/{id}": { + "parameters": [ + { + "schema": { + "type": "integer" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Atualiza um local", + "operationId": "patch-api-v2-places-id", + "responses": { + "204": { + "description": "Quando o local é atualizado" + }, + "404": { + "description": "Quando o local não é encontrado" + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Locais" + ], + "description": "Atualiza um local", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "address_line_1": { + "type": "string", + "maxLength": 80 + }, + "address_line_2": { + "type": "string", + "maxLength": 80 + }, + "city": { + "type": "string", + "maxLength": 80 + }, + "neighborhood": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "home_page": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "email": { + "type": "string" + }, + "first_phone": { + "type": "string" + }, + "second_phone": { + "type": "string" + }, + "mobile_phone": { + "type": "string", + "default": "false" + }, + "only_cash": { + "type": "boolean" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "marker_url": { + "type": "string" + }, + "state": { + "type": "string" + }, + "opening_hours": { + "type": "string" + }, + "warehouse": { + "type": "boolean" + }, + "legal_name": { + "type": "string" + }, + "cnpj": { + "type": "string" + } + }, + "required": [ + "name", + "address_line_1", + "city", + "email" + ] + } + } + } + } + }, + "delete": { + "summary": "Remove um local", + "operationId": "delete-api-v2-places-id", + "responses": { + "204": { + "description": "Quando o local é removido" + }, + "404": { + "description": "Quando o local não é encontrado" + } + }, + "tags": [ + "Locais" + ], + "description": "Remove um local" + } + }, + "/api/v2/orders/{code}/packages/{package_code}/invoices": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "package_code", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista as notas fiscais", + "tags": [ + "Notas fiscais" + ], + "responses": { + "200": { + "description": "Quando as notas fiscais são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Invoice.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-orders-code-packages-package_code-invoices", + "description": "Lista as notas fiscais" + }, + "post": { + "summary": "Cria uma nota fiscal", + "operationId": "post-api-v2-orders-code-packages-package_code-invoices", + "responses": { + "201": { + "description": "Quando a nota fiscal é criada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos" + } + }, + "tags": [ + "Notas fiscais" + ], + "description": "Cria uma nota fiscal", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "number": { + "type": "number" + }, + "series": { + "type": "number" + }, + "issued_at": { + "type": "string", + "format": "date-time" + }, + "key": { + "type": "string" + }, + "volumes": { + "type": "integer" + } + }, + "required": [ + "number" + ] + } + } + } + } + } + }, + "/api/v2/orders/{code}/packages/{package_code}/invoices/{number}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "package_code", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "number", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Atualiza uma nota fiscal", + "operationId": "patch-api-v2-orders-code-packages-package_code-invoices-number", + "responses": { + "204": { + "description": "Quando a nota fiscal é atualizada" + }, + "404": { + "description": "Quando a nota fiscal não é encontrada" + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Notas fiscais" + ], + "description": "Atualiza uma nota fiscal", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "number": { + "type": "number" + }, + "series": { + "type": "number" + }, + "issued_at": { + "type": "string", + "format": "date-time" + }, + "key": { + "type": "string" + }, + "volumes": { + "type": "integer" + } + }, + "required": [ + "number" + ] + } + } + } + } + }, + "delete": { + "summary": "Remove uma nota fiscal", + "operationId": "delete-api-v2-orders-code-packages-package_code-invoices-number", + "responses": { + "204": { + "description": "Quando a nota fiscal é removida" + }, + "404": { + "description": "Quando a nota fiscal não é encontrada" + } + }, + "tags": [ + "Notas fiscais" + ], + "description": "Remove uma nota fiscal" + } + }, + "/api/v2/users/reset_password": { + "post": { + "summary": "Solicita renovação da senha", + "operationId": "post-api-v2-users-reset_password", + "responses": { + "200": { + "description": "Quando o email foi enviado" + }, + "404": { + "description": "Quando o usuário não existe" + } + }, + "tags": [ + "Usuários" + ], + "description": "Será enviado por email um link para o cadastro da nova senha\nO link tem validade de 24 horas", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "email" + ] + } + } + } + } + }, + "patch": { + "summary": "Cadastra a nova senha", + "operationId": "patch-api-v2-users-reset_password", + "responses": { + "200": { + "description": "Quando a senha foi alterada" + }, + "400": { + "description": "Quando o token é inválido ou expirou" + }, + "404": { + "description": "Quando não foi encontrado um usuário com o token informado" + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Usuários" + ], + "description": "Cadastra a nova senha", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "Token pare renovação de senha enviado por email" + }, + "password": { + "type": "string", + "description": "Nova senha para o usuário" + }, + "password_confirmation": { + "type": "string", + "description": "Confirmação da nova senha do usuário" + } + }, + "required": [ + "token", + "password", + "password_confirmation" + ] + } + } + } + } + } + }, + "/api/v2/orders/{code}/shipping_address": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Endereço de entrega", + "tags": [ + "Pedidos" + ], + "responses": { + "200": { + "description": "Quando o endereço é retornado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "company_name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "documents": { + "type": "object", + "description": "Serão retornados apenas os campos preenchidos", + "properties": { + "cpf": { + "type": "string" + }, + "cnpj": { + "type": "string" + }, + "ie": { + "type": "string" + } + } + }, + "street_name": { + "type": "string" + }, + "street_number": { + "type": "string", + "example": "188A" + }, + "complement": { + "type": "string" + }, + "neighborhood": { + "type": "string" + }, + "first_phone_area": { + "type": "string", + "description": "Somente números", + "example": "11" + }, + "first_phone": { + "type": "string", + "description": "Somente números", + "example": "984453322" + }, + "second_phone_area": { + "type": "string", + "description": "Somente números" + }, + "second_phone": { + "type": "string", + "description": "Somente números" + }, + "reference": { + "type": "string" + }, + "zip": { + "type": "string", + "description": "Somente números", + "example": "90050000" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string", + "example": "RS", + "minLength": 2, + "maxLength": 2 + }, + "recipient_name": { + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "street_name", + "street_number", + "neighborhood", + "first_phone_area", + "first_phone", + "zip", + "city", + "state" + ] + } + } + } + } + }, + "operationId": "get-api-v2-orders-code-shipping_address", + "description": "Retorna o endereço de entrega" + } + }, + "/api/v2/payment_recipients": { + "get": { + "summary": "Lista os recebedores", + "tags": [ + "Recebedores" + ], + "responses": { + "200": { + "description": "Quando os recebedores são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Payment_recipient.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-payment_recipients", + "description": "Lista os recebedores" + }, + "post": { + "summary": "Cria um recebedor", + "operationId": "post-api-v2-payment_recipients", + "responses": { + "200": { + "description": "Quando o recebedor foi criado com sucesso", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Payment_recipient.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + }, + "examples": { + "Parâmetro `percentage` em branco": { + "value": { + "errors": { + "percentage": [ + "não pode ficar em branco" + ] + } + } + } + } + } + } + } + }, + "tags": [ + "Recebedores" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tag_id": { + "type": "integer" + }, + "recipient_id": { + "type": "integer" + }, + "percentage": { + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "active": { + "type": "boolean", + "default": true + }, + "charge_processing_fee": { + "type": "boolean", + "default": false + }, + "liable": { + "type": "boolean", + "default": false + }, + "code": { + "type": "string" + }, + "place_id": { + "type": "integer" + }, + "user_id": { + "type": "integer" + }, + "include_shipping": { + "type": "boolean", + "default": true, + "description": "Indica se o frete deve ser incluído no split do pagamento" + } + }, + "required": [ + "percentage" + ] + } + } + } + }, + "description": "Cria um recebedor" + } + }, + "/api/v2/payment_recipients/{id}": { + "get": { + "summary": "Retorna um recebedor", + "tags": [ + "Recebedores" + ], + "responses": { + "200": { + "description": "Quando o recebedor é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Payment_recipient.v1" + } + } + } + }, + "404": { + "description": "Quando o recebedor não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-payment_recipients-id", + "description": "Retorna um recebedor" + }, + "patch": { + "summary": "Atualiza um recebedor", + "operationId": "patch-api-v2-payment_recipients-id", + "responses": { + "204": { + "description": "Quando o recebedor é atualizado" + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Recebedores" + ], + "description": "Atualiza um recebedor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tag_id": { + "type": "integer" + }, + "recipient_id": { + "type": "integer" + }, + "percentage": { + "type": "number", + "maximum": 100 + }, + "active": { + "type": "boolean", + "default": true + }, + "charge_processing_fee": { + "type": "boolean", + "default": false + }, + "liable": { + "type": "boolean", + "default": false + }, + "code": { + "type": "string" + }, + "place_id": { + "type": "integer" + }, + "user_id": { + "type": "integer" + }, + "include_shipping": { + "type": "boolean", + "default": true, + "description": "Indica se o frete deve ser incluído no split do pagamento" + } + } + } + } + } + } + }, + "delete": { + "summary": "Remove um recebedor", + "operationId": "delete-api-v2-payment_recipients-id", + "responses": { + "204": { + "description": "Quando o recebedor é removido" + }, + "404": { + "description": "Quando o recebedor não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "description": "Remove um recebedor", + "tags": [ + "Recebedores" + ] + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ] + }, + "/api/v2/users/{user_id}/payables": { + "get": { + "summary": "Lista os recebíveis do usuário", + "responses": { + "200": { + "description": "Retorna a lista de recebíveis do usuário", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Payables.v1" + } + } + } + } + }, + "404": { + "description": "Quando o usuário não está cadastrado como recebedor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "description": "Permite a listagem de recebíveis do usuário", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "user_id", + "in": "path", + "required": true + } + ], + "operationId": "get-api-v2-users-user_id-payables", + "tags": [ + "Usuários" + ] + } + }, + "/api/v2/audience_members": { + "get": { + "summary": "Retorna os membros da audiência", + "responses": { + "200": { + "description": "Quando os membros da audiência são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Audience_member.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-audience_members", + "description": "Lista os membros da audiência", + "tags": [ + "Público" + ] + }, + "post": { + "summary": "Cria uma membro da audiência", + "operationId": "post-api-v2-audience_members", + "responses": { + "201": { + "description": "Quando o membro da audiência é criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Audience_member.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "description": "Permite criar um membro da audiência", + "tags": [ + "Público" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string" + }, + "phone_area": { + "type": "string", + "nullable": true + }, + "phone": { + "type": "string", + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "email" + ] + } + } + }, + "description": "" + } + }, + "parameters": [] + }, + "/api/v2/audience_members/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "delete": { + "summary": "Remove um membro da audiência", + "operationId": "delete-api-v2-audience-members-id", + "responses": { + "204": { + "description": "Quando o membro da audiência é removido" + }, + "404": { + "description": "Quando a audiência não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Público" + ], + "description": "Permite remover um membro da audiência" + }, + "patch": { + "summary": "Altera um membro da audiência", + "operationId": "patch-api-v2-audience-members-id", + "responses": { + "204": { + "description": "Quando o membro da audiência é alterado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Audience_member.v1" + } + } + } + }, + "404": { + "description": "Quando o membro da audiência não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "description": "Permite alterar um membro da audiência", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "phone_area": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "tags": [ + "Público" + ] + } + }, + "/api/v2/orders/{order_code}/packages/{package_code}/trackings": { + "post": { + "summary": "Adiciona um rastreio", + "operationId": "post-api-v2-orders-order_code-packages-package_code-trackings", + "responses": { + "200": { + "description": "Quando o rastreio é criado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Código de rastreio do pacote" + }, + "company": { + "type": "string", + "description": "Transportadora" + }, + "url": { + "type": "string", + "description": "URL para rastreio do pacote na transportadora" + } + }, + "required": [ + "code" + ] + }, + "examples": { + "200": { + "value": { + "code": "PL123456789", + "url": "https://examble.com/tracking", + "company": "Correios" + } + } + } + } + } + }, + "404": { + "description": "Quando o pedido ou o pacote não existem", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "tags": [ + "Rastreios" + ], + "description": "Adiciona um rastreio para um pacote de um pedido", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Código de rastreio" + }, + "company": { + "type": "string", + "description": "Transportadora" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Link de rastreamento" + } + }, + "required": [ + "code" + ] + } + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "order_code", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "package_code", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista os rastreios", + "operationId": "get-api-v2-orders-order_code-packages-package_code-trackings", + "responses": { + "200": { + "description": "Quando os rastreios são listados", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "tracking_code": { + "type": "string", + "description": "Código de rastreio do pacote" + }, + "tracked_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização do código de rastreio do pacote" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL para rastreio do pedido com a transportadora" + }, + "company": { + "type": "string", + "description": "Transportadora do pacote" + } + }, + "required": [ + "tracking_code" + ] + }, + "examples": { + "Rastreio": { + "value": { + "id": 15, + "tracking_code": "codigo-rastreio", + "tracked_at": "2022-12-23T15:20:18.893-03:00", + "url": "rastreiocorreios.com.br", + "company": "Correios" + } + } + } + } + } + }, + "404": { + "description": "Quando o pedido ou o pacote não existem", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "tags": [ + "Rastreios" + ], + "description": "Lista os rastreios de um pacote de um pedido" + } + }, + "/api/v2/orders/{order_code}/packages/{package_code}/trackings/{id}": { + "delete": { + "summary": "Remove um rastreio", + "operationId": "delete-api-v2-orders-order_code-packages-package_code-trackings-id", + "responses": { + "204": { + "description": "Quando o rastreio é removido" + }, + "404": { + "description": "Quando o pedido ou o pacote não existem", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Rastreios" + ], + "description": "Remove um rastreio" + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "order_code", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "package_code", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ] + }, + "/api/v2/carts/{cart_id}/items": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "cart_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista os itens de um carrinho", + "operationId": "get-api-v2-carts-cart_id-items", + "responses": { + "200": { + "description": "Quando os itens do carrinho são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Cart_item.v1" + } + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "description": "Lista os itens de um carrinho", + "tags": [ + "Itens do carrinho" + ] + }, + "post": { + "summary": "Cria um item do carrinho", + "operationId": "post-api-v2-carts-cart_id-items", + "responses": { + "201": { + "description": "Quando um item do carrinho é criado com sucesso", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart_item.v1" + } + } + } + }, + "400": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + }, + "examples": { + "Sem os parâmetros obrigatórios": { + "value": { + "errors": { + "sku": [ + "precisa ser informado" + ], + "quantity": [ + "não pode ficar em branco" + ] + } + } + } + } + } + } + }, + "404": { + "description": "Quando um carrinho ou variante não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "Registro não encontrado": { + "value": { + "error": "not found" + } + } + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "description": "Permite criar um item do carrinho", + "tags": [ + "Itens do carrinho" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "extra": { + "type": "object" + }, + "place_id": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "store_coupon_code": { + "type": "string" + }, + "customizations": { + "type": "array" + } + }, + "required": [ + "sku", + "quantity" + ], + "$ref": "#/components/schemas/Product.v0" + } + } + }, + "description": "Cria um item do carrinho" + } + } + }, + "/api/v2/carts/{cart_id}/items/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "cart_id", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Atualiza um item do carrinho", + "operationId": "patch-api-v2-carts-cart_id-items-id", + "responses": { + "204": { + "description": "Quando o item do carrinho é alterado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Audience_member.v1" + } + } + } + }, + "404": { + "description": "Quando o item do carrinho ou o carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "description": "Atualiza um item do carrinho", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "quantity": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "place_id": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "extra": { + "type": "object" + }, + "store_coupon_code": { + "type": "string" + } + } + } + } + } + }, + "tags": [ + "Itens do carrinho" + ] + }, + "delete": { + "summary": "Remove um item do carrinho", + "operationId": "delete-api-v2-carts-cart_id-items-id", + "responses": { + "204": { + "description": "Quando o item do carrinho é removido" + }, + "404": { + "description": "Quando o item do carrinho ou o carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Itens do carrinho" + ], + "description": "Remove um item do carrinho" + } + }, + "/api/v2/carts/{cart_id}/items/bulk": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "cart_id", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Adiciona itens ao carrinho", + "operationId": "post-api-v2-carts-cart_id-items-bulk", + "responses": { + "201": { + "description": "Quando os itens são adicionados com sucesso", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Cart_item.v1" + } + } + } + } + }, + "400": { + "description": "Quando os parâmetros são inválidos", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errors": { + "type": "object", + "properties": { + "items": { + "type": "object", + "properties": { + "0": { + "type": "object", + "properties": { + "quantity": { + "type": "array", + "items": { + "type": "string" + } + }, + "sku": { + "type": "array", + "items": { + "type": "string" + } + }, + "place_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "examples": { + "Erros de validação": { + "value": { + "errors": { + "items": { + "0": { + "sku": [ + "deve ser preenchido" + ] + }, + "1": { + "quantity": [ + "deve ser maior que 0" + ] + }, + "2": { + "place_id": [ + "deve ser maior que 0" + ] + }, + "3": { + "extra": [ + "precisa ser um hash" + ], + "quantity": [ + "precisa ser um inteiro" + ] + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Quando um carrinho ou variante não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "Registro não encontrado": { + "value": { + "error": "not found" + } + } + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "tags": [ + "Carrinhos" + ], + "description": "Permite adicionar itens em bulk ao carrinho", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "description": "Itens do carrinho", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string", + "description": "Código SKU da variante do produto" + }, + "quantity": { + "type": "integer", + "description": "Unidades do produto" + }, + "customizations": { + "type": "array", + "description": "[Personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) do produto", + "items": { + "properties": { + "Customization": { + "type": "string", + "description": "Adicione a customização de acordo com a [personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) incluídas no Admin da loja. \nSe por exemplo a customização do produto é a cor, o parâmetro para a requisição deve ser `Color` ao invés de `CUstomization`. \nSaiba mais sobre como utilizar esse parâmetro pelo exemplo de requsição localizado na seção de **Request Example** (ao lado do código da requisição)." + } + } + } + } + }, + "required": [ + "sku", + "quantity" + ] + } + } + }, + "example": { + "items": [ + { + "sku": "teste", + "quantity": 1, + "customizations": [ + { + "Color": "Black" + } + ] + }, + { + "sku": "variante.sku2", + "quantity": 10, + "customizations": [ + { + "Color": "Red" + } + ] + } + ] + } + } + } + } + } + } + }, + "/api/v2/discounts": { + "post": { + "summary": "Cria uma promoção", + "operationId": "post-api-v2-discounts", + "responses": { + "201": { + "description": "Quando a promoção é criada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Discount.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Promoções" + ], + "description": "Cria uma promoção", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "start_at": { + "type": "string", + "format": "date-time" + }, + "end_at": { + "type": "string", + "format": "date-time" + }, + "valid_to": { + "type": "string", + "enum": [ + "store", + "cart" + ] + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "email": { + "type": "string", + "format": "email" + }, + "cpf": { + "type": "string", + "pattern": "[0-9]{11}" + }, + "tags": { + "type": "string" + } + }, + "required": [ + "name", + "start_at" + ] + } + } + } + } + } + }, + "/api/v2/discounts/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Retorna uma promoção", + "tags": [ + "Promoções" + ], + "responses": { + "200": { + "description": "Quando a promoção é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Discount.v1" + } + } + } + }, + "404": { + "description": "Quando a promoção não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-discounts-id", + "description": "Retorna uma promoção" + }, + "patch": { + "summary": "Altera uma promoção", + "tags": [ + "Promoções" + ], + "responses": { + "204": { + "description": "Quando a promoção é alterada" + }, + "404": { + "description": "Quando a promoção não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "operationId": "patch-api-v2-discounts-id", + "description": "Altera uma promoção", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "start_at": { + "type": "string", + "format": "date-time" + }, + "end_at": { + "type": "string", + "format": "date-time" + }, + "valid_to": { + "type": "string", + "enum": [ + "store", + "cart" + ] + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean", + "default": true + }, + "email": { + "type": "string", + "format": "email" + }, + "cpf": { + "type": "string", + "pattern": "[0-9]{11}" + }, + "tags": { + "type": "string" + } + }, + "required": [ + "name", + "start_at" + ] + } + } + } + } + }, + "delete": { + "summary": "Remove uma promoção", + "operationId": "delete-api-v2-discounts-id", + "responses": { + "204": { + "description": "Quando a promoção é removida" + }, + "400": { + "description": "Quando a promoção não pode ser removida", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "with-coupons": { + "value": { + "error": "Essa promoção não pode ser excluída pois possui cupons utilizados" + } + } + } + } + } + }, + "404": { + "description": "Quando a promoção não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Promoções" + ], + "description": "Remove uma promoção" + } + }, + "/api/v2/discounts/{discount_id}/rules": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "discount_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista as regras", + "tags": [ + "Regras de desconto" + ], + "responses": { + "200": { + "description": "Quando as regras de desconto são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Discount_rule.v1" + } + } + } + } + } + }, + "description": "Lista as regras de desconto de uma promoção", + "operationId": "get-api-v2-discounts-discount_id-rules" + }, + "post": { + "summary": "Cria uma regra", + "operationId": "post-api-v2-discounts-discount_id-rules", + "responses": { + "200": { + "description": "Quando a regra de desconto é criada", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "amount": { + "type": "number" + }, + "apply_to": { + "type": "string", + "enum": [ + "product", + "tag", + "subtotal", + "total", + "shipping" + ] + }, + "min_quantity": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "channel": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Regras de desconto" + ], + "description": "Cria uma regra de desconto", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "apply_to": { + "type": "string", + "enum": [ + "product", + "tag", + "subtotal", + "total", + "shipping" + ] + }, + "amount_type": { + "type": "string", + "enum": [ + "R$", + "%" + ] + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "product_id": { + "type": "integer" + }, + "tag_name": { + "type": "string" + }, + "min_quantity": { + "type": "integer" + }, + "shipping_method": { + "type": "string" + }, + "min_subtotal": { + "type": "number", + "minimum": 0 + }, + "gift": { + "type": "boolean", + "default": false + }, + "combinated_product_id": { + "type": "integer" + }, + "client_tag": { + "type": "string" + }, + "shipping_rule": { + "type": "string" + }, + "gift_quantity": { + "type": "integer", + "minimum": 1 + }, + "agent_tag": { + "type": "string" + }, + "regions": { + "type": "array", + "items": { + "type": "string" + } + }, + "channel": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/v2/discounts/{discount_id}/rules/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "discount_id", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "delete": { + "summary": "Remove uma regra", + "operationId": "delete-api-v2-discounts-discount_id-rules-id", + "responses": { + "200": { + "description": "Quando a regra de desconto é removida" + } + }, + "tags": [ + "Regras de desconto" + ], + "description": "Remove uma regra de desconto" + }, + "patch": { + "summary": "Altera uma regra", + "operationId": "patch-api-v2-discounts-discount_id-rules-id", + "responses": { + "204": { + "description": "Quandoa regra de desconto é alterada" + }, + "404": { + "description": "Quando a regra de desconto não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "description": "Altera uma regra de desconto", + "tags": [ + "Regras de desconto" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "apply_to": { + "type": "string", + "enum": [ + "product", + "tag", + "subtotal", + "total", + "shipping" + ] + }, + "amount_type": { + "type": "string", + "enum": [ + "R$", + "%" + ] + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "product_id": { + "type": "integer" + }, + "tag_id": { + "type": "integer" + }, + "min_quantity": { + "type": "integer" + }, + "shipping_method": { + "type": "string" + }, + "min_subtotal": { + "type": "number", + "minimum": 0 + }, + "gift": { + "type": "boolean", + "default": false + }, + "combinated_product_id": { + "type": "integer" + }, + "client_tag": { + "type": "string" + }, + "shipping_rule": { + "type": "string" + }, + "gift_quantity": { + "type": "integer", + "minimum": 1 + }, + "agent_tag": { + "type": "string" + }, + "regions": { + "type": "array", + "items": { + "type": "string" + } + }, + "channel": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/v2/discounts/{discount_id}/coupons": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "discount_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista os cupons", + "tags": [ + "Cupons de desconto" + ], + "responses": { + "200": { + "description": "Quando os cupons são retornados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coupon.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-discounts-discount_id-coupons", + "description": "Permite listar os cupons de desconto de uma promoção", + "parameters": [ + { + "schema": { + "type": "number" + }, + "in": "query", + "name": "uses_per_code", + "description": "Filtra os cupons pelo campo uses_per_code" + } + ] + }, + "post": { + "summary": "Cria um cupom", + "operationId": "post-api-v2-discounts-discount_id-coupons", + "responses": { + "201": { + "description": "Quando o cupom é criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Coupon.v1" + } + } + } + }, + "404": { + "description": "Quando o desconto não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Cupons de desconto" + ], + "description": "Cria um cupom de desconto", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "uses_per_code": { + "type": "integer" + }, + "uses_per_user": { + "type": "integer" + }, + "referrer_email": { + "type": "string", + "format": "email" + }, + "quantity": { + "type": "integer" + }, + "user_id": { + "type": "integer" + } + } + } + } + } + } + } + }, + "/api/v2/discounts/{discount_id}/coupons{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "discount_id", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Atualiza um cupom", + "operationId": "patch-api-v2-discounts-discount_id-coupons-coupons_id", + "responses": { + "204": { + "description": "Quando o cupom é atualizado" + }, + "404": { + "description": "Quando o coupom não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Cupons de desconto" + ], + "description": "Atualiza um cupom de desconto", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uses_per_code": { + "type": "integer", + "description": "Caso deseje um uso ilimitado do cupom, o valor desse campo deverá ser 0" + }, + "uses_per_user": { + "type": "integer", + "description": "Caso deseje um uso ilimitado do cupom, o valor desse campo deverá ser 0" + } + } + } + } + } + } + }, + "delete": { + "summary": "Remove um cupom", + "operationId": "delete-api-v2-discounts-discount_id-coupons-id", + "responses": { + "204": { + "description": "Quando o cupom é removido" + }, + "404": { + "description": "Quando o cupom não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando o cupom estiver utilizado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Cupons de desconto" + ], + "description": "Remove um cupom de desconto" + } + }, + "/api/v2/products": { + "get": { + "summary": "Lista os produtos", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "description": "Quando os produtos são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product.v1" + } + }, + "examples": { + "example-1": { + "value": [ + { + "id": 0, + "active": true, + "available": true, + "category_tags": [ + { + "tag_type": "string", + "name": "string", + "title": "string" + } + ], + "description": "string", + "discount_id": 0, + "html_description": "string", + "image_url": "string", + "installments": [ + 0 + ], + "min_quantity": "string", + "name": "string", + "on_sale": true, + "plain_description": "string", + "price": 0, + "rating": { + "rating": 0, + "votes": 0 + }, + "reference": "string", + "sale_price": 0, + "slug": "string", + "tag_names": [ + "string" + ], + "updated_at": "string", + "url": "string", + "variants": [ + { + "{id}": { + "available": true, + "available_quantity": 0, + "custom_attributes": {}, + "handling_days": 0, + "height": 0, + "id": 1, + "image_url": "string", + "installments": [ + 0 + ], + "inventories": [ + { + "created_at": "2019-08-24T14:15:22Z", + "id": 0, + "name": null, + "place_id": 0, + "price": 0, + "quantity": 0, + "quantity_sold": 0, + "sale_price": 0, + "slug": "string", + "updated_at": "2019-08-24T14:15:22Z", + "variant_id": 0 + } + ], + "length": 0, + "main": true, + "min_quantity": 0, + "name": "string", + "norder": 0, + "price": 0, + "product_id": 0, + "properties": { + "property1": { + "defining": true, + "name": "string", + "value": "string" + }, + "property2": { + "defining": true, + "name": "string", + "value": "string" + }, + "property3": { + "defining": true, + "name": "string", + "value": "string" + } + }, + "quantity": 0, + "quantity_sold": 0, + "sale_price": 0, + "sku": "string", + "slug": "string", + "stock": 0, + "updated_at": "2019-08-24T14:15:22Z", + "weight": 0, + "width": 0 + } + } + ], + "discount_rule": null, + "images": [ + { + "id": 0, + "url": "string", + "updated_at": "2019-08-24T14:15:22Z", + "variant_ids": [ + 0 + ] + } + ] + } + ] + } + } + } + } + }, + "404": { + "description": "Domínio de loja não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-products", + "description": "Lista os produtos", + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "limit", + "description": "Delimita a quantidade de itens retornados" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "page", + "description": "Número da página" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "per_page", + "description": "Quantidade de produtos por página" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "reference", + "description": "Filtra pela referência" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "ids", + "description": "Filtra pelo ID dos produtos " + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "tag", + "description": "Filtra produtos que coném a tag" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "updated_after", + "description": "Filtra produtos alterados depois da data" + }, + { + "schema": { + "type": "string", + "enum": [ + "newest" + ] + }, + "in": "query", + "name": "sort", + "description": "Exibe os produtos cadastrados recentemente primeiro" + }, + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "include_inactive", + "description": "Inclui os produtos inativos na listagem" + }, + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "include_images", + "description": "Inclui na requisição se deseja que venham todas as imagens do produto" + } + ] + }, + "post": { + "summary": "Cria um produto", + "operationId": "post-api-v2-products", + "responses": { + "201": { + "description": "Quando o produto é criado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "active": { + "type": "string", + "description": "Indica se o produto está ativo (`true`) ou inativo (`false`)", + "default": true + }, + "reference": { + "type": "string", + "description": "Código de Referência do produto" + }, + "name": { + "type": "string", + "description": "Nome do produto" + }, + "description": { + "type": "string", + "description": "Descrição do produto" + }, + "tag_list": { + "type": "array", + "items": { + "type": "string" + }, + "example": "tag1, tag2", + "description": "Lista de tags associadas ao produto" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "product_type": { + "description": "Tipo de produto, entre:\n - `sample`: amostra\n - `subscription`: assinatura\n - `product`: produto em geral", + "type": "string", + "enum": [ + "product", + "sample", + "subscription" + ], + "default": "product" + } + }, + "required": [ + "reference", + "name" + ] + } + } + } + }, + "404": { + "description": "Domínio de loja não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Produtos" + ], + "description": "Cria um produto", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "reference": { + "type": "string" + }, + "tag_list": { + "type": "string", + "example": "tag1, tag2" + } + }, + "required": [ + "name", + "reference" + ], + "$ref": "#/components/schemas/SimpleProduct" + } + } + } + } + } + }, + "/api/v2/products/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Retorna um produto", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "description": "Quando o produto é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product.v1" + }, + "examples": { + "example-1": { + "value": { + "id": 0, + "active": true, + "available": true, + "category_tags": [ + { + "tag_type": "string", + "name": "string", + "title": "string" + } + ], + "description": "string", + "discount_id": 0, + "html_description": "string", + "image_url": "string", + "installments": [ + 0 + ], + "min_quantity": "string", + "name": "string", + "on_sale": true, + "plain_description": "string", + "price": 0, + "rating": { + "rating": 0, + "votes": 0 + }, + "reference": "string", + "sale_price": 0, + "slug": "string", + "tag_names": [ + "string" + ], + "updated_at": "string", + "url": "string", + "variants": [ + { + "{id}": { + "available": true, + "available_quantity": 0, + "custom_attributes": {}, + "handling_days": 0, + "height": 0, + "id": 1, + "image_url": "string", + "installments": [ + 0 + ], + "inventories": [ + { + "created_at": "2019-08-24T14:15:22Z", + "id": 0, + "name": null, + "place_id": 0, + "price": 0, + "quantity": 0, + "quantity_sold": 0, + "sale_price": 0, + "slug": "string", + "updated_at": "2019-08-24T14:15:22Z", + "variant_id": 0, + "place_name": "string" + } + ], + "length": 0, + "main": true, + "min_quantity": 0, + "name": "string", + "norder": 0, + "price": 0, + "product_id": 0, + "properties": { + "property1": { + "defining": true, + "name": "string", + "value": "string" + }, + "property2": { + "defining": true, + "name": "string", + "value": "string" + }, + "property3": { + "defining": true, + "name": "string", + "value": "string" + } + }, + "quantity": 0, + "quantity_sold": 0, + "sale_price": 0, + "sku": "string", + "slug": "string", + "stock": 0, + "updated_at": "2019-08-24T14:15:22Z", + "weight": 0, + "width": 0 + } + } + ], + "discount_rule": null, + "images": [ + { + "id": 0, + "url": "string", + "updated_at": "2019-08-24T14:15:22Z", + "variant_ids": [ + 0 + ] + } + ] + } + } + } + } + } + }, + "404": { + "description": "Quando o produto não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product.v1" + } + } + } + } + }, + "operationId": "get-api-v2-products-id", + "description": "Retorna um produto", + "parameters": [ + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "coupon_codes", + "description": "Lista de cupons para calcular o desconto do produto" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "include_inventory_place", + "description": "Se \"true\", inclui o nome do local nos inventários das variantes" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "include_images", + "description": "Se \"true\", inclui todas as imagens do produto" + } + ] + }, + "patch": { + "summary": "Atualiza um produto", + "operationId": "patch-api-v2-products-id", + "responses": { + "204": { + "description": "Quando o produto é atualizado" + }, + "404": { + "description": "Quando o produto não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviado são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Produtos" + ], + "description": "Atualiza um produto", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "reference": { + "type": "string" + }, + "tag_list": { + "type": "string", + "example": "tag1, tag2" + } + }, + "required": [ + "name", + "reference" + ] + } + } + } + } + }, + "delete": { + "summary": "Remove um produto", + "operationId": "delete-api-v2-products-id", + "responses": { + "204": { + "description": "Quando o produto é removido" + }, + "404": { + "description": "Quando o produto não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Produtos" + ], + "description": "Remove um produto" + } + }, + "/api/v2/products/reference/{reference}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "reference", + "in": "path", + "required": true, + "description": "Referência do produto" + } + ], + "patch": { + "summary": "Atualiza um produto pela referência", + "operationId": "patch-api-v2-products-reference-reference", + "responses": { + "204": { + "$ref": "#/components/responses/204" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "description": "Permite atualizar um produto pela referência", + "requestBody": { + "$ref": "#/components/requestBodies/Product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimpleProduct", + "type": "object", + "properties": { + "reference": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "active": { + "type": "boolean", + "default": true + }, + "product_type": { + "type": "string", + "enum": [ + "product", + "sample", + "subscription" + ], + "default": "product" + } + }, + "required": [ + "reference", + "name" + ] + } + } + }, + "description": "" + }, + "tags": [ + "Produtos" + ] + } + }, + "/api/v2/products/{id}/rate": { + "post": { + "summary": "Avalia um produto", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "description": "Quando a avaliação é recebida", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rating": { + "type": "string" + }, + "votes": { + "type": "string" + } + } + }, + "examples": { + "example-1": { + "value": { + "rating": "0.9", + "votes": "2" + } + } + } + } + } + }, + "400": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "example-1": { + "value": { + "error": "invalid rate value" + } + } + } + } + } + }, + "404": { + "description": "Quando o produto não tem variantes", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "example-1": { + "value": { + "error": "product without variants" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-products-id-rate", + "description": "Recebe uma avaliação e recalcula a pontuação atual", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "in": "query", + "name": "rate", + "description": "Avaliação" + } + ] + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ] + }, + "/api/v2/products/search": { + "get": { + "summary": "Busca os produtos", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "description": "Quando os produtos são encontrados", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductSearch" + } + }, + "aggregations": { + "type": "object", + "properties": { + "min_price": { + "type": "number" + }, + "max_price": { + "type": "number" + }, + "types": { + "type": "object" + }, + "properties": { + "type": "object", + "properties": { + "property1": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "count": { + "type": "number" + } + } + } + }, + "property2": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "count": { + "type": "number" + } + } + } + }, + "property3": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "count": { + "type": "number" + } + } + } + } + } + } + } + } + } + }, + "examples": { + "example-1": { + "value": { + "results": [ + { + "id": 0, + "active": true, + "available": true, + "subscription": true, + "slug": "string", + "reference": "string", + "reference_lowercase": "string", + "name": "string", + "description": "string", + "image_url": "string", + "url": "string", + "tags": [ + { + "name": "string", + "title": "string", + "subtitle": "string", + "description": "string", + "importance": 0, + "type": "string", + "image_url": "string" + } + ], + "price": 0, + "on_sale": true, + "sale_price": 0, + "intl_price": 0, + "discount_id": 0, + "discount_rule": { + "type": "fixed", + "amount": 0 + }, + "discount": { + "name": "string", + "description": "string", + "facebook": true, + "valid_to": "string" + }, + "images": [ + { + "sku": "string", + "url": "string" + } + ], + "variants": [ + { + "id": 1, + "sku": "string", + "sku_lowercase": "string", + "name": "string", + "full_name": "string", + "main": true, + "available": true, + "image_url": "string", + "price": 0, + "sale_price": 0, + "intl_price": 0, + "installments": [ + { + "number": 1, + "price": 10, + "interest": false, + "interest_rate": 0, + "total": 10 + } + ], + "stock": 0, + "quantity": 0, + "quantity_sold": 0, + "min_quantity": 0, + "available_quantity": 0, + "custom_attributes": {}, + "properties": { + "property1": { + "defining": true, + "name": "string", + "value": "string" + }, + "property2": { + "defining": true, + "name": "string", + "value": "string" + }, + "property3": { + "defining": true, + "name": "string", + "value": "string" + } + }, + "inventories": [ + { + "name": null, + "slug": "string", + "available": true, + "price": 0, + "sale_price": 0, + "quantity": 0, + "quantity_sold": 0, + "place": { + "id": 0, + "name": "string" + } + } + ], + "handling_days": 0, + "barcode": "string", + "weight": 0, + "width": 0, + "height": 0, + "length": 0 + } + ], + "installments": [ + { + "number": 1, + "price": 10, + "interest": false, + "interest_rate": 0, + "total": 10 + } + ], + "created_at": "2019-08-24T14:15:22Z", + "updated_at": "2019-08-24T14:15:22Z" + } + ], + "aggregations": { + "min_price": 0, + "max_price": 0, + "types": { + "tag_0": [ + { + "name": "string", + "title": "string", + "count": 0 + }, + { + "name": "string", + "title": "string", + "count": 0 + } + ], + "tag_1": [ + { + "name": "string", + "title": "string", + "count": 0 + } + ] + }, + "properties": { + "property1": [ + { + "value": "string", + "count": 0 + } + ], + "property2": [ + { + "value": "string", + "count": 0 + } + ], + "property3": [ + { + "value": "string", + "count": 0 + } + ] + } + } + } + } + } + } + } + } + }, + "operationId": "get-api-v2-products-search", + "description": "Busca os produtos de acordo com os parâmetros definidos", + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "page", + "description": "Número da página" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "per_page", + "description": "Quantidade de produtos por página" + }, + { + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "in": "query", + "name": "ids[]", + "description": "Filtra pelo ID dos produtos" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "term", + "description": "Filtra produtos que contenham o termo" + }, + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "wildcard", + "description": "Permite que o filtro 'term' realize filtragem de produtos por termo parcial" + }, + { + "schema": { + "type": "object" + }, + "in": "query", + "name": "type_tags[]", + "description": "Filtra pelo nome da tag dentro de um tipo de tag. Exemplo, type_tags[cor]=verde" + }, + { + "schema": { + "type": "string", + "enum": [ + "and", + "or" + ] + }, + "in": "query", + "name": "type_tags_operator", + "description": "Operador lógico para o filtro de tag" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "property1_values[]", + "description": "Filtra pelo valor da propriedade 1" + }, + { + "schema": { + "type": "string", + "enum": [ + "and", + "or" + ] + }, + "in": "query", + "name": "property1_operator", + "description": "Operador lógico para o filtro de valor da propriedade 1" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "property2_values[]", + "description": "Filtra pelo valor da propriedade 2" + }, + { + "schema": { + "type": "string", + "enum": [ + "and", + "or" + ] + }, + "in": "query", + "name": "property2_operator", + "description": "Operador lógico para o filtro de valor da propriedade 2" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "property3_values[]", + "description": "Filtra pelo valor da propriedade 3" + }, + { + "schema": { + "type": "string", + "enum": [ + "and", + "or" + ] + }, + "in": "query", + "name": "property3_operator", + "description": "Operador lógico para o filtro de valor da propriedade 3" + }, + { + "schema": { + "type": "number" + }, + "in": "query", + "name": "min_price", + "description": "Filtra pelo preço de venda mínimo do produto" + }, + { + "schema": { + "type": "number" + }, + "in": "query", + "name": "max_price", + "description": "Filtra pelo preço de venda máximo do produto" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "tags[]", + "description": "Filtra pelo nome das tags, independente do tipo" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "parent_tags", + "description": "Filtra pelo nome das tags, independente do tipo" + }, + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "show_only_available", + "description": "Filtra por produtos disponíveis" + }, + { + "schema": { + "type": "string", + "enum": [ + "newest", + "oldest", + "lowest_price", + "highest_price" + ] + }, + "in": "query", + "name": "sort", + "description": "Ordena o resultado da busca de produtos conforme a opção escolhida" + } + ] + } + }, + "/api/v2/variants/{sku}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "sku", + "in": "path", + "required": true, + "description": "SKU da variante" + } + ], + "get": { + "summary": "Retorna uma variante", + "tags": [ + "Variantes" + ], + "responses": { + "201": { + "description": "Quando a variante é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Variant" + }, + "examples": { + "example-1": { + "value": { + "id": 95, + "main": false, + "available": true, + "sku": "SHOP0001", + "name": "Variation", + "slug": "variation", + "min_quantity": 1, + "quantity": 1, + "quantity_sold": 0, + "stock": 1, + "custom_attributes": {}, + "properties": {}, + "updated_at": "2020-10-27T11:54:32.018-03:00", + "price": 10, + "installments": [ + 10 + ], + "available_quantity": 1, + "weight": 0.001, + "width": 1, + "height": 1, + "length": 1, + "handling_days": 0, + "inventories": [], + "sale_price": 10, + "image_url": "//b0.vnda.com.br/x120/shop/2014/07/08/variation.jpg", + "product_id": 6, + "norder": 1 + } + } + } + } + } + }, + "404": { + "description": "Quando a variante não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-variants-sku", + "description": "Retorna uma variante pelo SKU" + }, + "patch": { + "summary": "Atualiza uma variante", + "operationId": "patch-api-v2-variants-sku", + "responses": { + "204": { + "$ref": "#/components/responses/204" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "description": "Permite atualizar uma variante pelo SKU", + "tags": [ + "Variantes" + ], + "requestBody": { + "$ref": "#/components/requestBodies/Variant", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "main": { + "type": "boolean" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + }, + "height": { + "type": "number", + "description": "Altura do produto, em centímetros" + }, + "length": { + "type": "number", + "description": "Comprimento do produito, em centímetros" + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "handling_days": { + "type": "integer", + "description": "Dias de manuseio da variante" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "custom_attributes": { + "type": "object", + "description": "Customização da variante" + }, + "min_quantity": { + "type": "integer" + }, + "norder": { + "type": "integer" + }, + "property1": { + "$ref": "#/components/schemas/VariantProperty" + }, + "property2": { + "$ref": "#/components/schemas/VariantProperty" + }, + "property3": { + "$ref": "#/components/schemas/VariantProperty" + }, + "barcode": { + "type": "string" + }, + "quantity_sold": { + "type": "integer", + "description": "Quantidade de itens vendidos" + } + }, + "required": [ + "sku", + "quantity", + "price" + ] + } + } + } + } + } + }, + "/api/v2/products/{product_id}/variants/{sku}/images": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "$ref": "#/components/parameters/sku" + } + ], + "get": { + "summary": "Lista as imagens da variante com SKU na URL", + "tags": [ + "Variantes" + ], + "responses": { + "200": { + "$ref": "#/components/responses/VariantImages" + } + }, + "operationId": "get-api-v2-products-product_id-variants-sku-images", + "description": "Lista as imagens de uma variante passando o SKU da mesma na URL" + } + }, + "/api/v2/products/{product_id}/variants/images": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "sku", + "description": "SKU da variante" + } + ], + "get": { + "summary": "Lista as imagens da variante com SKU nos parâmetros", + "tags": [ + "Variantes" + ], + "responses": { + "200": { + "$ref": "#/components/responses/VariantImages" + } + }, + "operationId": "get-api-v2-products-product_id-variants-images", + "description": "Lista as imagens de uma variante passando o SKU da mesma nos parâmetros" + } + }, + "/api/v2/variants/{sku}/shipping_methods": { + "get": { + "parameters": [ + { + "$ref": "#/components/parameters/sku" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "quantity", + "allowEmptyValue": false, + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "zip", + "allowEmptyValue": false, + "required": true + } + ], + "summary": "Calcula frete", + "tags": [ + "Variantes" + ], + "operationId": "get-api-v2-variants-variant_sku-shipping_methods", + "responses": { + "200": { + "$ref": "#/components/responses/VariantShippings" + }, + "404": { + "description": "Quando a variante não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "description": "Calcula o frete para uma determinada variante" + } + }, + "/api/v2/tags": { + "get": { + "summary": "Lista as tags", + "tags": [ + "Tags" + ], + "responses": { + "200": { + "description": "Quando as tags são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tag.v1" + } + }, + "examples": { + "example-1": { + "value": [ + { + "products_count": 1, + "name": "camiseta", + "image_url": null, + "type": "", + "title": "Camisetas Legais", + "updated_at": "2015-03-10T04:33:08.699-03:00", + "subtitle": "Adulto e infantil", + "description": "

A loja oferece Camisetas para adultos e crianças

\n" + }, + { + "updated_at": "2017-11-05T13:23:50.107-02:00", + "products_count": 0, + "image_url": "//a0.vnda.com.br/loja/2017/03/14/15_49_52_10_Flag.png?1509895430", + "name": "promo-camiseta", + "title": "promo-camiseta", + "subtitle": null, + "description": null, + "type": "flag" + } + ] + } + } + } + } + } + }, + "operationId": "get-api-v2-tags", + "description": "Permite listar as tags", + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "limit", + "description": "Indica a quantidade de tags que devem ser listadas (page será ignorado)", + "deprecated": true + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "page", + "description": "Número da página" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "per_page", + "description": "Quantidade de resultados por página" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "type", + "description": "Exibe somente as tags com o tipo indicado" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "types", + "description": "Exibe somente as tags com um dos tipos indicados" + }, + { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "names", + "description": "Exibe somente as tags com um dos nomes indicados" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "images", + "description": "Quando passado qualquer valor filtra as tags que contenham imagens" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "show_in_carts", + "description": "Quando passado qualquer valor filtra as tags marcadas para serem exibidas no carrinho" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "product_id", + "description": "Exibe somente as tags do produto indicado" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "name", + "description": "Texto livre que permite filtrar as tags pelo nome" + }, + { + "schema": { + "type": "string", + "default": "name,asc", + "enum": [ + "name,asc", + "name,desc", + "type,asc", + "type,desc", + "title,asc", + "title,desc", + "products_count,asc", + "products_count,desc" + ] + }, + "in": "query", + "name": "sort", + "description": "String no formato , que determina o campo a ser ordenado e qual a ordem (asc,desc)" + } + ] + }, + "post": { + "summary": "Cria uma tag", + "operationId": "post-api-v2-tags", + "responses": { + "201": { + "description": "Quando a tag é criada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetos enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Tags" + ], + "description": "Cria uma tag", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "blurb": { + "type": "string", + "description": "Equivalente ao subtítulo" + }, + "description": { + "type": "string" + }, + "tag_type": { + "type": "string" + }, + "show_in_carts": { + "type": "boolean" + } + }, + "required": [ + "name" + ] + } + } + } + } + } + }, + "/api/v2/tags/types": { + "get": { + "summary": "Lista os tipos de tags", + "tags": [ + "Tags" + ], + "responses": { + "200": { + "description": "Quanto os tipos são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "operationId": "get-api-v2-tags-types", + "description": "Lista os tipos de tags usados em alguma tag", + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "page", + "description": "Número da página" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "per_page", + "description": "Quantidade de resultados por página" + } + ] + } + }, + "/api/v2/tags/{name}": { + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "[a-z0-9\\-_]+" + }, + "name": "name", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Retorna uma tag", + "tags": [ + "Tags" + ], + "responses": { + "200": { + "description": "Quando a tag é retornada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tag.v1" + } + } + } + }, + "404": { + "description": "Quando a tag não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-tags-name", + "description": "Retorna uma tag" + }, + "patch": { + "summary": "Atualiza uma tag", + "operationId": "patch-api-v2-tags-name", + "responses": { + "204": { + "description": "Quando a tag é atualizada" + }, + "404": { + "description": "Quando a tag não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Tags" + ], + "description": "Permite atualizar uma tag" + }, + "delete": { + "summary": "Remove uma tag", + "operationId": "delete-api-v2-tags-name", + "responses": { + "204": { + "description": "Quando a tag é removida" + }, + "404": { + "description": "Quando a tag não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Tags" + ], + "description": "Remove uma tag" + } + }, + "/api/v2/coupon_codes/{code}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "code", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Retorna um cupom", + "tags": [ + "Cupons de desconto" + ], + "responses": { + "200": { + "description": "Quando o cupom é encontrado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "code": { + "type": "string" + }, + "discount_id": { + "type": "number" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "code", + "discount_id", + "updated_at" + ] + }, + "examples": { + "example-1": { + "value": { + "code": "98008F", + "discount_id": 1, + "id": 1231, + "updated_at": "2020-10-27T19:12:51.858-03:00" + } + } + } + } + } + }, + "404": { + "description": "Quando o cupom não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-coupon_codes-code", + "description": "Retorna os dados de um cupom usando o seu código" + } + }, + "/api/v2/carts/{cart_id}/payment/paypal": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "cart_id", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Cria um pedido no Paypal", + "operationId": "post-api-v2-carts-cart_id-payments-paypal", + "responses": { + "200": { + "description": "Quando o pedido é criado no Paypal", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "id": { + "type": "string" + }, + "links": { + "type": "array", + "items": { + "type": "object", + "properties": { + "href": { + "type": "string" + }, + "rel": { + "type": "string" + }, + "method": { + "type": "string" + } + } + } + } + } + }, + "examples": { + "example-1": { + "value": { + "id": "31G50456P87181405", + "status": "CREATED", + "links": [ + { + "href": "https://api.sandbox.paypal.com/v2/checkout/orders/31G50456P87181405", + "rel": "self", + "method": "GET" + }, + { + "href": "https://www.sandbox.paypal.com/checkoutnow?token=31G50456P87181405", + "rel": "approve", + "method": "GET" + }, + { + "href": "https://api.sandbox.paypal.com/v2/checkout/orders/31G50456P87181405", + "rel": "update", + "method": "PATCH" + }, + { + "href": "https://api.sandbox.paypal.com/v2/checkout/orders/31G50456P87181405/capture", + "rel": "capture", + "method": "POST" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Quando o pedido não é criado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + }, + "tags": [ + "Pagamentos" + ], + "description": "Cria um pedido no Paypal para que posteriormente possa receber um pagamento" + } + }, + "/api/v2/clients": { + "get": { + "summary": "Lista os clientes", + "tags": [ + "Clientes" + ], + "responses": { + "200": { + "description": "Quando os clientes são encontrados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Client.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-clients", + "description": "Retorna uma lista de clientes. Caso seja informado o parâmetro \"email\", então apenas o cliente com esse email será retornado", + "parameters": [ + { + "schema": { + "type": "string", + "format": "email" + }, + "in": "query", + "name": "email", + "description": "Retorna somente o cliente com o email informado", + "deprecated": true + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "page", + "description": "Número da página" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "per_page", + "description": "Registros por página" + }, + { + "schema": { + "type": "string", + "format": "date-time" + }, + "in": "query", + "name": "min_updated_at", + "description": "Filtra os clientes pela menor data de atualização" + }, + { + "schema": { + "type": "string", + "format": "date-time" + }, + "in": "query", + "name": "max_updated_at", + "description": "Filtra os clientes pela maior data de atualização" + }, + { + "schema": { + "type": "string", + "format": "date-time" + }, + "in": "query", + "name": "birthday_start", + "description": "Data de inicío da filtragem de clientes pela data de aniversário" + }, + { + "schema": { + "type": "string", + "format": "date-time" + }, + "in": "query", + "name": "birthday_end", + "description": "Data final da filtragem de clientes pela data de aniversário" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "has_phone", + "description": "Filtra os clientes que possuem telefone" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "has_first_name", + "description": "Filtra os clientes que possuem first name" + }, + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "user_id", + "description": "Filtra os clientes por vendedor" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "term", + "description": "Filtra os clientes que possuem o termo em alguns dos campos" + }, + { + "schema": { + "type": "string", + "enum": [ + "name", + "birthdate" + ] + }, + "in": "query", + "name": "sort", + "description": "Ordena o resultado da busca de clientes conforme a opção escolhida" + } + ], + "security": [ + { + "Token": [] + } + ] + }, + "post": { + "summary": "Cria um cliente", + "operationId": "post-api-v2-clients", + "responses": { + "201": { + "description": "Quando o cliente é criado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "gender": { + "type": "string" + }, + "phone_area": { + "type": "string", + "pattern": "[0-9]+" + }, + "phone": { + "type": "string", + "pattern": "[0-9]+" + }, + "cpf": { + "type": "string", + "pattern": "[0-9]+" + }, + "cnpj": { + "type": "string", + "pattern": "[0-9]+" + }, + "ie": { + "type": "string" + }, + "tags": { + "type": "string" + }, + "lists": { + "type": "array", + "items": { + "type": "string" + } + }, + "facebook_uid": { + "type": "string" + }, + "liked_facebook_page": { + "type": "boolean" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "birthdate": { + "type": "string", + "format": "date" + }, + "recent_address": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "company_name": { + "type": "string" + }, + "street_name": { + "type": "string" + }, + "street_number": { + "type": "string" + }, + "neighborhood": { + "type": "string" + }, + "complement": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "first_phone_area": { + "type": "string" + }, + "first_phone": { + "type": "string" + }, + "second_phone_area": { + "type": "string" + }, + "second_phone": { + "type": "string" + }, + "email": { + "type": "string" + }, + "documents": { + "type": "object", + "properties": { + "cpf": { + "type": "string" + }, + "cnpj": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Clientes" + ], + "description": "Permite criar um cliente", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "birthdate": { + "type": "string", + "format": "date" + }, + "gender": { + "type": "string", + "enum": [ + "M", + "F" + ] + }, + "tags": { + "type": "string", + "description": "separado por vírgula", + "example": "foo,bar" + }, + "lists": { + "type": "array", + "items": { + "type": "string" + } + }, + "password": { + "type": "string" + }, + "password_confirmation": { + "type": "string" + }, + "terms": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/api/v2/clients/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true, + "description": "" + } + ], + "get": { + "summary": "Retorna um cliente", + "tags": [ + "Clientes" + ], + "responses": { + "200": { + "description": "Quando o cliente é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client.v1" + } + } + } + }, + "404": { + "description": "Quando o cliente não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-clients-id", + "description": "Permite retornar as informações do cliente\nO auth_token do cliente pode ser informado no lugar do ID na URL" + }, + "patch": { + "summary": "Atualiza um cliente", + "operationId": "patch-api-v2-clients-id", + "responses": { + "204": { + "description": "Quando o cliente é atualizado" + }, + "404": { + "description": "Quando o cliente não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados estão incorretos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Clientes" + ], + "description": "Permite atualizar as informações do cliente", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "birthdate": { + "type": "string", + "format": "date" + }, + "gender": { + "type": "string", + "enum": [ + "M", + "F" + ] + }, + "tags": { + "type": "string", + "description": "separado por vírgula", + "example": "foo,bar" + }, + "lists": { + "type": "array", + "items": { + "type": "string" + } + }, + "password": { + "type": "string" + }, + "password_confirmation": { + "type": "string" + }, + "terms": { + "type": "boolean" + } + } + } + } + } + } + }, + "delete": { + "summary": "Remove um cliente", + "operationId": "delete-api-v2-clients-id", + "responses": { + "204": { + "description": "Quando o cliente é removido" + }, + "404": { + "description": "Quando o cliente não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Clientes" + ], + "description": "Permite remover um cliente" + } + }, + "/api/v2/clients/{id}/orders": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista os pedidos", + "tags": [ + "Clientes" + ], + "responses": { + "200": { + "description": "Quando os pedidos são retornados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-clients-id-orders", + "description": "Retorna a lista de pedidos do cliente" + } + }, + "/api/v2/clients/{id}/addresses": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista os endereços", + "tags": [ + "Clientes" + ], + "responses": { + "200": { + "description": "Quando os endereços são listados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Address.v1" + } + } + } + } + }, + "operationId": "get-api-v2-clients-id-addresses", + "description": "Lista os endereços do cliente utilizados nos pedidos que foram confirmados", + "parameters": [ + { + "$ref": "#/components/parameters/status" + } + ] + } + }, + "/api/v2/clients/{client_id}/registered_addresses": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "client_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista os endereços cadastrados pelo cliente", + "tags": [ + "Clientes" + ], + "responses": { + "200": { + "description": "Quando os endereços são listados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientAddress.v1" + } + } + } + } + }, + "operationId": "get-api-v2-clients-id-regitered-addresses", + "description": "Lista os endereços cadastrados pelo cliente" + }, + "post": { + "summary": "Cria um endereço do cliente", + "operationId": "post-api-v2-clients-id-regitered-addresses", + "responses": { + "201": { + "description": "Quando o endereço do cliente é criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientAddress.v1" + } + } + } + }, + "404": { + "description": "Quando o cliente não existe", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Clientes" + ], + "description": "Permite criar um endereço do cliente", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "street_name": { + "type": "string" + }, + "street_number": { + "type": "string" + }, + "complement": { + "type": "string" + }, + "neighborhood": { + "type": "string" + }, + "label": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "reference": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/v2/clients/{client_id}/registered_addresses/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "client_id", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Atualiza um endereço do cliente", + "operationId": "patch-api-v2-clients-id-regitered-addresses-id", + "responses": { + "200": { + "description": "Quando o endereço do cliente é atualizado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientAddress.v1" + } + } + } + }, + "404": { + "description": "Quando o cliente ou endereço não existe", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Clientes" + ], + "description": "Permite atualizar um endereço do cliente", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "street_name": { + "type": "string" + }, + "street_number": { + "type": "string" + }, + "complement": { + "type": "string" + }, + "neighborhood": { + "type": "string" + }, + "label": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "reference": { + "type": "string" + } + } + } + } + } + } + }, + "delete": { + "summary": "Deleta o endereço cadastrados pelo cliente", + "tags": [ + "Clientes" + ], + "responses": { + "204": { + "description": "Quando o endereço é deletado" + }, + "404": { + "description": "Quando o endereço não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "delete-api-v2-clients-id-regitered-addresses-id", + "description": "Delete o endereço cadastrado pelo cliente" + } + }, + "/api/v2/clients/recover_password": { + "parameters": [], + "post": { + "summary": "Reseta a senha", + "operationId": "post-api-v2-clients-recover_password", + "responses": { + "200": { + "description": "Quando a senha for criada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client.v1" + } + } + } + }, + "404": { + "description": "Quando um cliente não é encontrado com o email informado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Clientes" + ], + "description": "Cria uma senha para o cliente e envia por email", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "email", + "description": "Email do cliente", + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "no_send", + "description": "Preencher para pular o envio do email de senha para o cliente" + } + ] + } + }, + "/api/v2/clients/{id}/credits": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Saldo de créditos", + "tags": [ + "Clientes" + ], + "responses": { + "200": { + "description": "Quando o saldo é retornado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "balance": { + "type": "number" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-clients-client_id-credits", + "description": "Retorna o saldo de crétitos do cliente" + } + }, + "/api/v2/clients/{id}/credits/transfers": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Extrato de créditos", + "tags": [ + "Clientes" + ], + "responses": { + "200": { + "description": "Quando as transferências são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "from": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "amount": { + "type": "number" + } + } + }, + "to": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "amount": { + "type": "number" + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Quando o cliente não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-clients-id-credits-transfers", + "description": "Retorna as transfertências de crétidos realizadas" + } + }, + "/api/v2/clients/{id}/bonuses": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true, + "description": "Código do cliente" + } + ], + "get": { + "summary": "Lista os bônus", + "tags": [ + "Clientes" + ], + "responses": { + "200": { + "description": "Quando os bônus são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bonus.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-clients-id-bonuses", + "description": "Lista os bônus do cliente que ainda não foram utilizados", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "page", + "description": "Número da página" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "per_page", + "description": "Registros por página" + } + ] + } + }, + "/api/v2/clients/{id}/remove_personal_data": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true, + "description": "" + } + ], + "patch": { + "summary": "Solicitação de esquecimento", + "operationId": "patch-api-v2-clients-id-remove-personal-data", + "responses": { + "204": { + "description": "Quando o cliente é marcado para ter seus dados pessoais removidos" + }, + "404": { + "description": "Quando o cliente não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Clientes" + ], + "description": "Solicita a remoção (esquecimento) dos dados pessoais de um cliente, de acordo com a LGPD" + } + }, + "/api/v2/auth/email/{token}": { + "get": { + "summary": "Faz login do cliente por token", + "operationId": "get-api-v2-auth-email-token", + "responses": { + "200": { + "description": "Quanto o login é feito com sucesso", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "token": { + "type": "string" + } + }, + "required": [ + "id", + "token" + ] + } + } + } + }, + "401": { + "description": "Quando o token do email é inválido ou expirou" + } + }, + "tags": [ + "Clientes" + ], + "description": "Faz o login do cliente pelo token salvo no campo auth_token", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "token", + "in": "path", + "required": true + } + ] + } + }, + "/api/v2/auth/client": { + "post": { + "summary": "Faz login do cliente", + "operationId": "post-api-v2-auth-client", + "responses": { + "200": { + "description": "Quanto o login é feito com sucesso", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "auth_token": { + "type": "string" + } + }, + "required": [ + "id", + "auth_token" + ] + } + } + } + }, + "400": { + "description": "Quando o email e/ou a senha estão vazios", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": [ + "'email' is mandatory", + "'password' is mandatory" + ] + } + }, + "required": [ + "error" + ] + } + } + } + }, + "422": { + "description": "Quando o cliente não é encontrado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": [ + "email and/or password invalid" + ] + } + }, + "required": [ + "error" + ] + } + } + } + } + }, + "tags": [ + "Clientes" + ], + "description": "Faz o login do cliente por usuário e senha", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + } + }, + "required": [ + "email", + "password" + ] + } + } + } + } + } + }, + "/api/v2/carts/{cart_id}/payment": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "cart_id", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Faz o pagamento do carrinho", + "operationId": "post-api-v2-carts-cart-payment_id-payment", + "responses": { + "301": { + "description": "Quando o pagamento é processado corretamente", + "headers": { + "X-Attempt-Count": { + "schema": { + "type": "number" + }, + "description": "Número de tentativas de pagamento feitas para o carrinho" + }, + "Location": { + "schema": { + "type": "string" + }, + "description": "URL do pedido na API" + } + } + }, + "400": { + "description": "Quando o carrinho não pode ser pago", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + }, + "examples": { + "Itens indisponíveis": { + "value": { + "error": "Os itens do carrinho não estão mais disponíveis" + } + }, + "Alteração nos preços dos itens": { + "value": { + "error": "Houve uma alteração nos valores do carrinho" + } + } + } + } + } + }, + "404": { + "description": "Quando o carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "example-1": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "description": "Faz o pagamento do carrinho usando a forma de pagamento informada", + "tags": [ + "Pagamentos" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "payment_method": { + "type": "string", + "enum": [ + "pix" + ], + "description": "Meio de pagamento" + }, + "channel": { + "type": "string", + "default": "ecommerce", + "enum": [ + "ecommerce", + "direct" + ], + "description": "Canal de venda do carrinho" + } + }, + "required": [ + "payment_method" + ] + } + } + }, + "description": "" + } + } + }, + "/api/v2/menus": { + "get": { + "summary": "Lista os menus", + "operationId": "get-api-v2-menus", + "tags": [ + "Menus" + ], + "responses": { + "200": { + "description": "Quando os menus são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Menu.v1" + } + } + } + } + } + }, + "description": "Lista os menus", + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "parent_id" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "position" + } + ] + }, + "post": { + "summary": "Cria um menu", + "operationId": "post-api-v2-menus", + "tags": [ + "Menus" + ], + "responses": { + "201": { + "description": "Quando o menu é criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Menu.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "description": "Cria um menu", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "tooltip": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "page_id": { + "type": "integer" + }, + "parent_id": { + "type": "integer" + }, + "position": { + "type": "string" + }, + "new_position": { + "type": "string" + }, + "external": { + "type": "boolean" + }, + "tag_id": { + "type": "integer" + } + }, + "required": [ + "label", + "position", + "type" + ] + } + } + } + } + } + }, + "/api/v2/menus/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Retorna um menu", + "tags": [ + "Menus" + ], + "responses": { + "200": { + "description": "Quando o menu é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Menu.v1" + } + } + } + }, + "404": { + "description": "Quando o menu não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-menus-id", + "description": "Retorna um menu" + }, + "patch": { + "summary": "Atualiza um menu", + "operationId": "patch-api-v2-menus-id", + "responses": { + "204": { + "description": "Quando o menu é atualizado" + }, + "404": { + "description": "Quando o menu não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Menus" + ], + "description": "Atualiza um menu", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "tooltip": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "page_id": { + "type": "integer" + }, + "parent_id": { + "type": "integer" + }, + "position": { + "type": "string" + }, + "new_position": { + "type": "string" + }, + "external": { + "type": "boolean" + }, + "tag_id": { + "type": "integer" + } + }, + "required": [ + "label", + "position", + "type" + ] + } + } + } + } + }, + "delete": { + "summary": "Remove um menu", + "operationId": "delete-api-v2-menus-id", + "responses": { + "204": { + "description": "Quando o menu é removido" + }, + "404": { + "description": "Quando o menu não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Menus" + ], + "description": "Remove um menu" + } + }, + "/api/v2/menus/positions": { + "get": { + "summary": "Lista as posições dos menus", + "operationId": "get-api-v2-menus-positions", + "tags": [ + "Menus" + ], + "responses": { + "200": { + "description": "Quando as posições são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "example-1": { + "value": [ + "principal", + "submenu" + ] + } + } + } + } + } + }, + "description": "Lista as posições dos menus" + } + }, + "/api/v2/menus/reorder": { + "post": { + "summary": "Reordena os menus", + "operationId": "post-api-v2-menus-reorder", + "tags": [ + "Menus" + ], + "responses": { + "200": { + "description": "Quando os menus são reordenados" + } + }, + "description": "Reordena os menus na ordem em que seus ids são listados no request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "description": "A ordem dos elementos será replicada para os menus", + "items": { + "type": "integer" + } + } + }, + "required": [ + "ids" + ] + }, + "examples": { + "example-1": { + "value": { + "ids": [ + 32, + 29, + 28, + 31, + 30, + 27 + ] + } + } + } + } + } + } + } + }, + "/api/v2/menus/tree": { + "get": { + "summary": "Retorna os menus em árvore", + "operationId": "get-api-v2-menus-trees", + "tags": [ + "Menus" + ], + "responses": { + "200": { + "description": "Quando os menus são listados", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "description": "Posição", + "items": { + "$ref": "#/components/schemas/Menu_in_tree.v1" + } + } + } + } + } + } + }, + "description": "Retorna os menus em árvore, organizados pela posição" + } + }, + "/api/v2/site_message": { + "get": { + "summary": "Retorna uma mensagem do site", + "tags": [ + "Mensagens do site" + ], + "responses": { + "200": { + "description": "Quando a mensagem do site existe", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Site_message.v1" + } + } + } + }, + "404": { + "description": "Quando a mensagem do site não existe", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-site-message", + "description": "Retorna uma mensagem do site" + }, + "patch": { + "summary": "Cria ou atualiza uma mensagem do site", + "operationId": "post-api-v2-site-message", + "tags": [ + "Mensagens do site" + ], + "responses": { + "204": { + "description": "Quando a mensagem é criada ou atualizada" + } + }, + "description": "Cria ou atualiza uma mensagem do site", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "call_to_action": { + "type": "string" + } + } + } + } + } + } + }, + "delete": { + "summary": "Remove uma mensagem do site", + "operationId": "delete-api-v2-site-message", + "responses": { + "204": { + "description": "Quando a mensagem é ou não removida" + }, + "404": { + "description": "Quando o menu não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Mensagens do site" + ], + "description": "Remove uma mensagem do site" + } + }, + "/api/v2/shop/images": { + "get": { + "summary": "Lista as images", + "tags": [ + "Loja" + ], + "responses": { + "200": { + "description": "Quando as imagens são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Shop_asset.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-shop-images", + "description": "Lista as imagens associadas a loja", + "parameters": [ + { + "$ref": "#/components/parameters/page" + }, + { + "$ref": "#/components/parameters/per_page" + }, + { + "$ref": "#/components/parameters/sort" + } + ] + }, + "post": { + "summary": "Cria uma imagem", + "operationId": "post-api-v2-shop-images", + "responses": { + "201": { + "description": "Quando a imagem é criada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shop_asset.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviado são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Loja" + ], + "description": "Permite cadastrar uma imagem", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "position": { + "type": "string" + }, + "file_uid": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/v2/shop/images/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "delete": { + "summary": "Remove uma imagem", + "operationId": "delete-api-v2-shop-images-id", + "responses": { + "204": { + "description": "Quando a imagem é removida" + }, + "404": { + "description": "Quando a imagem não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "description": "Permite remover uma imagem da loja", + "tags": [ + "Loja" + ] + } + }, + "/api/v2/shop/product_attributes": { + "post": { + "summary": "Cria um atributo customizado de produto", + "operationId": "post-api-v2-shop-product-attributes", + "responses": { + "201": { + "description": "Quando o atributo customizado de produto é criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Products_attributes.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "description": "Permite adicionar um atributo customizado de produto", + "tags": [ + "Loja" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "index": { + "type": "number" + }, + "name": { + "type": "string" + }, + "mandatory": { + "type": "boolean" + } + }, + "required": [ + "index", + "name", + "mandatory" + ] + } + } + }, + "description": "Quando o atributo customizado é criado" + } + }, + "parameters": [] + }, + "/api/v2/customizations": { + "get": { + "summary": "Lista as personalizações", + "responses": { + "200": { + "description": "Quando as personalizações são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Customization.v1" + } + } + } + } + } + }, + "operationId": "get-api-v2-customizations", + "description": "Permite listar as personalizações", + "tags": [ + "Personalizações" + ], + "parameters": [ + { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "product_id", + "description": "Filtra por produto" + } + ] + }, + "post": { + "summary": "Cria uma personalização", + "operationId": "post-api-v2-customizations", + "responses": { + "201": { + "description": "Quando a personalização é criada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customization.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "description": "Permite criar uma personalização", + "tags": [ + "Personalizações" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_name": { + "type": "string" + }, + "group_type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "image_uid": { + "type": "string" + }, + "image_name": { + "type": "string" + }, + "price": { + "type": "number", + "default": 0 + }, + "quantity": { + "type": "integer", + "default": 0 + }, + "handling_days": { + "type": "integer", + "default": 0 + }, + "tag_id": { + "type": "integer" + }, + "sku": { + "type": "string" + }, + "pattern": { + "type": "string" + } + }, + "required": [ + "group_name", + "group_type", + "name", + "tag_id" + ] + } + } + }, + "description": "" + } + }, + "parameters": [] + }, + "/api/v2/customizations/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "delete": { + "summary": "Remove uma personalização", + "operationId": "delete-api-v2-customizations-id", + "responses": { + "204": { + "description": "Quando a personalização é removida" + }, + "404": { + "description": "Quando a personalização não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Personalizações" + ], + "description": "Permite remover uma personalização" + }, + "patch": { + "summary": "Altera uma personalização", + "operationId": "patch-api-v2-customizations-id", + "responses": { + "204": { + "description": "Quando a personalização é alterada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customization.v1" + } + } + } + }, + "404": { + "description": "Quando a personalização não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "description": "Permite alterar uma personalização", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "group_name": { + "type": "string" + }, + "group_type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "image_uid": { + "type": "string" + }, + "image_name": { + "type": "string" + }, + "price": { + "type": "string" + }, + "quantity": { + "type": "string" + }, + "handling_days": { + "type": "string" + }, + "tag_id": { + "type": "string" + }, + "sku": { + "type": "string" + }, + "pattern": { + "type": "string" + } + } + } + } + } + }, + "tags": [ + "Personalizações" + ] + }, + "get": { + "summary": "Retorna uma personalização", + "operationId": "get-api-v2-customizations-id", + "responses": { + "200": { + "description": "Quando a personalização é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customization.v1" + } + } + } + }, + "404": { + "description": "Quando a personalização não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Personalizações" + ], + "description": "Permite retornar uma personalização" + } + }, + "/api/v2/orders/{order_id}/items": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "order_id", + "in": "path", + "required": true + }, + { + "schema": { + "type": "boolean", + "default": false + }, + "in": "query", + "name": "include_customizations_in_total", + "description": "Inclui o preço dos produtos customizados no total do pedido" + } + ], + "get": { + "summary": "Lista os itens do pedido", + "responses": { + "200": { + "description": "Quando os itens do pedido são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order_items.v1" + } + } + } + } + }, + "404": { + "description": "Quando a lista de itens do pedido não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-orders-order_id-items", + "tags": [ + "Pedidos" + ], + "description": "Permite listar os itens do pedido" + } + }, + "/api/v2/orders/{order_id}/items/{item_id}/customizations": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "order_id", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "item_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista as personalizações do item do pedido", + "responses": { + "200": { + "description": "Quando as personalizações são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order_item_customization.v1" + } + }, + "examples": { + "Com pesonalizações": { + "value": [ + { + "id": 1, + "number": 1, + "group_name": "Color", + "sku": "A1", + "name": "Red", + "price": 0, + "intl_price": 0, + "handling_days": 0 + } + ] + }, + "Sem personalizações": { + "value": [] + } + } + } + } + }, + "404": { + "description": "Quando o item não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-orders-order_id-items-item_id-customizations", + "tags": [ + "Pedidos" + ], + "description": "Permite listar as personalizações de cada item do pedido" + } + }, + "/api/v2/carts/{cart_id}/items/{item_id}/customizations": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "cart_id", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "item_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista as personalizações do item do carrinho", + "responses": { + "200": { + "$ref": "#/components/responses/CartItemCustomizationList" + }, + "404": { + "description": "Quando o item não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-carts-cart_id-items-item_id-customizations", + "tags": [ + "Carrinhos" + ], + "description": "Permite listar as personalizações de cada item do carrinho" + }, + "delete": { + "summary": "Remove uma personalização do item do carrinho", + "operationId": "delete-api-v2-carts-cart_id-items-item_id-customizations", + "responses": { + "204": { + "description": "Quando a personalização é removida" + }, + "404": { + "description": "Quando a personalização não é encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "description": "Permite remover uma customização do item do carrinho", + "tags": [ + "Carrinhos" + ] + } + }, + "/api/v2/mappings": { + "get": { + "summary": "Lista os mapeamentos", + "tags": [ + "Mapeamentos" + ], + "responses": { + "200": { + "$ref": "#/components/responses/Mappings" + } + }, + "operationId": "get-api-v2-mappings", + "description": "Lista os mapeamentos", + "parameters": [ + { + "$ref": "#/components/parameters/page" + }, + { + "$ref": "#/components/parameters/per_page" + } + ] + }, + "post": { + "summary": "Cria um mapeamento", + "operationId": "post-api-v2-mappings", + "responses": { + "201": { + "$ref": "#/components/responses/MappingCreate" + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "tags": [ + "Mapeamentos" + ], + "description": "Cria um mapeamento", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "from": { + "type": "array", + "items": { + "type": "string" + } + }, + "to": { + "type": "string" + } + }, + "required": [ + "key" + ] + } + } + } + } + } + }, + "/api/v2/mappings/{id}": { + "parameters": [ + { + "schema": { + "type": "integer" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Retorna um mapeamento", + "tags": [ + "Mapeamentos" + ], + "responses": { + "200": { + "$ref": "#/components/responses/Mapping" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "operationId": "get-api-v2-mappings-id", + "description": "Retorna os dados de um mapeamento" + }, + "patch": { + "summary": "Atualiza um mapeamento", + "operationId": "patch-api-v2-mappings-id", + "responses": { + "204": { + "description": "Quando o mapeamento é atualizado" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "tags": [ + "Mapeamentos" + ], + "description": "Atualiza um mapeamento", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "from": { + "type": "array", + "items": { + "type": "string" + } + }, + "to": { + "type": "string" + } + }, + "required": [ + "key" + ] + } + } + } + } + }, + "delete": { + "summary": "Remove um mapeamento", + "operationId": "delete-api-v2-mappings-id", + "responses": { + "204": { + "description": "Quando o mapeamento é removido" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "tags": [ + "Mapeamentos" + ], + "description": "Remove um mapeamento" + } + }, + "/api/v2/banners": { + "get": { + "summary": "Lista os banners", + "responses": { + "200": { + "$ref": "#/components/responses/Banners" + } + }, + "operationId": "get-api-v2-banners", + "description": "Retorna a lista de banners", + "tags": [ + "Mídias" + ], + "parameters": [ + { + "$ref": "#/components/parameters/only_valid" + }, + { + "$ref": "#/components/parameters/only_expired" + }, + { + "$ref": "#/components/parameters/only_scheduled" + }, + { + "$ref": "#/components/parameters/tag" + }, + { + "$ref": "#/components/parameters/title" + }, + { + "$ref": "#/components/parameters/no_paginate" + }, + { + "$ref": "#/components/parameters/page" + }, + { + "$ref": "#/components/parameters/per_page" + } + ] + } + }, + "/api/v2/banners/{id}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Retorna um banner", + "tags": [ + "Mídias" + ], + "responses": { + "200": { + "$ref": "#/components/responses/Banner" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "operationId": "get-api-v2-banners-id", + "description": "Retorna os dados de um banner" + } + }, + "/api/v2/banners/all": { + "get": { + "summary": "Retorna os banners agrupados por tag", + "tags": [ + "Mídias" + ], + "responses": { + "200": { + "$ref": "#/components/responses/AllBanners" + } + }, + "operationId": "get-api-v2-banners-all", + "parameters": [], + "description": "Retorna todos os banners disponíveis agrupados por tag" + }, + "parameters": [] + }, + "/api/v2/carts/{cart_id}/shipping_methods/intl": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "cart_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Cálculo de frete internacional", + "tags": [ + "Carrinhos" + ], + "responses": { + "200": { + "description": "Quando as formas de entrega são retornadas", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "{package_label}": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Shipping_methods.v1" + } + } + } + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "operationId": "get-api-v2-carts-cart_id-shipping_methods-intl", + "description": "Permite calcular o frete para pedidos internacionais", + "parameters": [ + { + "schema": { + "type": "string", + "example": "BRA", + "pattern": "^[A-Z]{3}$" + }, + "in": "query", + "name": "country", + "description": "Código do país de destino", + "required": true + } + ] + } + }, + "/api/v2/carts/{cart_id}/samples": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "cart_id", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Lista as amostras", + "tags": [ + "Carrinhos" + ], + "responses": { + "200": { + "description": "Quando as amostras são listadas", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "image_url": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string" + }, + "variants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "main": { + "type": "boolean" + }, + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "image_url": { + "type": "string", + "nullable": true + }, + "product_id": { + "type": "integer" + }, + "norder": { + "type": "integer" + } + }, + "required": [ + "id", + "main", + "sku", + "name", + "updated_at", + "image_url", + "product_id", + "norder" + ] + } + } + }, + "required": [ + "id", + "image_url", + "name", + "reference", + "updated_at", + "url", + "variants" + ] + } + } + } + }, + "404": { + "description": "Quando um carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-carts-cart-id-samples", + "description": "Lista as amostras disponíveis para determinado carrinho" + } + }, + "/api/v2/carts/{cart_id}/shipping_address": { + "parameters": [ + { + "$ref": "#/components/parameters/cart_id" + } + ], + "get": { + "summary": "Endereço de entrega", + "tags": [ + "Carrinhos" + ], + "responses": { + "200": { + "description": "Quando o endereço é retornado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "company_name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "documents": { + "type": "object", + "description": "Serão retornados apenas os campos preenchidos", + "properties": { + "cpf": { + "type": "string" + }, + "cnpj": { + "type": "string" + }, + "ie": { + "type": "string" + } + } + }, + "street_name": { + "type": "string" + }, + "street_number": { + "type": "string", + "example": "188A" + }, + "complement": { + "type": "string" + }, + "neighborhood": { + "type": "string" + }, + "first_phone_area": { + "type": "string", + "description": "Somente números", + "example": "11" + }, + "first_phone": { + "type": "string", + "description": "Somente números", + "example": "984453322" + }, + "second_phone_area": { + "type": "string", + "description": "Somente números" + }, + "second_phone": { + "type": "string", + "description": "Somente números" + }, + "reference": { + "type": "string" + }, + "zip": { + "type": "string", + "description": "Somente números", + "example": "90050000" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string", + "example": "RS", + "minLength": 2, + "maxLength": 2 + }, + "recipient_name": { + "type": "string" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "street_name", + "street_number", + "neighborhood", + "first_phone_area", + "first_phone", + "zip", + "city", + "state" + ], + "$ref": "#/components/schemas/Shipping_address" + } + } + } + }, + "404": { + "description": "Quando o carrinho não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-carts-cart_id-shipping_address", + "description": "Retorna o endereço de entrega" + }, + "post": { + "summary": "Adiciona um endereço de entrega", + "description": "Adiciona um endereço de entrega no carrinho", + "tags": [ + "Envio do carrinho" + ], + "operationId": "post-api-v2-carts-cart_id-shipping_address", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shipping_address" + } + } + } + }, + "responses": { + "201": { + "description": "Endereço adicionado com sucesso", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart_item.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/carts/{cart_id}/coupon_code": { + "parameters": [ + { + "$ref": "#/components/parameters/cart_id" + } + ], + "post": { + "summary": "Associa código de cupom ao carrinho", + "tags": [ + "Carrinhos" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Código do cupom" + } + }, + "required": [ + "code" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Quando o cupom é associado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Código do cupom" + }, + "discount": { + "type": "number" + }, + "rebate_token": { + "type": "string" + }, + "rebate_discount": { + "type": "number" + } + }, + "required": [ + "code", + "discount", + "rebate_token", + "rebate_discount" + ] + } + } + } + }, + "404": { + "description": "Quando o carrinho ou o desconto não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "operationId": "post-api-v2-carts-cart_id-coupon_code", + "description": "Associa um código de cupom ao carrinho" + } + }, + "/api/v2/orders/channels": { + "get": { + "summary": "Lista os canais dos pedidos", + "tags": [ + "Pedidos" + ], + "responses": { + "200": { + "$ref": "#/components/responses/Channels" + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "operationId": "get-api-v2-orders-channels", + "description": "Lista todos os channels usados nos pedidos criados" + } + }, + "/api/v2/orders/states": { + "get": { + "summary": "Lista os estados dos pedidos", + "tags": [ + "Pedidos" + ], + "responses": { + "200": { + "$ref": "#/components/responses/States" + }, + "404": { + "description": "Domínio de loja não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "get-api-v2-orders-states", + "description": "Lista todos os estados usados nos pedidos criados", + "security": [ + { + "Token": [] + } + ] + } + }, + "/api/v2/products/{product_id}/price": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "product_id", + "in": "path", + "required": true, + "description": "ID do produto" + } + ], + "get": { + "summary": "Lista os preços do produto", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "$ref": "#/components/responses/ProductPrice" + } + }, + "operationId": "get-api-v2-products-product_id-price", + "description": "Retorna o preço do produto e das variantes", + "security": [ + { + "Token": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/coupon_codes" + } + ] + } + }, + "/api/v2/products/{product_id}/images": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + } + ], + "get": { + "summary": "Lista as imagens do produto", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "$ref": "#/components/responses/ProductImages" + } + }, + "operationId": "get-api-v2-products-product_id-images", + "description": "Lista as imagens do produto" + }, + "post": { + "summary": "Cria uma imagem do produto", + "tags": [ + "Produtos" + ], + "responses": { + "201": { + "$ref": "#/components/responses/ProductImage" + }, + "404": { + "description": "Quando o produto não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "file_url": { + "type": "string", + "format": "uri" + }, + "variant_ids": { + "type": "array", + "description": "IDs da variantes associadas a imagem", + "items": { + "type": "integer" + } + } + }, + "required": [ + "file_url" + ] + } + } + } + }, + "operationId": "post-api-v2-products-product_id-images", + "description": "Cria uma imagem do produto" + } + }, + "/api/v2/products/{product_id}/images/{id}": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "delete": { + "summary": "Deleta uma imagem do produto", + "tags": [ + "Produtos" + ], + "responses": { + "204": { + "description": "Quando a imagem é deletada" + }, + "404": { + "description": "Quando o produto ou a imagem não são encontrados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "delete-api-v2-products-product_id-images-id", + "description": "Deleta uma imagem do produto" + } + }, + "/api/v2/products/{product_id}/images/reorder": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + } + ], + "post": { + "summary": "Reordena imagens do produto", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "description": "Quando as imagens são reordenadas" + }, + "404": { + "description": "Quando o produto não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [ + "ids" + ] + } + } + } + }, + "operationId": "post-api-v2-products-product_id-images-reorder", + "description": "Reordena as imagens do produto" + } + }, + "/api/v2/products/{product_id}/images/{id}/add_variant": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Associa imagem com variante", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "description": "Quando a imagem é associada com a variante" + }, + "404": { + "description": "Quando o produto ou a imagem não são encontrados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "variant_id": { + "type": "integer" + } + }, + "required": [ + "variant_id" + ] + } + } + } + }, + "operationId": "post-api-v2-products-product_id-images-id-add_variant", + "description": "Associa a imagem com uma variante" + } + }, + "/api/v2/products/{product_id}/images/{id}/remove_variant": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Desassocia imagem da variante", + "tags": [ + "Produtos" + ], + "responses": { + "200": { + "description": "Quando a imagem é desassociada da variante" + }, + "404": { + "description": "Quando o produto ou a imagem não são encontrados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "variant_id": { + "type": "integer" + } + }, + "required": [ + "variant_id" + ] + } + } + } + }, + "operationId": "post-api-v2-products-product_id-images-id-remove_variant", + "description": "Remove a associação da imagem com uma variante" + } + }, + "/api/v2/orders/{order_code}/packages": { + "parameters": [ + { + "$ref": "#/components/parameters/order_code" + } + ], + "get": { + "summary": "Lista os pacotes de um pedido", + "tags": [ + "Pacotes" + ], + "responses": { + "200": { + "$ref": "#/components/responses/Packages" + }, + "404": { + "description": "Pedido ou pacote não encontrados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-orders-order_code-packages", + "description": "Retorna uma lista de pacotes de um pedido", + "security": [ + { + "Token": [] + } + ] + } + }, + "/api/v2/events": { + "post": { + "summary": "Dispara eventos", + "operationId": "post-api-v2-events", + "responses": { + "204": { + "description": "Quando o evento é recebido" + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Eventos" + ], + "description": "Indica para a API que dererminado evento aconteceu e que ela deve disparar as ações relacionadas", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "event_type", + "required": true, + "description": "Evento que ocorreu" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "id", + "description": "ID do recurso selacionado ao evento", + "required": true + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "browser_ip", + "description": "IP do usuário" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "user_agent", + "description": "User agent do usuário" + } + ] + } + }, + "/api/v2/users/{id}/payables": { + "get": { + "tags": [ + "Recebíveis de usuários" + ], + "summary": "Lista os recebíveis de um usuário pelo ID", + "responses": { + "200": { + "description": "Lista de recebíveis do usuário", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Payables.v1" + } + } + } + } + }, + "404": { + "description": "Quando o usuário não está cadastrado como recebedor", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "description": "Permite a listagem de recebíveis (comissão) de um usuário vendedor da loja, quando ocorre split de pagamentos via Pagarme ", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id", + "in": "path", + "required": true, + "description": "Código idenficador de usuário" + } + ], + "operationId": "get-api-v2-users-id-payables" + } + }, + "/api/v2/products/{product_id}": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + } + ], + "get": { + "summary": "Retorna um produto", + "tags": [ + "Produto" + ], + "responses": { + "200": { + "description": "Produto encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product.v1" + }, + "examples": { + "example-1": { + "value": { + "id": 0, + "active": true, + "available": true, + "category_tags": [ + { + "tag_type": "string", + "name": "string", + "title": "string" + } + ], + "description": "string", + "discount_id": 0, + "html_description": "string", + "image_url": "string", + "installments": [ + 0 + ], + "min_quantity": "string", + "name": "string", + "on_sale": true, + "plain_description": "string", + "price": 0, + "rating": { + "rating": 0, + "votes": 0 + }, + "reference": "string", + "sale_price": 0, + "slug": "string", + "tag_names": [ + "string" + ], + "updated_at": "string", + "url": "string", + "variants": [ + { + "{id}": { + "available": true, + "available_quantity": 0, + "custom_attributes": {}, + "handling_days": 0, + "height": 0, + "id": 1, + "image_url": "string", + "installments": [ + 0 + ], + "inventories": [ + { + "created_at": "2019-08-24T14:15:22Z", + "id": 0, + "name": null, + "place_id": 0, + "price": 0, + "quantity": 0, + "quantity_sold": 0, + "sale_price": 0, + "slug": "string", + "updated_at": "2019-08-24T14:15:22Z", + "variant_id": 0, + "place_name": "string" + } + ], + "length": 0, + "main": true, + "min_quantity": 0, + "name": "string", + "norder": 0, + "price": 0, + "product_id": 0, + "properties": { + "property1": { + "defining": true, + "name": "string", + "value": "string" + }, + "property2": { + "defining": true, + "name": "string", + "value": "string" + }, + "property3": { + "defining": true, + "name": "string", + "value": "string" + } + }, + "quantity": 0, + "quantity_sold": 0, + "sale_price": 0, + "sku": "string", + "slug": "string", + "stock": 0, + "updated_at": "2019-08-24T14:15:22Z", + "weight": 0, + "width": 0 + } + } + ], + "discount_rule": null, + "images": [ + { + "id": 0, + "url": "string", + "updated_at": "2019-08-24T14:15:22Z", + "variant_ids": [ + 0 + ] + } + ] + } + } + } + } + } + }, + "404": { + "description": "Produto não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product.v1" + } + } + } + } + }, + "operationId": "get-api-v2-products-id", + "description": "Retorna um produto pelo código identificador (`product_id`)", + "parameters": [ + { + "$ref": "#/components/parameters/coupon_codes", + "description": "Cupons para calcular o desconto no produto consultado" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "include_inventory_place", + "description": "Selecione `true` para incluir o nome do local de armazenamento no retorno da requisição" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "include_images", + "description": "Selecione `true` para incluir todas as imagens do produto" + } + ] + }, + "patch": { + "summary": "Atualiza um produto", + "operationId": "patch-api-v2-products-id", + "responses": { + "204": { + "description": "Produto atualizado" + }, + "404": { + "description": "Produto não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Parâmetros enviados inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Produto" + ], + "description": "Atualiza informações de um produto no catálogo pelo código identificador (`product_id`)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimpleProduct" + } + } + } + } + }, + "delete": { + "summary": "Remove um produto", + "operationId": "delete-api-v2-products-id", + "responses": { + "204": { + "description": "Produto removido" + }, + "404": { + "description": "Produto não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Produto" + ], + "description": "Remove um produto do catálogo pelo código indentificador (`product_id`)" + } + }, + "/api/v2/products/{product_id}/rate": { + "post": { + "summary": "Avalia um produto", + "tags": [ + "Produto" + ], + "responses": { + "200": { + "description": "Avaliação enviada", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rating": { + "type": "string", + "description": "Média das avaliações" + }, + "votes": { + "type": "string", + "description": "Número de avaliações recebidas" + } + } + }, + "examples": { + "example-1": { + "value": { + "rating": "0.9", + "votes": "2" + } + } + } + } + } + }, + "400": { + "description": "Parâmetros enviados inválidos", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "example-1": { + "value": { + "error": "invalid rate value" + } + } + } + } + } + }, + "404": { + "description": "Produto não possui variantes", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "example-1": { + "value": { + "error": "product without variants" + } + } + } + } + } + } + }, + "operationId": "get-api-v2-products-id-rate", + "description": "Recebe uma avaliação e recalcula a pontuação atual", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "in": "query", + "name": "rate", + "description": "Avaliação" + } + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + } + ] + }, + "/api/v2/products/{product_id}/variants/{variant_id}": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "schema": { + "type": "string" + }, + "name": "variant_id", + "in": "path", + "required": true, + "description": "Código identificador da variante" + } + ], + "patch": { + "summary": "Atualiza uma variante", + "operationId": "patch-api-v2-products-product_id-variants-id", + "responses": { + "204": { + "description": "Variante atualizada" + }, + "404": { + "description": "Variante não existente", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Parâmetros enviados inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Variante de produto" + ], + "description": "Atualiza as informações de um variante", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "main": { + "type": "boolean" + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + }, + "height": { + "type": "number", + "description": "Altura do produto, em centímetros" + }, + "length": { + "type": "number", + "description": "Comprimento do produito, em centímetros" + }, + "handling_days": { + "type": "integer", + "description": "Dias de manuseio da variante" + }, + "price": { + "type": "number" + }, + "custom_attributes": { + "type": "object", + "description": "Customização da variante" + }, + "min_quantity": { + "type": "integer" + }, + "norder": { + "type": "integer" + }, + "property1": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "defining": { + "type": "boolean" + } + } + }, + "property2": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "defining": { + "type": "boolean" + } + } + }, + "property3": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "defining": { + "type": "boolean" + } + } + }, + "barcode": { + "type": "string" + }, + "quantity_sold": { + "type": "integer", + "description": "Quantidade de itens vendidos" + } + }, + "required": [ + "sku", + "quantity", + "price" + ] + } + } + } + }, + "deprecated": true + }, + "delete": { + "summary": "Remove uma variante", + "operationId": "delete-api-v2-products-product_id-variants-id", + "responses": { + "204": { + "description": "Quando a variante é removida" + }, + "404": { + "description": "Quando a variante não existe", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Variante de produto" + ], + "description": "Permite remover uma variante" + } + }, + "/api/v2/products/{product_id}/images/{image_id}": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "$ref": "#/components/parameters/image_id" + } + ], + "delete": { + "summary": "Deleta uma imagem do produto", + "tags": [ + "Imagens de produtos e variantes" + ], + "responses": { + "204": { + "description": "Quando a imagem é deletada" + }, + "404": { + "description": "Quando o produto ou a imagem não são encontrados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "operationId": "delete-api-v2-products-product_id-images-id", + "description": "Deleta uma imagem do produto" + } + }, + "/api/v2/products/{product_id}/images/{image_id}/add_variant": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "$ref": "#/components/parameters/image_id" + } + ], + "post": { + "summary": "Associa imagem com variante", + "tags": [ + "Imagens de produtos e variantes" + ], + "responses": { + "200": { + "description": "Quando a imagem é associada com a variante" + }, + "404": { + "description": "Quando o produto ou a imagem não são encontrados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "variant_id": { + "type": "integer" + } + }, + "required": [ + "variant_id" + ] + } + } + } + }, + "operationId": "post-api-v2-products-product_id-images-id-add_variant", + "description": "Associa a imagem com uma variante" + } + }, + "/api/v2/products/{product_id}/images/{image_id}/remove_variant": { + "parameters": [ + { + "$ref": "#/components/parameters/product_id" + }, + { + "$ref": "#/components/parameters/image_id" + } + ], + "post": { + "summary": "Desassocia imagem da variante", + "tags": [ + "Imagens de produtos e variantes" + ], + "responses": { + "200": { + "description": "Quando a imagem é desassociada da variante" + }, + "404": { + "description": "Quando o produto ou a imagem não são encontrados", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "variant_id": { + "type": "integer" + } + }, + "required": [ + "variant_id" + ] + } + } + } + }, + "operationId": "post-api-v2-products-product_id-images-id-remove_variant", + "description": "Remove a associação da imagem com uma variante" + } + }, + "/api/v2/carts/{cart_id}": { + "parameters": [ + { + "$ref": "#/components/parameters/Cart.id" + } + ], + "get": { + "summary": "Retorna um carrinho", + "operationId": "get-api-v2-carts-id", + "tags": [ + "Carrinhos da loja" + ], + "description": "Retorna as informações de um carrinho pelo seu `id` ou `token`", + "responses": { + "200": { + "description": "Carrinho encontrado", + "headers": { + "X-Attempt-Count": { + "schema": { + "type": "integer" + }, + "description": "O número de tentativas de pagamento" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart.v1" + } + } + } + }, + "404": { + "description": "Carrinho não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + } + }, + "patch": { + "summary": "Atualiza um carrinho", + "operationId": "patch-api-v2-carts-id", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart.simple" + } + } + } + }, + "tags": [ + "Carrinhos da loja" + ], + "description": "Permite atualizar os atributos de um carrinho", + "responses": { + "204": { + "description": "Carrinho atualizado com sucesso", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart.v1" + } + } + } + }, + "404": { + "description": "Carrinho não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + }, + "delete": { + "summary": "Exclui um carrinho", + "operationId": "delete-api-v2-carts-id", + "tags": [ + "Carrinhos da loja" + ], + "description": "Permite excluir um carrinho", + "responses": { + "204": { + "description": "Carrinho excluído com sucesso" + }, + "404": { + "description": "Carrinho não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "404": { + "value": { + "error": "not found" + } + } + } + } + } + } + } + } + }, + "/api/v2/carts/{cart_id}/items/{item_id}": { + "parameters": [ + { + "$ref": "#/components/parameters/Cart.id" + }, + { + "schema": { + "type": "string" + }, + "name": "item_id", + "in": "path", + "required": true + } + ], + "patch": { + "summary": "Atualiza um item do carrinho", + "operationId": "patch-api-v2-carts-cart_id-items-id", + "responses": { + "204": { + "description": "Item do carrinho alterado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart.v1" + } + } + } + }, + "404": { + "description": "Item ou o carrinho não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "description": "Atualiza um item do carrinho", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product.v0" + } + } + } + }, + "tags": [ + "Itens do carrinho" + ] + }, + "delete": { + "summary": "Remove um item do carrinho", + "operationId": "delete-api-v2-carts-cart_id-items-id", + "responses": { + "204": { + "description": "Item do carrinho removido" + }, + "404": { + "description": "Item ou o carrinho não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Itens do carrinho" + ], + "description": "Remove um item do carrinho" + } + }, + "/api/v2/carts/{cart_id}/shipping_methods/{value_method}": { + "parameters": [ + { + "$ref": "#/components/parameters/Cart.id" + }, + { + "schema": { + "type": "string" + }, + "name": "value_method", + "in": "path", + "required": true, + "description": "Tipo de envio (`value`)" + } + ], + "patch": { + "summary": "Atualiza o método de envio de um carrinho", + "operationId": "patch-api-v2-carts-cart_id-shipping_methods-value_method", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shipping_methods.v1" + } + } + } + }, + "tags": [ + "Envio do carrinho" + ], + "description": "Atualiza o método para o envio dos itens do carrinho", + "responses": { + "204": { + "description": "Carrinho atualizado com sucesso" + }, + "404": { + "description": "Carrinho não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/carts/{cart_id}/shipping_methods": { + "parameters": [ + { + "$ref": "#/components/parameters/Cart.id" + } + ], + "get": { + "summary": "Cálculo de frete", + "tags": [ + "Envio do carrinho" + ], + "operationId": "get-api-v2-carts-cart_id-shipping_methods", + "description": "Calculo os método de envio disponíveis para o carrinho", + "parameters": [ + { + "$ref": "#/components/parameters/Cart.id" + } + ], + "responses": { + "200": { + "description": "Formas de envio disponíveis retornadas", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "{package_label}": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Shipping_methods.v1" + } + } + } + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/carts/{cart_id}/installments": { + "parameters": [ + { + "$ref": "#/components/parameters/Cart.id" + } + ], + "get": { + "summary": "Calcula as parcelas de pagamento", + "tags": [ + "Pagamento" + ], + "operationId": "get-api-v2-carts-cart_id-installments", + "description": "Calcula as parcelas de pagamento para valor total do carrinho", + "responses": { + "200": { + "description": "Carrinho encontrado", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Cart_installment.v1" + } + } + } + } + }, + "404": { + "description": "Carrinho não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + }, + "examples": { + "Não encontrado": { + "value": { + "error": "not found" + } + } + } + } + } + } + } + } + }, + "/api/v2/orders/{order_code}": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + }, + { + "$ref": "#/components/parameters/include_customizations_in_total" + } + ], + "get": { + "summary": "Retorna um pedido", + "tags": [ + "Pedidos" + ], + "operationId": "get-api-v2-orders-order-code", + "description": "Retorna os dados de um pedido pelo `code` ou `token` do pedido", + "parameters": [ + { + "schema": { + "type": "boolean", + "default": false + }, + "in": "query", + "name": "include_shipping_address", + "description": "Inclui as formas de entrega do pedido" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order.v1" + } + } + } + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + } + }, + "patch": { + "summary": "Atualiza dados extras de um pedido", + "operationId": "patch-api-v2-orders-order_code", + "description": "Atualiza o campo de dados extras de um pedido pelo `code` do pedido", + "tags": [ + "Pedidos" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "extra": { + "type": "object", + "description": "Campo para registro de observações, chave ou valores necessários" + } + } + } + } + } + }, + "responses": { + "204": { + "description": "Dado extra alterado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shipping_address" + } + } + } + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/orders/{order_code}/events": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + } + ], + "get": { + "summary": "Retorna os eventos ocorridos em um pedido", + "tags": [ + "Pedidos" + ], + "operationId": "get-api-v2-orders-order-code-events", + "description": "Retorna a *timeline* de eventos ocorridos em um pedido", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": {} + } + }, + "examples": { + "events": { + "value": [ + { + "occurred_at": "2022-12-26T11:53:12.401-03:00", + "name": "Pedido enviado", + "user": "Jessica", + "ip": "172.29.33.150", + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", + "created_at": "2022-12-26T11:53:12.402-03:00", + "updated_at": "2022-12-26T11:53:12.402-03:00" + }, + { + "occurred_at": "2022-12-26T11:53:28.136-03:00", + "name": "Pedido entregue", + "user": "Jessica", + "ip": "178.29.79.40", + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", + "created_at": "2022-12-26T11:53:28.136-03:00", + "updated_at": "2022-12-26T11:53:28.136-03:00" + } + ] + } + } + } + } + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + } + } + }, + "/api/v2/orders/{order_code}/reviews": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + } + ], + "get": { + "summary": "Retorna a avaliação de um pedido", + "tags": [ + "Pedidos" + ], + "operationId": "get-api-v2-orders-order-code-reviews", + "description": "Retorna a avaliação que o cliente fez em um pedido", + "responses": { + "200": { + "description": "Resenhas retornadas" + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + } + } + }, + "/api/v2/orders/{order_code}/discounts": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + } + ], + "get": { + "summary": "Retorna os descontos de um pedido", + "tags": [ + "Pedidos" + ], + "operationId": "get-api-v2-orders-order-code-discounts", + "description": "Retorna os descontos de um pedido pelo `code` ou `token` do pedido", + "responses": { + "200": { + "description": "Descontos retornados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": {} + } + }, + "example": { + "Desconto": { + "value": [ + { + "name": "Pagamento via slip", + "valid_to": "payment", + "apply_to": "cart", + "type": "%", + "value": "10,", + "package": null, + "sku": null, + "created_at": "2022-12-02T12:00:03.651-03:00", + "updated_at": "2022-12-02T12:00:03.651-03:00" + } + ] + } + } + } + } + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + } + } + }, + "/api/v2/orders/{order_code}/shipping_address": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + } + ], + "get": { + "summary": "Retorna o endereço de envio", + "description": "Retorna o endereço de envio pelo `code` do pedido", + "tags": [ + "Envio de pedido" + ], + "operationId": "get-api-v2-orders-order-code-shipping_address", + "responses": { + "200": { + "description": "Endereço do pedido retornado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shipping_address" + } + } + } + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + } + }, + "patch": { + "summary": "Atualiza endereço do pedido", + "operationId": "patch-api-v2-orders-order-code-shipping-address", + "description": "Atualiza dados de endereço do pedido", + "tags": [ + "Envio de pedido" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shipping_address" + } + } + } + }, + "responses": { + "204": { + "description": "Endereço do pedido alterado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shipping_address" + } + } + } + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/orders/{order_code}/capture": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + } + ], + "post": { + "summary": "Captura um pedido", + "operationId": "post-api-v2-orders-capture", + "description": "Captura o pagamento no adquirente para pedidos com pagamento por cartão de crédito.", + "tags": [ + "Fluxo e andamento" + ], + "responses": { + "200": { + "description": "Captura realizada com sucesso", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + }, + "examples": { + "Pagar.me": { + "value": { + "object": "transaction", + "status": "paid", + "refuse_reason": null, + "status_reason": "acquirer", + "acquirer_response_code": "0000", + "acquirer_name": "pagarme", + "acquirer_id": "5eab10915eab10915eab1091", + "authorization_code": "123456", + "soft_descriptor": "", + "tid": 1234567, + "nsu": 1234567, + "date_created": "2020-05-14T19:14:50.322Z", + "date_updated": "2020-05-15T14:19:34.699Z", + "amount": 1400, + "authorized_amount": 1400, + "paid_amount": 1400, + "refunded_amount": 0, + "installments": 1, + "id": 1234567, + "cost": 120, + "card_holder_name": "John Doe", + "card_last_digits": "6565", + "card_first_digits": "470373", + "card_brand": "visa", + "card_pin_mode": null, + "card_magstripe_fallback": false, + "cvm_pin": false, + "postback_url": "https://demo.vnda.com.br/v2/payments/pagarme/notifications", + "payment_method": "credit_card", + "capture_method": "ecommerce", + "antifraud_score": null, + "boleto_url": null, + "boleto_barcode": null, + "boleto_expiration_date": null, + "referer": "api_key", + "ip": "127.0.0.1", + "subscription_id": null, + "phone": null, + "address": null, + "customer": { + "object": "customer", + "id": 2954669, + "external_id": "example@vnda.com.br", + "type": "individual", + "country": "br", + "document_number": null, + "document_type": "cpf", + "name": "John Doe", + "email": "example@vnda.com.br", + "phone_numbers": [ + "+5511111111111" + ], + "born_at": null, + "birthday": null, + "gender": null, + "date_created": "2020-05-14T19:14:50.248Z", + "documents": [ + { + "object": "document", + "id": "doc_cka75cka75cka75cka75cka75", + "type": "cpf", + "number": 191 + } + ] + }, + "billing": { + "object": "billing", + "id": 1255695, + "name": "John Doe", + "address": { + "object": "address", + "street": "Rua João Neves da Fontoura", + "complementary": null, + "street_number": "1", + "neighborhood": "Azenha", + "city": "Porto Alegre", + "state": "RS", + "zipcode": "90050030", + "country": "br", + "id": 2808888 + } + }, + "shipping": null, + "items": [ + { + "object": "item", + "id": "05.01.4.1.006", + "title": "Aceto Balsâmico Di Modena IGP 500ml Aceto Balsamico Di Modena IGP 500ml", + "unit_price": 1400, + "quantity": 1, + "category": null, + "tangible": true, + "venue": null, + "date": null + } + ], + "card": { + "object": "card", + "id": "card_cka75cka75cka75cka75cka75", + "date_created": "2020-05-14T19:14:50.307Z", + "date_updated": "2020-05-14T19:14:50.717Z", + "brand": "visa", + "holder_name": "f dc", + "first_digits": "470373", + "last_digits": "6565", + "country": "RUSSIA", + "fingerprint": "cka75cka75cka75cka75cka75", + "valid": true, + "expiration_date": "0423" + }, + "split_rules": null, + "metadata": { + "order": "7A4F490570", + "seller-1": { + "name": "default", + "package": "7A4F490570-01" + } + }, + "antifraud_metadata": {}, + "reference_key": null, + "device": null, + "local_transaction_id": null, + "local_time": null, + "fraud_covered": false, + "fraud_reimbursed": null, + "order_id": null, + "risk_level": "very_low", + "receipt_url": null, + "payment": null, + "addition": null, + "discount": null, + "private_label": null + } + } + } + } + } + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Captura não efetuada junto ao adquirente", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "examples": { + "failure": { + "value": { + "error": "Capture was unsuccessful" + } + } + } + } + } + } + } + } + }, + "/api/v2/orders/{order_code}/confirm": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + } + ], + "post": { + "summary": "Confirma um pedido", + "operationId": "post-api-v2-orders-order-code-confirm", + "description": "Confirma um pedido", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "confirmation_data": { + "type": "string", + "description": "Parâmetro para incluir o retorno [da requisição de captura do pagamento](https://developers.vnda.com.br/reference/post-api-v2-orders-capture).\nEsse parâmetro é **obrigatório** para pedidos com pagamento por cartão de crédito. " + } + } + }, + "examples": { + "Depósito": { + "value": { + "banco": "Banco do Brasil", + "data_credito": "2020-03-26", + "conferido_por": "Nome do usuário do financeiro" + } + }, + "Cartão de crédito via Pagar.me": { + "value": { + "object": "transaction", + "status": "paid", + "refuse_reason": null, + "status_reason": "acquirer", + "acquirer_response_code": "0000", + "acquirer_name": "pagarme", + "acquirer_id": "5eab10915eab10915eab1091", + "authorization_code": "123456", + "soft_descriptor": "", + "tid": 1234567, + "nsu": 1234567, + "date_created": "2020-05-14T19:14:50.322Z", + "date_updated": "2020-05-15T14:19:34.699Z", + "amount": 1400, + "authorized_amount": 1400, + "paid_amount": 1400, + "refunded_amount": 0, + "installments": 1, + "id": 1234567, + "cost": 120, + "card_holder_name": "John Doe", + "card_last_digits": "6565", + "card_first_digits": "470373", + "card_brand": "visa", + "card_pin_mode": null, + "card_magstripe_fallback": false, + "cvm_pin": false, + "postback_url": "https://demo.vnda.com.br/v2/payments/pagarme/notifications", + "payment_method": "credit_card", + "capture_method": "ecommerce", + "antifraud_score": null, + "boleto_url": null, + "boleto_barcode": null, + "boleto_expiration_date": null, + "referer": "api_key", + "ip": "127.0.0.1", + "subscription_id": null, + "phone": null, + "address": null, + "customer": { + "object": "customer", + "id": 2954669, + "external_id": "example@vnda.com.br", + "type": "individual", + "country": "br", + "document_number": null, + "document_type": "cpf", + "name": "John Doe", + "email": "example@vnda.com.br", + "phone_numbers": [ + "+5511111111111" + ], + "born_at": null, + "birthday": null, + "gender": null, + "date_created": "2020-05-14T19:14:50.248Z", + "documents": [ + { + "object": "document", + "id": "doc_cka75cka75cka75cka75cka75", + "type": "cpf", + "number": 191 + } + ] + }, + "billing": { + "object": "billing", + "id": 1255695, + "name": "John Doe", + "address": { + "object": "address", + "street": "Rua João Neves da Fontoura", + "complementary": null, + "street_number": "1", + "neighborhood": "Azenha", + "city": "Porto Alegre", + "state": "RS", + "zipcode": "90050030", + "country": "br", + "id": 2808888 + } + }, + "shipping": null, + "items": [ + { + "object": "item", + "id": "05.01.4.1.006", + "title": "Aceto Balsâmico Di Modena IGP 500ml Aceto Balsamico Di Modena IGP 500ml", + "unit_price": 1400, + "quantity": 1, + "category": null, + "tangible": true, + "venue": null, + "date": null + } + ], + "card": { + "object": "card", + "id": "card_cka75cka75cka75cka75cka75", + "date_created": "2020-05-14T19:14:50.307Z", + "date_updated": "2020-05-14T19:14:50.717Z", + "brand": "visa", + "holder_name": "f dc", + "first_digits": "470373", + "last_digits": "6565", + "country": "RUSSIA", + "fingerprint": "cka75cka75cka75cka75cka75", + "valid": true, + "expiration_date": "0423" + }, + "split_rules": null, + "metadata": { + "order": "7A4F490570", + "seller-1": { + "name": "default", + "package": "7A4F490570-01" + } + }, + "antifraud_metadata": {}, + "reference_key": null, + "device": null, + "local_transaction_id": null, + "local_time": null, + "fraud_covered": false, + "fraud_reimbursed": null, + "order_id": null, + "risk_level": "very_low", + "receipt_url": null, + "payment": null, + "addition": null, + "discount": null, + "private_label": null + } + } + } + } + }, + "description": "Confirma um pedido" + }, + "tags": [ + "Fluxo e andamento" + ], + "responses": { + "200": { + "description": "Pedido confirmado" + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/orders/{order_code}/chargeback": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + } + ], + "post": { + "summary": "Estorna pagamento por cartão de crédito", + "operationId": "post-api-v2-orders-order-code-chargeback", + "description": "Faz o estorno do pagamento no adquirente do cartão de crédito\nOperação válida para pedidos pagos com cartão de crédito", + "tags": [ + "Fluxo e andamento" + ], + "responses": { + "200": { + "description": "OK" + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/orders/{order_code}/cancel": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + } + ], + "post": { + "summary": "Cancela um pedido", + "operationId": "post-api-v2-orders-order-code-cancel", + "description": "Altera o status do pedido para `cancelado`", + "tags": [ + "Fluxo e andamento" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cancelation_data": { + "type": "string", + "description": "Parâmetro para incluir uma confirmação de estorno de pagamento para o cliente.\nPara pedidos com pagamento via cartão de crédito, é obrigatório que nesse campo seja incluído no parâmetro o retorno [da requisição de estorno de pagamento](https://developers.vnda.com.br/reference/post-api-v2-orders-order-code-chargeback). " + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Pedido cancelado", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + } + } + }, + "/api/v2/orders/{order_code}/items": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + }, + { + "$ref": "#/components/parameters/include_customizations_in_total" + } + ], + "get": { + "summary": "Lista os itens de um pedido", + "tags": [ + "Itens de pedido" + ], + "operationId": "get-api-v2-orders-items", + "responses": { + "200": { + "description": "Itens retornados", + "content": { + "application/json": { + "schema": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product.order" + } + } + } + } + } + }, + "404": { + "description": "Pedido não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "description": "Retorna os itens de um pedido pelo código do pedido" + } + }, + "/api/v2/orders/{order_code}/items/{item_id}/customizations": { + "parameters": [ + { + "schema": null, + "$ref": "#/components/parameters/Order.code" + }, + { + "schema": { + "type": "string" + }, + "name": "item_id", + "in": "path", + "description": "Código identificador do item", + "required": true + } + ], + "get": { + "summary": "Retorna personalizações de um item", + "operationId": "get-api-v2-orders-order_id-items-item_id-customizations", + "tags": [ + "Itens de pedido" + ], + "description": "Lista as personalizações de um item do pedido pelos códigos do item e do pedido", + "responses": { + "200": { + "description": "Personalizações listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order_item_customization.v1" + } + }, + "examples": { + "Com pesonalizações": { + "value": [ + { + "id": 1, + "number": 1, + "group_name": "Color", + "sku": "A1", + "name": "Red", + "price": 0, + "intl_price": 0, + "handling_days": 0 + } + ] + }, + "Sem personalizações": { + "value": [] + } + } + } + } + }, + "404": { + "description": "Quando o item não é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + } + } + }, + "/api/v2/clients/{id_client}/orders": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "id_client", + "in": "path", + "required": true, + "description": "Código identificador do cliente" + } + ], + "get": { + "summary": "Lista os pedidos de um cliente", + "tags": [ + "Clientes" + ], + "operationId": "get-api-v2-clients-id-orders", + "responses": { + "200": { + "description": "Pedidos retornados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order.v1" + } + } + } + } + }, + "404": { + "description": "Cliente não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "description": "Retorna os pedidos de um cliente pelo seu `id_client`" + } + }, + "/api/v2/orders/{order_code}/packages/{package_code}/invoices": { + "parameters": [ + { + "$ref": "#/components/parameters/Order.code" + }, + { + "$ref": "#/components/parameters/Package.code" + } + ], + "get": { + "summary": "Retorna notas fiscais de um pacote", + "tags": [ + "Notas fiscais de pedidos" + ], + "responses": { + "200": { + "description": "Notas fiscais listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Invoice.v1" + } + } + } + } + }, + "404": { + "description": "Pedido ou pacote não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "$ref": "#/components/responses/422" + } + }, + "operationId": "get-api-v2-orders-order-code-packages-package_code-invoices", + "description": "Retorna as notas fisicais de um pacote do pedido" + }, + "post": { + "summary": "Inclui nota fiscal em um pedido", + "operationId": "post-api-v2-orders-order-code-packages-package_code-invoices", + "responses": { + "201": { + "description": "Nota fiscal adicionada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice.v1" + } + } + } + }, + "422": { + "description": "Parâmetros enviados inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Notas fiscais de pedidos" + ], + "description": "Inclui nota fiscal no pacote de um pedido", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice.v1" + } + } + } + } + } + }, + "/api/v2/orders/{order_code}/packages/{package_code}/invoices/{number}": { + "parameters": [ + { + "schema": null, + "$ref": "#/components/parameters/Order.code" + }, + { + "schema": null, + "$ref": "#/components/parameters/Package.code" + }, + { + "schema": { + "type": "string" + }, + "name": "number", + "in": "path", + "required": true, + "description": "Número da nota fiscal" + } + ], + "patch": { + "summary": "Atualiza uma nota fiscal", + "operationId": "patch-api-v2-orders-order-code-packages-package_code-invoices-number", + "responses": { + "204": { + "description": "Nota fiscal atualizada" + }, + "404": { + "description": "Pedido ou nota fiscal não encontrada", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + }, + "422": { + "description": "Parâmetros enviados inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + } + } + }, + "tags": [ + "Notas fiscais de pedidos" + ], + "description": "Atualiza uma nota fiscal", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice.v1" + } + } + } + } + }, + "delete": { + "summary": "Remove uma nota fiscal", + "operationId": "delete-api-v2-orders-order-code-packages-package_code-invoices-number", + "responses": { + "204": { + "description": "Nota fiscal removida" + }, + "404": { + "description": "Nota fiscal não encontrada" + } + }, + "tags": [ + "Notas fiscais de pedidos" + ], + "description": "Remove uma nota fiscal" + } + }, + "/api/feed/orders": { + "parameters": [], + "get": { + "summary": "Lista os pedidos do feed", + "tags": [ + "Order Feed" + ], + "operationId": "get-feed-orders", + "description": "Permite listar os pedidos pendentes do feed", + "parameters": [ + { + "schema": { + "type": "boolean", + "enum": [ + true + ] + }, + "in": "query", + "name": "include_shipping_address", + "allowEmptyValue": true, + "description": "Selecione `true` para incluir o endereço na resposta" + }, + { + "schema": { + "type": "string", + "enum": [ + "received", + "confirmed", + "canceled" + ] + }, + "in": "query", + "name": "status", + "description": "Filtra os pedidos por status" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Orders" + }, + "404": { + "description": "Domínio de loja não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + } + }, + "post": { + "summary": "Marca os pedidos do feed", + "operationId": "post-api-feed-orders", + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Domínio de loja não encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404.v1" + } + } + } + } + }, + "tags": [ + "Order Feed" + ], + "description": "Permite marcar os pedidos para que eles sejam filtrados da listagem do feed", + "requestBody": { + "$ref": "#/components/requestBodies/Orders" + } + } + } + }, + "components": { + "schemas": { + "Banner": { + "title": "Banner", + "type": "object", + "description": "Modelo que representa um banner na API", + "properties": { + "big_thumb": { + "type": "string" + }, + "color": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "end_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "external": { + "type": "boolean" + }, + "file_name": { + "type": "string" + }, + "file_uid": { + "type": "string" + }, + "html_description": { + "type": "string", + "nullable": true + }, + "id": { + "type": "integer" + }, + "norder": { + "type": "integer", + "nullable": true + }, + "plain_description": { + "type": "string", + "nullable": true + }, + "small_thumb": { + "type": "string" + }, + "start_at": { + "type": "string", + "format": "date-time" + }, + "subtitle": { + "type": "string", + "nullable": true + }, + "tag": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "nullable": true, + "format": "uri" + } + }, + "required": [ + "big_thumb", + "color", + "description", + "end_at", + "external", + "file_name", + "file_uid", + "html_description", + "id", + "norder", + "plain_description", + "small_thumb", + "start_at", + "subtitle", + "tag", + "title", + "updated_at", + "url" + ] + }, + "SlimBanner": { + "title": "SlimBanner", + "type": "object", + "description": "Modelo que representa um banner simplificado na API", + "properties": { + "id": { + "type": "integer" + }, + "tag": { + "type": "string" + }, + "title": { + "type": "string" + }, + "subtitle": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "nullable": true + }, + "external": { + "type": "boolean" + }, + "start_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "end_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "file_url": { + "type": "string", + "nullable": true + }, + "norder": { + "type": "integer", + "nullable": true + }, + "color": { + "type": "string", + "nullable": true + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "tag", + "title", + "subtitle", + "description", + "url", + "external", + "start_at", + "end_at", + "file_url", + "norder", + "color", + "updated_at" + ] + }, + "Variant": { + "title": "Variant", + "type": "object", + "description": "Modelo que representa uma variante na API", + "properties": { + "id": { + "type": "integer", + "description": "Código identificador da variante" + }, + "main": { + "type": "boolean", + "description": "Identifica se é a variante principal do produto. Para `true` a variante é principal e `false` a variante é secundária" + }, + "available": { + "type": "boolean", + "description": "Identifica se a variante está ativa em `true` e desativa em `false`" + }, + "sku": { + "type": "string", + "description": "Código SKU da variante" + }, + "name": { + "type": "string", + "description": "Nome da variante" + }, + "slug": { + "type": "string", + "description": "Slug da URL da variante" + }, + "min_quantity": { + "type": "integer", + "description": "Quantidade mínima para venda" + }, + "quantity": { + "type": "integer", + "description": "Quantidade física" + }, + "quantity_sold": { + "type": "integer", + "description": "" + }, + "stock": { + "type": "integer", + "description": "Quantidade disponível" + }, + "custom_attributes": { + "type": "object", + "description": "Customização da variante" + }, + "properties": { + "type": "object", + "properties": { + "property1": { + "$ref": "#/components/schemas/Variant_property.v1" + }, + "property2": { + "$ref": "#/components/schemas/Variant_property.v1" + }, + "property3": { + "$ref": "#/components/schemas/Variant_property.v1" + } + }, + "description": "[Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização da variante" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "installments": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Relação das parcelas para pagamento do item parcelado" + }, + "available_quantity": { + "type": "integer", + "description": "Unidades reservadas e não reservadas do item" + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + }, + "height": { + "type": "number", + "description": "Altura do produto, em centímetros" + }, + "length": { + "type": "number", + "description": "Comprimento do produito, em centímetros" + }, + "handling_days": { + "type": "integer", + "description": "Dias de manuseio da variante" + }, + "inventories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Variant_inventory.v1" + }, + "description": "Relação de itens por estoque (armazém)" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "intl_price": { + "type": "number", + "description": "Preço internacional" + }, + "image_url": { + "type": "string", + "description": "URL da imagem da variante" + }, + "product_id": { + "type": "integer", + "description": "Código identificador `ID` do produto" + }, + "barcode": { + "type": "string", + "nullable": true, + "description": "Código de barra da variante" + }, + "norder": { + "type": "integer" + } + }, + "x-examples": { + "example-1": { + "id": 27, + "main": false, + "available": true, + "sku": "13001", + "name": "Tamanho: PP | Cor: Branca", + "slug": "camiseta", + "min_quantity": 1, + "quantity": 85, + "stock": 83, + "custom_attributes": { + "size": "PP", + "color": "#FFFFFF" + }, + "properties": {}, + "updated_at": "2019-08-01T18:36:52.718-03:00", + "price": 169.9, + "installments": [ + 169.9 + ], + "available_quantity": 83, + "weight": 0.1, + "width": 11, + "height": 2, + "length": 16, + "handling_days": 0, + "inventories": [], + "sale_price": 169.9, + "intl_price": 33.98, + "image_url": "//b0.vnda.com.br/x120/shop/2014/07/08/camiseta.jpg", + "product_id": 6, + "barcode": null, + "norder": 1 + } + } + }, + "ProductImage": { + "title": "ProductImage", + "type": "object", + "description": "Modelo que representa uma imagem de um produto", + "properties": { + "id": { + "type": "integer", + "description": "Código identificador `ID` da imagem" + }, + "url": { + "type": "string", + "description": "URL da imagem" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização da imagem do produto" + }, + "variant_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Códigos das variantes que utilizam a imagem" + } + }, + "required": [ + "id", + "url", + "updated_at", + "variant_ids" + ] + }, + "ProductSearch": { + "title": "ProductSearch", + "type": "object", + "description": "Modelo que representa um produto retornado via busca no Elasticsearch", + "properties": { + "id": { + "type": "integer" + }, + "active": { + "type": "boolean" + }, + "available": { + "type": "boolean" + }, + "subscription": { + "type": "boolean" + }, + "slug": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "reference_lowercase": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "image_url": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "title", + "subtitle", + "description", + "importance", + "type", + "image_url" + ], + "properties": { + "name": { + "type": "string", + "pattern": "[a-z0-9\\-_]+" + }, + "title": { + "type": "string" + }, + "subtitle": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "importance": { + "type": "number", + "nullable": true + }, + "type": { + "type": "string" + }, + "image_url": { + "type": "string", + "nullable": true + } + } + } + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "on_sale": { + "type": "boolean" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "intl_price": { + "type": "number" + }, + "discount_id": { + "type": "integer" + }, + "discount_rule": { + "type": "object", + "nullable": true, + "required": [ + "type", + "amount" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fixed", + "percentage" + ] + }, + "amount": { + "type": "number" + } + } + }, + "discount": { + "type": "object", + "nullable": true, + "required": [ + "name", + "description", + "facebook", + "valid_to" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "facebook": { + "type": "boolean", + "description": "Em desuso", + "default": false + }, + "valid_to": { + "type": "string" + } + } + }, + "images": { + "type": "array", + "items": { + "type": "object", + "required": [ + "sku", + "url" + ], + "properties": { + "sku": { + "type": "string" + }, + "url": { + "nullable": true, + "type": "string" + } + } + } + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VariantProductSearch" + } + }, + "installments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductInstallment" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização do produto" + } + }, + "required": [ + "id", + "active", + "available", + "subscription", + "slug", + "reference", + "reference_lowercase", + "name", + "description", + "image_url", + "url", + "tags", + "price", + "on_sale", + "sale_price", + "intl_price", + "discount_id", + "discount_rule", + "discount", + "images", + "variants", + "installments", + "created_at", + "updated_at" + ] + }, + "VariantProductSearch": { + "title": "VariantProductSearch", + "type": "object", + "description": "Modelo que representa uma variante retornada via busca no Elasticsearch", + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "sku": { + "type": "string", + "minLength": 1 + }, + "sku_lowercase": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "nullable": true + }, + "full_name": { + "type": "string", + "minLength": 1 + }, + "main": { + "type": "boolean" + }, + "available": { + "type": "boolean" + }, + "image_url": { + "type": "string", + "nullable": true + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "intl_price": { + "type": "number" + }, + "installments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductInstallment" + } + }, + "stock": { + "type": "integer", + "description": "Quantidade de itens disponíveis" + }, + "quantity": { + "type": "integer" + }, + "quantity_sold": { + "type": "integer", + "description": "Quantidade de itens vendidos" + }, + "min_quantity": { + "type": "integer", + "description": "Quantidade mínima para venda" + }, + "available_quantity": { + "type": "integer" + }, + "custom_attributes": { + "type": "object", + "nullable": true, + "description": "Customização da variante" + }, + "properties": { + "type": "object", + "properties": { + "property1": { + "$ref": "#/components/schemas/VariantPropertyProductSearch" + }, + "property2": { + "$ref": "#/components/schemas/VariantPropertyProductSearch" + }, + "property3": { + "$ref": "#/components/schemas/VariantPropertyProductSearch" + } + }, + "description": "[Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante" + }, + "inventories": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "slug", + "available", + "price", + "sale_price", + "quantity", + "quantity_sold", + "place" + ], + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "slug": { + "type": "string", + "minLength": 1 + }, + "available": { + "type": "boolean" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "quantity": { + "type": "number", + "nullable": true + }, + "quantity_sold": { + "type": "number", + "description": "Quantidade de itens vendidos" + }, + "place": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "handling_days": { + "type": "integer", + "description": "Dias de manuseio da variante" + }, + "barcode": { + "type": "string", + "nullable": true + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + }, + "height": { + "type": "number", + "description": "Altura do produto, em centímetros" + }, + "length": { + "type": "number", + "description": "Comprimento do produito, em centímetros" + } + }, + "required": [ + "id", + "sku", + "sku_lowercase", + "name", + "full_name", + "main", + "available", + "image_url", + "price", + "sale_price", + "intl_price", + "installments", + "stock", + "quantity", + "quantity_sold", + "min_quantity", + "available_quantity", + "custom_attributes", + "properties", + "inventories", + "handling_days", + "barcode", + "weight", + "width", + "height", + "length" + ] + }, + "VariantPropertyProductSearch": { + "title": "VariantPropertyProductSearch", + "type": "object", + "description": "Modelo que representa uma propriedade de uma variante quando retornada via Elasticsearch", + "nullable": true, + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "defining": { + "type": "boolean" + } + }, + "required": [ + "name", + "value", + "defining" + ] + }, + "Cart": { + "type": "object", + "title": "Cart", + "description": "Modelo que representa um carrinho na API", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" + }, + "email": { + "type": "string", + "nullable": true + }, + "shipping_method": { + "type": "string", + "nullable": true + }, + "items_count": { + "type": "integer" + }, + "quotation_responses_count": { + "type": "integer" + }, + "payment_responses_count": { + "type": "integer" + }, + "has_payment_responses": { + "type": "boolean" + }, + "has_phone": { + "type": "boolean" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "email", + "shipping_method", + "items_count", + "quotation_responses_count", + "payment_responses_count", + "has_payment_responses", + "has_phone", + "updated_at" + ] + }, + "CartItemCustomization": { + "type": "object", + "title": "CartItemCustomization", + "description": "Modelo que representa uma personalização de item do carrinho na API", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "description": "Código identificador do produto" + }, + "group_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "price": { + "type": "number", + "description": "Preço unitário" + }, + "intl_price": { + "type": "number", + "description": "Preço internacional" + }, + "handling_days": { + "type": "integer", + "description": "Número de dias para manuseio" + }, + "sku": { + "type": "string", + "nullable": true, + "description": "Código SKU do produto" + } + }, + "required": [ + "id", + "group_name", + "name", + "number", + "price", + "intl_price", + "handling_days", + "sku" + ] + }, + "ProductInstallment": { + "title": "ProductInstallment", + "type": "object", + "properties": { + "number": { + "type": "integer" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "interest": { + "type": "boolean" + }, + "interest_rate": { + "type": "number" + }, + "total": { + "type": "number" + } + }, + "required": [ + "number", + "price", + "interest", + "interest_rate", + "total" + ], + "description": "Modelo que representa uma parcela", + "x-internal": false + }, + "VariantProperty": { + "title": "VariantProperty", + "type": "object", + "description": "Modelo que representa uma propriedade de uma variante", + "properties": { + "name": { + "type": "string", + "description": "Nome da propriedade" + }, + "value": { + "type": "string", + "description": "Valor da propriedade" + }, + "defining": { + "type": "boolean", + "description": "Indica se a variante possui uma definição (`true`) ou se a variante não possui (`false`)" + } + }, + "required": [ + "name", + "value", + "defining" + ], + "example": { + "example-property1": { + "name": "Tamanho", + "value": "G", + "defining": true + }, + "example-property2": { + "name": "Cor", + "value": "Amarelo", + "defining": true + } + } + }, + "ProductPriceVariant": { + "title": "ProductPriceVariant", + "type": "object", + "description": "Modelo que representa os preços de uma variante", + "properties": { + "main": { + "type": "boolean", + "description": "Define se a variante do produto é a principal" + }, + "sku": { + "type": "string", + "description": "Código SKU da variante" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "on_sale": { + "type": "boolean" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "intl_price": { + "type": "number" + }, + "available": { + "type": "boolean" + }, + "properties": { + "type": "object", + "properties": { + "property1": { + "$ref": "#/components/schemas/VariantProperty" + }, + "property2": { + "$ref": "#/components/schemas/VariantProperty" + }, + "property3": { + "$ref": "#/components/schemas/VariantProperty" + } + }, + "description": "[Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante" + }, + "stock": { + "type": "number", + "description": "Quantidade de itens disponíveis" + }, + "installments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductInstallment" + } + } + }, + "required": [ + "main", + "sku", + "price", + "on_sale", + "sale_price", + "intl_price", + "available", + "properties", + "stock", + "installments" + ], + "x-internal": false + }, + "Mapping": { + "type": "object", + "description": "Modelo que representa um mapeamento na API", + "properties": { + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "from": { + "type": "array", + "items": { + "type": "string" + } + }, + "to": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "key" + ] + }, + "Package": { + "title": "Package", + "type": "object", + "description": "Modelo que representa um pacote na API", + "properties": { + "actual_shipping_method": { + "type": "string", + "nullable": true + }, + "code": { + "type": "string", + "minLength": 1, + "description": "Código identificador do pacote" + }, + "delivered_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Data de entrega do pacote" + }, + "delivered_email_sent_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "delivery_days": { + "type": "integer", + "minimum": 0, + "description": "Número de dias para entrega" + }, + "delivery_type": { + "type": "string", + "minLength": 1, + "description": "Tipo de envio do pacote" + }, + "delivery_work_days": { + "type": "integer", + "minimum": 0, + "description": "Quantidade de dias úteis para entrega" + }, + "fulfillment_company": { + "type": "string", + "nullable": true, + "description": "Transportadora" + }, + "fulfillment_status": { + "type": "string", + "minLength": 1, + "enum": [ + "waiting", + "shipped", + "delivered" + ], + "description": "Status de envio" + }, + "integrated": { + "type": "boolean", + "default": false + }, + "invoiced": { + "type": "boolean", + "default": false + }, + "label": { + "type": "string", + "minLength": 1 + }, + "properties": { + "type": "object" + }, + "quoted_shipping_price": { + "type": "number", + "minimum": 0 + }, + "shipped_at": { + "type": "string", + "format": "date-time" + }, + "shipped_email_sent_at": { + "type": "string", + "format": "date-time" + }, + "shipping_label": { + "type": "string" + }, + "shipping_name": { + "type": "string" + }, + "shipping_price": { + "type": "number" + }, + "total": { + "type": "number", + "minimum": 0 + }, + "tracked_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Data e horário da última atualização do código de rastreio do pacote" + }, + "tracking_code": { + "type": "string", + "nullable": true, + "description": "Código de rastreio do pacote" + } + }, + "required": [ + "actual_shipping_method", + "code", + "delivered_at", + "delivered_email_sent_at", + "delivery_days", + "delivery_type", + "delivery_work_days", + "fulfillment_company", + "fulfillment_status", + "integrated", + "invoiced", + "label", + "properties", + "quoted_shipping_price", + "shipped_at", + "shipped_email_sent_at", + "shipping_label", + "shipping_name", + "shipping_price", + "total", + "tracked_at", + "tracking_code" + ] + }, + "404.v1": { + "title": "404", + "type": "object", + "x-examples": { + "Not found": { + "error": "not found" + } + }, + "description": "Modelo que representa uma mensagem de erro 404", + "properties": { + "error": { + "type": "string", + "enum": [ + "not found" + ] + } + }, + "required": [ + "error" + ] + }, + "422.v1": { + "title": "422", + "type": "object", + "properties": { + "errors": { + "type": "object", + "properties": { + "{field}": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "description": "Modelo que representa um erro de validação na criação e atualização de registros" + }, + "Address.v1": { + "title": "Address", + "type": "object", + "description": "Modelo que representa um endereço na API", + "properties": { + "id": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "company_name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "documents": { + "type": "object", + "properties": { + "cpf": { + "type": "string" + }, + "cnpj": { + "type": "string" + } + } + }, + "street_name": { + "type": "string" + }, + "street_number": { + "type": "string" + }, + "complement": { + "type": "string" + }, + "neighborhood": { + "type": "string" + }, + "first_phone_area": { + "type": "string" + }, + "first_phone": { + "type": "string" + }, + "second_phone_area": { + "type": "string" + }, + "second_phone": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "recipient_name": { + "type": "string" + } + } + }, + "Audience_member.v1": { + "title": "Audience Member", + "type": "object", + "description": "Modelo que representa um membro do público", + "properties": { + "id": { + "type": "integer" + }, + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "format": "email" + }, + "phone_area": { + "type": "string", + "nullable": true + }, + "phone": { + "type": "string", + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "email" + ] + }, + "Bonus.v1": { + "title": "Bonus", + "type": "object", + "description": "Modelo que representa um bônus na API", + "properties": { + "amount": { + "type": "number" + }, + "token": { + "type": "string" + }, + "valid_from": { + "type": "string", + "format": "date-time" + }, + "valid_thru": { + "type": "string", + "format": "date-time" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "Cart.v1": { + "title": "Cart", + "type": "object", + "description": "Modelo que representa um carrinho na API", + "properties": { + "agent": { + "type": "string", + "nullable": true, + "description": "Agente que criou o carrinho" + }, + "billing_address_id": { + "type": "integer", + "nullable": true, + "description": "Código identificador `ID` do endereço de cobrança do carrinho" + }, + "channel": { + "type": "string", + "nullable": true, + "description": "Canal de venda que originou o carrinho" + }, + "client_id": { + "type": "integer", + "nullable": true, + "description": "Código identificador `ID` do cliente" + }, + "code": { + "type": "string", + "description": "Código identificador `ID` do carrinho" + }, + "coupon_code": { + "type": "string", + "nullable": true, + "description": "Código de cupom de desconto utilizado no carrinho" + }, + "discount": { + "type": "object", + "nullable": true, + "required": [ + "id", + "name", + "description", + "facebook", + "valid_to", + "seal_uid", + "seal_url", + "start_at", + "end_at", + "email", + "cpf", + "tags" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "facebook": { + "type": "boolean", + "default": false + }, + "valid_to": { + "type": "string", + "enum": [ + "store", + "cart" + ] + }, + "seal_uid": { + "type": "string", + "description": "DEPRECATED" + }, + "seal_url": { + "type": "string", + "description": "DEPRECATED" + }, + "start_at": { + "type": "string", + "format": "date-time" + }, + "end_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "email": { + "type": "string", + "nullable": true + }, + "cpf": { + "type": "string", + "nullable": true + }, + "tags": { + "type": "string", + "nullable": true + } + }, + "$ref": "#/components/schemas/Discount.v1", + "description": "Promoção aplicada no carrinho" + }, + "discount_price": { + "type": "number", + "deprecated": true, + "description": "Valor do desconto" + }, + "extra": { + "type": "object", + "description": "Campo para registro de observações, chave ou valores necessários" + }, + "id": { + "type": "integer", + "description": "Código identificador `ID` do carrinho" + }, + "items": { + "type": "array", + "description": "Itens do carrinho", + "items": { + "$ref": "#/components/schemas/Cart_item.v1" + } + }, + "items_count": { + "type": "integer", + "description": "Unidades do item no carrinho" + }, + "shipping_address_id": { + "type": "integer", + "nullable": true, + "description": "Código identificador `ID` do endereço de entrega do carrinho" + }, + "shipping_method": { + "type": "string", + "nullable": true, + "description": "Método de envio selecionado para o carrinho, como por exemplo: normal, expressa e agendada." + }, + "shipping_methods": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "price": { + "type": "string" + }, + "delivery_days": { + "type": "string" + }, + "delivery_type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "fulfillment_company": { + "type": "string", + "nullable": true + } + }, + "required": [ + "package", + "name", + "label", + "price", + "delivery_days", + "delivery_type", + "description", + "short_description", + "fulfillment_company" + ], + "$ref": "#/components/schemas/Shipping_methods.v1" + }, + "description": "Lista com as entregas disponíveis para os itens do carrinho de acordo com o endereço de envio" + }, + "shipping_price": { + "type": "number", + "nullable": true, + "description": "Preço de envio" + }, + "subtotal": { + "type": "number", + "description": "Valor da soma dos itens do carrinho, sem considerar descontos de cupom, carrinho e frete." + }, + "token": { + "type": "string", + "description": "Token do carrinho" + }, + "total": { + "type": "number", + "description": "Valor final do carrinho" + }, + "total_for_deposit": { + "type": "number", + "description": "Valor total do carrinho para pagamento por depósito" + }, + "total_for_slip": { + "type": "number", + "description": "Valor total do carrinho para pagamento por boleto" + }, + "total_for_pix": { + "type": "number", + "description": "Valor do carrinho para pagamento por PIX" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data da última atualização do carrinho" + }, + "rebate_token": { + "type": "string", + "nullable": true, + "description": "Código identificador `ID` do desconto por bônus" + }, + "rebate_discount": { + "type": "number", + "description": "Desconto por bônus do cliente" + }, + "handling_days": { + "type": "number", + "description": "Número de dias para manuseio dos itens" + }, + "subtotal_discount": { + "type": "number", + "description": "Valor de desconto de promoções aplicadas ao subtotal do carrinho" + }, + "total_discount": { + "type": "number", + "description": "Valor de desconto de promoções aplicadas ao valor total do carrinho" + }, + "installments": { + "$ref": "#/components/schemas/Cart_installment.v1", + "description": "Parcelas para pagamento parcelado" + }, + "user_id": { + "type": "string", + "description": "Código identificador `ID` do cliente" + } + }, + "required": [ + "agent", + "billing_address_id", + "channel", + "client_id", + "code", + "coupon_code", + "discount", + "discount_price", + "extra", + "id", + "items", + "items_count", + "shipping_address_id", + "shipping_method", + "shipping_methods", + "shipping_price", + "subtotal", + "token", + "total", + "total_for_deposit", + "total_for_slip", + "total_for_pix", + "updated_at", + "rebate_token", + "rebate_discount", + "handling_days", + "subtotal_discount", + "total_discount" + ] + }, + "Cart_installment.v1": { + "title": "Cart Installment", + "type": "object", + "description": "Modelo que representa uma parcela do total de um carrinho", + "properties": { + "interest": { + "type": "boolean", + "description": "Identifica se há (`true`) ou não (`false`) juros no parcelamento" + }, + "interest_rate": { + "type": "number", + "description": "Taxa de juros do parcelamento" + }, + "number": { + "type": "integer", + "description": "Número de parcelas" + }, + "price": { + "type": "number", + "description": "Valor de cada parcela" + }, + "total": { + "type": "number", + "description": "Valor total das parcelas" + } + }, + "required": [ + "interest", + "interest_rate", + "number", + "price", + "total" + ], + "x-examples": { + "Primeira parcela": { + "interest": false, + "interest_rate": 0, + "number": 1, + "price": 837, + "total": 837 + }, + "Segunda parcela": { + "interest": false, + "interest_rate": 0, + "number": 2, + "price": 418.5, + "total": 837 + } + } + }, + "Cart_item.v1": { + "title": "Cart Item", + "type": "object", + "description": "Modelo que representa um item no carrinho na API", + "x-tags": [ + "Carrinhos" + ], + "properties": { + "available_quantity": { + "type": "integer", + "description": "Unidades disponíveis do produto" + }, + "delivery_days": { + "type": "integer", + "description": "Número de dias para a entrega" + }, + "extra": { + "type": "object", + "description": "Campo para registro de observações, chave ou valores necessários" + }, + "place_id": { + "type": "integer", + "nullable": true, + "description": "Código identificador do local do produto" + }, + "price": { + "type": "number", + "description": "Preço do produto" + }, + "intl_price": { + "type": "number", + "description": "Preço internacional" + }, + "product_id": { + "type": "integer", + "description": "Código identificador `ID` do produto" + }, + "product_name": { + "type": "string", + "description": "Nome do produto" + }, + "product_reference": { + "type": "string", + "description": "Código de referência do produto" + }, + "product_url": { + "type": "string", + "description": "URL do produto no e-commerce" + }, + "quantity": { + "type": "integer", + "description": "Unidades do produto no carrinho" + }, + "seller": { + "type": "string", + "nullable": true, + "description": "Identificador do seller" + }, + "seller_name": { + "type": "string", + "nullable": true, + "description": "Nome do seller" + }, + "subtotal": { + "type": "number", + "description": "Valor do produto sem descontos e promoções" + }, + "total": { + "type": "number", + "description": "Valor total do produto" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data da última atualização do carrinho" + }, + "variant_attributes": { + "type": "object", + "description": "Atributos da variante" + }, + "variant_min_quantity": { + "type": "integer", + "description": "Quantidade miníma de variantes para compra" + }, + "variant_name": { + "type": "string", + "description": "Nome da variante" + }, + "variant_price": { + "type": "number", + "description": "Preço da variante" + }, + "variant_intl_price": { + "type": "number", + "description": "Preço internacional da variante" + }, + "variant_properties": { + "type": "object", + "$ref": "#/components/schemas/Variant" + }, + "variant_sku": { + "type": "string", + "description": "Código SKU da [Variante](https://developers.vnda.com.br/docs/cat%C3%A1logo-de-produtos#produto-atributo-e-variante)" + }, + "id": { + "type": "string", + "description": "Código identificador do item no carrinho" + }, + "product_type": { + "type": "string", + "description": "Tipo de produto" + }, + "image_url": { + "type": "string", + "nullable": true, + "description": "URL da imagem da variante" + } + }, + "required": [ + "available_quantity", + "delivery_days", + "extra", + "place_id", + "price", + "intl_price", + "product_id", + "product_name", + "product_reference", + "product_url", + "quantity", + "seller", + "seller_name", + "subtotal", + "total", + "updated_at", + "variant_attributes", + "variant_min_quantity", + "variant_name", + "variant_price", + "variant_intl_price", + "variant_properties", + "variant_sku" + ] + }, + "Client.v1": { + "title": "Client", + "type": "object", + "description": "Modelo que representa um cliente na API", + "properties": { + "id": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "gender": { + "type": "string" + }, + "phone_area": { + "type": "string", + "pattern": "[0-9]+" + }, + "phone": { + "type": "string", + "pattern": "[0-9]+" + }, + "document_type": { + "type": "string", + "enum": [ + "CPF", + "CNPJ" + ] + }, + "document_number": { + "type": "string", + "description": "Número de documento cadastrado pelo cliente" + }, + "cpf": { + "type": "string", + "pattern": "[0-9]+" + }, + "cnpj": { + "type": "string", + "pattern": "[0-9]+" + }, + "ie": { + "type": "string" + }, + "tags": { + "type": "string" + }, + "lists": { + "type": "array", + "items": { + "type": "string" + } + }, + "facebook_uid": { + "type": "string" + }, + "liked_facebook_page": { + "type": "boolean" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "birthdate": { + "type": "string", + "format": "date" + }, + "recent_address": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "company_name": { + "type": "string" + }, + "street_name": { + "type": "string" + }, + "street_number": { + "type": "string" + }, + "neighborhood": { + "type": "string" + }, + "complement": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "first_phone_area": { + "type": "string" + }, + "first_phone": { + "type": "string" + }, + "second_phone_area": { + "type": "string" + }, + "second_phone": { + "type": "string" + }, + "email": { + "type": "string" + }, + "documents": { + "type": "object", + "properties": { + "cpf": { + "type": "string" + }, + "cnpj": { + "type": "string" + } + } + } + } + } + }, + "auth_token": { + "type": "string" + }, + "last_confirmed_order_at": { + "type": "string", + "format": "date-time" + }, + "received_orders_count": { + "type": "integer" + }, + "confirmed_orders_count": { + "type": "integer" + }, + "canceled_orders_count": { + "type": "integer" + }, + "renew_password": { + "type": "boolean", + "default": false + } + } + }, + "ClientAddress.v1": { + "title": "Client", + "type": "object", + "description": "Modelo que representa os endereços cadastrados pelo cliente na API", + "properties": { + "id": { + "type": "integer" + }, + "street_name": { + "type": "string" + }, + "street_number": { + "type": "string" + }, + "complement": { + "type": "string" + }, + "neighborhood": { + "type": "string" + }, + "label": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "client_id": { + "type": "integer" + } + } + }, + "Coupon.v1": { + "title": "Coupon", + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "code": { + "type": "string" + }, + "uses_per_code": { + "type": "integer" + }, + "uses_per_user": { + "type": "integer" + }, + "referrer_email": { + "type": "string", + "format": "email" + }, + "user_id": { + "type": "integer" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "orders_count": { + "type": "integer" + } + }, + "description": "Modelo que representa um cupom de desconto" + }, + "Customization.v1": { + "title": "Customization", + "type": "object", + "description": "Modelo que representa uma customização", + "properties": { + "id": { + "type": "integer" + }, + "group_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "image_uid": { + "type": "string" + }, + "image_name": { + "type": "string" + }, + "price": { + "type": "number" + }, + "intl_price": { + "type": "number" + }, + "quantity": { + "type": "integer" + }, + "handling_days": { + "type": "integer" + }, + "tag_id": { + "type": "integer" + }, + "sku": { + "type": "string" + }, + "pattern": { + "type": "string", + "nullable": true + } + } + }, + "Products_attributes.v1": { + "title": "Products Attributes", + "type": "object", + "description": "Modelo que representa um atributo customizado de produto", + "properties": { + "index": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "mandatory": { + "type": "boolean" + }, + "updated_at": { + "type": "string" + } + } + }, + "Discount.v1": { + "title": "Discount", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Código identificador `ID` do desconto" + }, + "name": { + "type": "string", + "description": "Nome do desconto ou promoção" + }, + "description": { + "type": "string", + "description": "Descrição do desconto" + }, + "start_at": { + "type": "string", + "format": "date-time", + "description": "Data de início da regra do desconto" + }, + "end_at": { + "type": "string", + "format": "date-time", + "description": "Data de fim da regra do desconto" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Indica se o desconto está habilitado (`true`) ou desabilitado (`false`)" + }, + "facebook": { + "type": "boolean", + "description": "Em desuso", + "default": false + }, + "valid_to": { + "type": "string", + "description": "Indica a regra da promoção: se o desconto é aplicado na vitrine ou no carrinho da loja " + }, + "email": { + "type": "string", + "format": "email", + "description": "Email do cliente, no caso de promoções direcionadas para clientes específicos" + }, + "cpf": { + "type": "string", + "pattern": "[0-9]{11}", + "description": "Cadastro de Pessoa Física (CPF) do cliente, no caso de promoções direcionadas para clientes específicos" + }, + "tags": { + "type": "string", + "description": "Tag de agrupamento de promoção" + } + }, + "required": [ + "name", + "start_at", + "enabled" + ], + "description": "Modelo que representa uma promoção na API" + }, + "Discount_rule.v1": { + "title": "Discount Rule", + "type": "object", + "description": "Modelo que representa uma regra de desconto na API", + "properties": { + "id": { + "type": "integer" + }, + "amount": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": false, + "exclusiveMaximum": false + }, + "type": { + "type": "string", + "enum": [ + "fixed", + "percentage" + ] + }, + "apply_to": { + "type": "string" + }, + "min_quantity": { + "type": "integer", + "default": 0 + }, + "product": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "reference": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "tag": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "combined_product": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "reference": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "min_subtotal": { + "type": "number" + }, + "shipping_method": { + "type": "string" + }, + "shipping_rule": { + "type": "string", + "enum": [ + "any", + "all" + ] + }, + "regions": { + "type": "array", + "items": { + "type": "string" + } + }, + "agent_tag": { + "type": "string" + }, + "channel": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "amount", + "type", + "apply_to", + "min_quantity", + "min_subtotal" + ] + }, + "Invoice.v1": { + "title": "Invoice", + "type": "object", + "description": "Modelo que representa uma nota fiscal na API", + "properties": { + "number": { + "type": "integer", + "description": "Número da nota fiscal" + }, + "series": { + "type": "integer", + "description": "Número de série da nota fiscal" + }, + "issued_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da criação da nota fiscal" + }, + "key": { + "type": "string", + "description": "Chave da nota fiscal" + }, + "volumes": { + "type": "integer" + } + }, + "required": [ + "number" + ] + }, + "Menu.v1": { + "title": "Menu", + "type": "object", + "description": "Modelo que representa um menu na API", + "properties": { + "id": { + "type": "integer" + }, + "label": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "external": { + "type": "boolean" + }, + "parent_id": { + "type": "integer" + }, + "tag_id": { + "type": "integer" + }, + "tag_name": { + "type": "string" + }, + "page_id": { + "type": "integer" + }, + "page_slug": { + "type": "string" + }, + "items_count": { + "type": "integer" + }, + "updated_at": { + "type": "string", + "format": "date" + }, + "tooltip": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Menu.v1" + } + }, + "image_url": { + "type": "string" + }, + "simple_url": { + "type": "string" + }, + "position": { + "type": "string" + }, + "norder": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "Menu_in_tree.v1": { + "title": "Menu (Tree)", + "type": "object", + "description": "Modelo que representa um menu na API quando retornado pela ação de menu em árvore", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "external": { + "type": "boolean" + }, + "url": { + "type": "string" + }, + "tag_id": { + "type": "integer" + }, + "page_id": { + "type": "integer" + }, + "items_count": { + "type": "integer" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Menu_in_tree.v1" + } + }, + "updated_at": { + "type": "string", + "format": "date" + }, + "tooltip": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "simple_url": { + "type": "string" + }, + "norder": { + "type": "integer" + } + } + }, + "Order.v1": { + "title": "Order", + "type": "object", + "description": "Modelo que representa um pedido na API", + "properties": { + "rebate_discount": { + "type": "number", + "minimum": 0, + "description": "Desconto por bônus do cliente" + }, + "rebate_token": { + "type": "string", + "nullable": true, + "description": "Código identificador `ID` do desconto por bônus" + }, + "user_id": { + "type": "integer", + "description": "Código identificador `ID` do cliente" + }, + "updated_at": { + "type": "string", + "description": "Data da última atualização do pedido" + }, + "tracking_code_list": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Lista com os códigos de rastreio dos pacotes do pedido" + }, + "tracking_code": { + "type": "string", + "description": "Código de rastreio do pacote" + }, + "total": { + "type": "number", + "minimum": 0, + "description": "Valor final do pedido" + }, + "token": { + "type": "string" + }, + "taxes": { + "type": "number", + "minimum": 0 + }, + "subtotal": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "description": "Valor da soma dos itens do pedido, desconsiderando descontos e frete." + }, + "status": { + "type": "string", + "enum": [ + "received", + "confirmed", + "canceled" + ], + "description": "Status do pedido" + }, + "payment_due_date": { + "type": "string", + "format": "date" + }, + "slip_url": { + "type": "string" + }, + "slip_token": { + "type": "string" + }, + "slip_due_date": { + "type": "string", + "format": "date" + }, + "slip": { + "type": "boolean" + }, + "shipping_tracked_at": { + "type": "string", + "format": "date-time" + }, + "shipping_price": { + "type": "number" + }, + "shipping_label": { + "type": "string" + }, + "shipped_at": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "Data e horário de envio do pedido" + }, + "received_at": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "Data e horário de recebimento do pedido" + }, + "payment_tid": { + "type": "string", + "nullable": true + }, + "payment_method": { + "type": "string", + "description": "Método de pagamento do pedido" + }, + "payment_gateway": { + "type": "string", + "nullable": true + }, + "payment_authorization": { + "type": "string", + "nullable": true + }, + "paid_at": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "Data e horário do pagamento do pedido" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "extra": { + "type": "object" + }, + "height": { + "type": "number" + }, + "id": { + "type": "integer" + }, + "length": { + "type": "number" + }, + "original_price": { + "type": "number" + }, + "package": { + "type": "string" + }, + "picture_url": { + "type": "string" + }, + "place_city": { + "type": "string", + "nullable": true + }, + "place_id": { + "type": "integer" + }, + "place_name": { + "type": "string", + "nullable": true + }, + "price": { + "type": "number" + }, + "product_id": { + "type": "integer" + }, + "product_name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "reference": { + "type": "string" + }, + "sku": { + "type": "string" + }, + "total": { + "type": "number", + "minimum": 0 + }, + "variant_id": { + "type": "integer" + }, + "variant_name": { + "type": "string", + "nullable": true + }, + "weight": { + "type": "number" + }, + "width": { + "type": "number" + }, + "barcode": { + "type": "string" + } + }, + "required": [ + "extra", + "product_id", + "product_name", + "quantity", + "reference", + "sku", + "total", + "variant_id", + "variant_name", + "weight", + "width" + ], + "$ref": "#/components/schemas/Product.order" + } + }, + "installments": { + "type": "number", + "minimum": 1, + "description": "Parcelas do pagamento parcelado" + }, + "id": { + "type": "integer", + "description": "Código identificador do pedido" + }, + "extra": { + "type": "object", + "description": "Campo de observações do pedido" + }, + "expected_delivery_date": { + "type": "string", + "format": "date" + }, + "email": { + "type": "string", + "description": "Email do cliente" + }, + "discount_price": { + "type": "number", + "description": "Valor do desconto aplicado no pedido" + }, + "deposit": { + "type": "boolean" + }, + "delivery_type": { + "type": "string" + }, + "delivery_message": { + "type": "string" + }, + "delivery_days": { + "type": "integer", + "description": "Dias para entrega" + }, + "delivered_at": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "Data de entrega do pedido" + }, + "coupon_code": { + "type": "string", + "nullable": true, + "description": "Código de cupom do pedido" + }, + "confirmed_at": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "Data e horário de confirmação do pedido" + }, + "code": { + "type": "string", + "description": "Código do pedido" + }, + "client_id": { + "type": "integer", + "description": "Código identificador (`ID`) do cliente" + }, + "channel": { + "type": "string", + "enum": [ + "ecommerce", + "direct" + ], + "description": "Canal de venda que originou o pedido" + }, + "cart_id": { + "type": "integer", + "description": "Código identificador do carrinho que originou o pedido" + }, + "card_validity": { + "type": "string", + "nullable": true, + "description": "Data de validade do cartão de crédito" + }, + "card_number": { + "type": "string", + "pattern": "^[*]{10,12}[0-9]{4}$", + "description": "Número do cartão de crédito" + }, + "card": { + "type": "boolean", + "description": "Retorna `true` se o método de pagamento do pedido é por cartão de crédito." + }, + "canceled_at": { + "type": "string", + "nullable": true, + "format": "date-time", + "description": "Data e horário do cancelamento do pedido" + }, + "browser_ip": { + "type": "string", + "format": "ipv4", + "description": "Endereço IP de origem do pedido" + }, + "agent": { + "type": "string", + "nullable": true, + "description": "Agente do pedido" + }, + "affiliate_tag": { + "type": "string", + "nullable": true + }, + "pix_qr_code": { + "type": "string", + "nullable": true + }, + "payment_authorization_code": { + "type": "string", + "nullable": true, + "description": "Código de autorização do pagamento" + }, + "bonus_granted": { + "type": "boolean", + "description": "Indica se o pedido gerou bônus" + }, + "has_split": { + "type": "boolean" + }, + "pix": { + "type": "boolean", + "description": "Indica se o pedido foi pago usando o Pix" + }, + "ame_qr_code": { + "type": "string", + "nullable": true + }, + "ame": { + "type": "boolean", + "description": "Indica se o pedido foi pago usando o Ame" + }, + "antifraud_assurance": { + "type": "string", + "nullable": true + } + }, + "required": [ + "rebate_discount", + "updated_at", + "total", + "token", + "taxes", + "subtotal", + "status", + "slip", + "shipped_at", + "received_at", + "payment_method", + "payment_gateway", + "payment_authorization", + "paid_at", + "email", + "discount_price", + "deposit", + "delivered_at", + "coupon_code", + "confirmed_at", + "code", + "client_id", + "channel", + "cart_id", + "card_validity", + "card_number", + "card", + "browser_ip", + "pix", + "ame" + ] + }, + "Order_item_customization.v1": { + "title": "Order Item Customization", + "type": "object", + "description": "Modelo que representa uma personalização de item do pedido na API", + "properties": { + "id": { + "type": "integer", + "description": "Código identificador `ID` da personalização" + }, + "number": { + "type": "integer", + "minimum": 1, + "description": "Número de tipos diferentes de personalizações em produtos do pedido" + }, + "group_name": { + "type": "string", + "description": "Grupo em que se enquadra a personalização" + }, + "name": { + "type": "string", + "description": "Nome do produto" + }, + "price": { + "type": "number", + "description": "Preço do produto" + }, + "intl_price": { + "type": "number", + "description": "Preço internacional" + }, + "handling_days": { + "type": "integer", + "description": "Dias de manuseio do produto" + }, + "sku": { + "type": "string", + "nullable": true, + "description": "Código SKU da variante de produto" + } + }, + "required": [ + "id", + "number", + "group_name", + "name", + "price", + "intl_price", + "handling_days", + "sku" + ] + }, + "Payables.v1": { + "title": "Recebíveis do usuário", + "description": "Valores que o usuário possui a receber", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "status": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "fee": { + "type": "number" + }, + "installment": { + "type": "number" + }, + "credit_date": { + "type": "string", + "format": "date-time" + }, + "order_date": { + "type": "string", + "format": "date-time" + }, + "transaction_id": { + "type": "number" + } + } + }, + "Payment_recipient.v1": { + "title": "Payment Recipient", + "type": "object", + "description": "Modelo que representa um recebedor na API", + "properties": { + "id": { + "type": "integer" + }, + "percentage": { + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "active": { + "type": "boolean", + "default": true + }, + "charge_processing_fee": { + "type": "boolean", + "default": false + }, + "liable": { + "type": "boolean", + "default": false + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tag_name": { + "type": "string" + }, + "place_id": { + "type": "integer" + }, + "recipient_id": { + "type": "integer" + }, + "tag_id": { + "type": "integer" + }, + "user_id": { + "type": "integer" + }, + "include_shipping": { + "type": "boolean", + "default": true, + "description": "Indica se o frete deve ser incluído no split do pagamento" + } + }, + "required": [ + "id", + "percentage", + "recipient_id" + ] + }, + "Place.v1": { + "title": "Place", + "type": "object", + "description": "Modelo que representa um local na API", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "address_line_1": { + "type": "string", + "maxLength": 80 + }, + "address_line_2": { + "type": "string", + "maxLength": 80 + }, + "city": { + "type": "string", + "maxLength": 80 + }, + "neighborhood": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "home_page": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string", + "maxLength": 200 + }, + "email": { + "type": "string" + }, + "first_phone": { + "type": "string" + }, + "second_phone": { + "type": "string" + }, + "mobile_phone": { + "type": "string" + }, + "only_cash": { + "type": "boolean", + "default": false + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "marker_url": { + "type": "string" + }, + "state": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "opening_hours": { + "type": "string" + }, + "warehouse": { + "type": "boolean", + "default": false + }, + "legal_name": { + "type": "string" + }, + "cnpj": { + "type": "string" + } + }, + "required": [ + "name", + "address_line_1", + "city", + "email" + ] + }, + "Product.v1": { + "title": "Product", + "type": "object", + "description": "Modelo que representa um produto na API", + "properties": { + "id": { + "type": "integer", + "description": "Código identificador `ID` do priduto" + }, + "active": { + "type": "boolean", + "description": "Indica se o produto está ativo (`true`) ou invativo (`false`)" + }, + "available": { + "type": "boolean", + "description": "Indica se o produto está disponível (`true`) ou indisponível (`false`)" + }, + "category_tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tag_type": { + "type": "string", + "description": "Tipo de tag" + }, + "name": { + "type": "string", + "description": "Nome da tag" + }, + "title": { + "type": "string", + "description": "Título da tag" + } + } + }, + "example": [ + { + "tag_type": "flag", + "name": "tag-veganos", + "title": "Veg" + }, + { + "tag_type": "flag", + "name": "liquida10", + "title": "10OFF" + } + ] + }, + "description": { + "type": "string", + "description": "Descrição do produto" + }, + "discount_id": { + "type": "integer", + "description": "Código de desconto" + }, + "html_description": { + "type": "string", + "description": "Descrição do produto em HTML" + }, + "image_url": { + "type": "string", + "description": "URL da imagem do produto" + }, + "installments": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Relação das parcelas para pagamento parcelado" + }, + "min_quantity": { + "type": "string", + "description": "Quantidade mínima para venda do produto" + }, + "name": { + "type": "string", + "description": "Nome do produto" + }, + "on_sale": { + "type": "boolean", + "description": "Indica se o produto está em promoção (`true`) ou não (`false`)" + }, + "plain_description": { + "type": "string", + "description": "Descrição simplificada" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "rating": { + "type": "object", + "properties": { + "rating": { + "type": "integer" + }, + "votes": { + "type": "integer" + } + }, + "description": "Média de avaliação do produto" + }, + "reference": { + "type": "string", + "description": "Código de referência do produto" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "slug": { + "type": "string", + "description": "slug do produto" + }, + "tag_names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Lista de tags que o produto é associado" + }, + "updated_at": { + "type": "string", + "description": "Data e horário da última atualização do produto" + }, + "url": { + "type": "string", + "description": "URL do produto" + }, + "variants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "{id}": { + "$ref": "#/components/schemas/Product_variant.v1" + } + } + }, + "description": "Variantes do produto" + }, + "discount_rule": { + "type": "object", + "nullable": true, + "required": [ + "type", + "amount" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fixed", + "percentage" + ] + }, + "amount": { + "type": "number" + } + }, + "description": "Regras de desconto de uma promoção" + }, + "images": { + "type": "array", + "description": "Imagens do produto", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "id do produto" + }, + "url": { + "type": "string", + "description": "Url do produto" + }, + "updated_at": { + "type": "string", + "description": "Data e horário da última atualização do produto" + }, + "variant_ids": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "example": [ + { + "id": 0, + "url": "https://b0.vnda.com.br/product.gif?v=1514479363", + "updated_at": "2017-12-28T14:42:43.000-02:00", + "variant_ids": [ + 0 + ] + } + ] + } + }, + "required": [ + "discount_rule" + ] + }, + "Product_variant.v1": { + "description": "Modelo que representa uma variante na API", + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "available_quantity": { + "type": "integer" + }, + "custom_attributes": { + "type": "object", + "description": "Customização da variante" + }, + "handling_days": { + "type": "integer", + "description": "Dias de manuseio da variante" + }, + "height": { + "type": "number" + }, + "id": { + "type": "integer", + "minimum": 1 + }, + "image_url": { + "type": "string", + "minLength": 1, + "description": "URL da imagem da variante" + }, + "installments": { + "type": "array", + "items": { + "type": "number" + } + }, + "inventories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product_variant_inventory.v1" + } + }, + "length": { + "type": "number" + }, + "main": { + "type": "boolean" + }, + "min_quantity": { + "type": "integer", + "description": "Quantidade mínima para venda" + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Nome da variante" + }, + "norder": { + "type": "integer" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "product_id": { + "type": "integer" + }, + "properties": { + "type": "object", + "properties": { + "property1": { + "$ref": "#/components/schemas/Variant_property.v1" + }, + "property2": { + "$ref": "#/components/schemas/Variant_property.v1" + }, + "property3": { + "$ref": "#/components/schemas/Variant_property.v1" + } + }, + "description": "[Atributos](https://developers.vnda.com.br/docs/atributos-de-produto) da variante" + }, + "quantity": { + "type": "integer" + }, + "quantity_sold": { + "type": "integer", + "description": "Quantidade de itens vendidos" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "sku": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string", + "minLength": 1 + }, + "stock": { + "type": "integer", + "description": "Quantidade de itens disponíveis" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização da variante" + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + } + }, + "required": [ + "available", + "available_quantity", + "custom_attributes", + "handling_days", + "height", + "image_url", + "installments", + "length", + "main", + "min_quantity", + "name", + "norder", + "price", + "product_id", + "properties", + "quantity", + "sale_price", + "sku", + "slug", + "stock", + "updated_at", + "weight", + "width" + ] + }, + "Product_variant_inventory.v1": { + "description": "Modelo que representa um inventory da variante na API", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Código identificador `ID` do inventário" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Nome do inventário" + }, + "place_id": { + "type": "integer", + "description": "Código identificador do local" + }, + "place_name": { + "type": "string", + "nullable": true, + "description": "Nome do local" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "quantity": { + "type": "integer", + "description": "Quantidade de itens no inventário" + }, + "quantity_sold": { + "type": "integer", + "description": "Quantidade de itens vendidos" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "slug": { + "type": "string", + "minLength": 1, + "description": "Slug do inventário" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização da variante no inventário" + }, + "variant_id": { + "type": "integer", + "description": "Código da variante" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Data de criação do inventário" + } + }, + "required": [ + "created_at", + "id", + "place_id", + "price", + "quantity", + "quantity_sold", + "sale_price", + "slug", + "updated_at", + "variant_id" + ], + "example": { + "inventories": { + "id": 524, + "slug": "normandia", + "price": 132, + "sale_price": 0, + "quantity": 500, + "quantity_sold": 0, + "name": null, + "variant_id": 532, + "updated_at": "2022-12-15T10:44:36.456-03:00", + "place_id": 3 + } + } + }, + "Shipping_methods.v1": { + "title": "Shipping Methods", + "type": "object", + "description": "Modelo que representa as formas de entrega na API", + "x-examples": { + "Forma de entrega": { + "name": "Normal", + "value": "pac", + "price": 1.99, + "description": "Prazo de até 7 dias corridos para a entrega do pedido", + "delivery_days": 7, + "value_needed_to_discount": null, + "shipping_method_id": 423, + "notice": null + } + }, + "properties": { + "name": { + "type": "string", + "description": "Nome do tipo de entrega, como por exemplo Normal, Expressa e Agendada" + }, + "value": { + "type": "string", + "description": "Identificador do método de envio" + }, + "price": { + "type": "number", + "description": "Preço de envio" + }, + "description": { + "type": "string", + "description": "Descrição do tipo de envio e prazo" + }, + "delivery_days": { + "type": "integer", + "description": "Número em dias do prazo de envio" + }, + "value_needed_to_discount": { + "type": "number", + "nullable": true, + "description": "Valor restante da compra para que o carrinho fique elegível para frete grátis" + }, + "shipping_method_id": { + "type": "integer", + "description": "Código identificador `ID` do tipo de envio" + }, + "notice": { + "type": "string", + "nullable": true, + "description": "Mensagem ou observação sobre a forma de envio" + }, + "fulfillment_company": { + "type": "string", + "nullable": true, + "description": "Empresa responsável pelo envio" + } + }, + "required": [ + "name", + "value", + "price", + "description", + "delivery_days", + "shipping_method_id", + "fulfillment_company" + ] + }, + "Order_items.v1": { + "title": "Order Items", + "type": "object", + "description": "Modelo que representa a lista de itens do pedido", + "properties": { + "id": { + "type": "integer" + }, + "variant_id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "quantity": { + "type": "integer" + }, + "price": { + "type": "number" + }, + "weight": { + "type": "number" + }, + "width": { + "type": "number" + }, + "height": { + "type": "number" + }, + "length": { + "type": "number" + }, + "extra": { + "type": "object", + "properties": { + "customization": { + "type": "string" + } + } + }, + "picture_url": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "sku": { + "type": "string" + }, + "product_name": { + "type": "string" + }, + "variant_name": { + "type": "string" + }, + "original_price": { + "type": "string" + }, + "place_id": { + "type": "string" + }, + "place_name": { + "type": "number" + }, + "place_city": { + "type": "number" + }, + "total": { + "type": "integer" + }, + "package": { + "type": "number" + }, + "has_customizations": { + "type": "integer" + }, + "barcode": { + "type": "integer" + } + } + }, + "Shop_asset.v1": { + "title": "Shop Asset", + "type": "object", + "description": "Modelo que representa as imagens da loja na API", + "properties": { + "id": { + "type": "integer" + }, + "position": { + "type": "string" + }, + "file_uid": { + "type": "string" + }, + "file_name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "Shop_property.v1": { + "title": "Shop Property", + "type": "object", + "description": "Modelo que representa o atributo dos produtos da loja na API", + "properties": { + "name": { + "type": "string" + }, + "defining": { + "type": "boolean" + } + } + }, + "Shop_properties.v1": { + "title": "Shop Properties", + "type": "object", + "description": "Modelo que representa os atributos dos produtos da loja na API", + "properties": { + "property1": { + "$ref": "#/components/schemas/Shop_property.v1" + }, + "property2": { + "$ref": "#/components/schemas/Shop_property.v1" + }, + "property3": { + "$ref": "#/components/schemas/Shop_property.v1" + } + } + }, + "Site_message.v1": { + "title": "Site Message", + "type": "object", + "description": "Modelo que representa as mensagens do site na API", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "call_to_action": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date" + }, + "updated_at": { + "type": "string", + "format": "date" + } + } + }, + "Tag.v1": { + "title": "Tag", + "type": "object", + "description": "Modelo que representa uma tag na API", + "properties": { + "name": { + "type": "string", + "pattern": "[a-z0-9\\-_]+" + }, + "title": { + "type": "string" + }, + "subtitle": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string" + }, + "products_count": { + "type": "integer" + }, + "image_url": { + "type": "string", + "nullable": true + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "name" + ] + }, + "Template.v1": { + "title": "Template", + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "body": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "path", + "updated_at" + ], + "description": "Modelo que representa um template na API" + }, + "User.v1": { + "title": "User", + "type": "object", + "description": "Modelo que representa um usuário na API", + "properties": { + "id": { + "type": "integer", + "description": "Código identificador do usuário" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email do usuário" + }, + "access_token": { + "type": "string", + "description": "Token de validação de usuário logado (`access_token`)\n \nO `access_token` é gerado quando o usuário loga no Admin" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Nome do usuário" + }, + "admin": { + "type": "boolean", + "description": "Identificador de usuários administradores\n\nEsse atributo retorna `true` para um usuário administrador do ambiente de loja" + }, + "renew_password": { + "type": "boolean", + "description": "Identificador de usuários que atualizaram a senha inicial\n\nEsse atributo retorna `true` para um usuário que já redefiniu sua senha pelo menos uma vez" + }, + "role": { + "type": "integer", + "description": "Código da função do usuário na loja:\n\n - Agente: `0`;\n - Gestor: `1`;\n - Local: `2`;\n - Agente Social Selling: `3`." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags para agrupamento de usuários\nAs tags podem ser são utilizadas para direcionar promoções para determinados usuários, organizar os recebedores em uma divisão de pagamentos, definir regras de comissão" + }, + "external_code": { + "type": "string", + "nullable": true, + "description": "Código externo do Vendedor. Esse campo é destinado para cadastrar um código de vendedor já existente em outro sistema." + }, + "phone_area": { + "type": "string", + "maxLength": 2, + "minLength": 2, + "description": "Código de Discagem Direta a Distância (DDD) do telefone do usuário" + }, + "phone": { + "type": "string", + "maxLength": 9, + "minLength": 8, + "description": "Número de telefone do usuário" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Data de inclusão do usuário no Admin" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data de atualização das informações do usuário" + } + }, + "required": [ + "email" + ] + }, + "Variant_inventory.v1": { + "description": "Model que representa um inventory da variante", + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "place_id": { + "type": "integer" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "quantity": { + "type": "integer" + }, + "quantity_sold": { + "type": "integer", + "description": "Quantidade de itens vendidos" + }, + "sale_price": { + "type": "number", + "description": "Preço promocional" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "place_id", + "price", + "quantity", + "quantity_sold", + "sale_price", + "slug" + ] + }, + "Variant_property.v1": { + "title": "variant_property", + "type": "object", + "properties": { + "defining": { + "type": "boolean", + "description": "Indica se a variante possui uma definição (`true`) ou se a variante não possui (`false`)" + }, + "name": { + "type": "string", + "description": "Nome da propriedade" + }, + "value": { + "type": "string", + "description": "Valor da propriedade" + } + }, + "required": [ + "defining", + "name" + ], + "description": "Modelo que representa uma propriedade customizada na API", + "example": { + "example-property1": { + "name": "Tamanho", + "value": "G", + "defining": true + }, + "example-property2": { + "name": "Cor", + "value": "Amarelo", + "defining": true + } + } + }, + "User.v0": { + "title": "Campos de cadastro de usuário", + "type": "object", + "description": "Modelo que representa o cadastro de usuário", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email do usuário" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Nome do usuário" + }, + "role_name": { + "type": "string", + "enum": [ + "Agente", + "Gestor", + "Local", + "Agente Social Selling" + ], + "description": "Função do usuário na loja" + }, + "password": { + "type": "string", + "description": "Senha do usuário" + }, + "password_confirmation": { + "type": "string", + "description": "Confirmação de senha do usuário" + }, + "external_code": { + "type": "string", + "description": "Código externo do Vendedor. Esse campo é destinado para cadastrar um código de vendedor já existente em outro sistema." + }, + "phone_area": { + "type": "string", + "maxLength": 2, + "minLength": 2, + "description": "Código de Discagem Direta a Distância (DDD) do telefone do usuário" + }, + "phone": { + "type": "string", + "maxLength": 9, + "minLength": 8, + "description": "Número de telefone do usuário" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags para agrupamento de usuários\nCom as tags o lojista pode agrupar usuários para direcionar promoções para determinados usuários, fazer uma divisão de pagamentos, definir regras de comissão, entre outras aplicações com usuários" + } + }, + "required": [ + "email" + ] + }, + "SimpleProduct": { + "title": "SimpleProduct", + "type": "object", + "description": "Modelo simplificado de um produto para atualização e criação", + "properties": { + "reference": { + "type": "string", + "description": "Código de Referência do produto" + }, + "name": { + "type": "string", + "description": "Nome do produto" + }, + "description": { + "type": "string", + "description": "Descrição do produto" + }, + "active": { + "type": "boolean", + "default": true, + "description": "Indica se o produto está ativo (`true`) ou invativo (`false`)" + }, + "tag_list": { + "type": "string", + "example": "tag1, tag2", + "description": "Tags associadas ao produto" + }, + "product_type": { + "type": "string", + "description": "Tipo de produto", + "enum": [ + "product", + "sample", + "subscription" + ], + "default": "product" + } + }, + "required": [ + "reference", + "name" + ] + }, + "Cart.simple": { + "title": "Parâmetros de carrinho resumido", + "description": "Parâmetros criação e atualização de carrinho", + "type": "object", + "properties": { + "agent": { + "type": "string", + "description": "Agente que criou o carrinho" + }, + "zip": { + "type": "string", + "description": "Código de Endereçamento Postal (CEP) do destinatário do pedido" + }, + "client_id": { + "type": "integer", + "minimum": 0, + "description": "Código identificador `ID` do cliente" + }, + "coupon_code": { + "type": "string", + "description": "Código identificador `ID` do desconto do carrinho" + }, + "email": { + "type": "string", + "deprecated": true, + "format": "email", + "description": "Email do cliente" + }, + "rebate_token": { + "type": "string", + "description": "Token do desconto" + }, + "user_id": { + "type": "number", + "description": "Id do agente" + } + } + }, + "Cart.v2": { + "title": "Lista de carrinhos", + "description": "Modelo de lista de carrinhos", + "type": "array", + "items": { + "$ref": "#/components/schemas/Cart.v1" + } + }, + "Product.v0": { + "type": "object", + "title": "Produto", + "description": "Modelo de carcaterística de produto para item no carrinho", + "properties": { + "sku": { + "type": "string", + "description": "Código SKU da variante do produto" + }, + "quantity": { + "type": "integer", + "minimum": 0, + "description": "Unidades do produto disponíveis fisicamente" + }, + "extra": { + "type": "object", + "description": "Campo para registro de observações, chave ou valores necessários" + }, + "place_id": { + "type": "integer", + "description": "Código identificador do local do produto", + "minimum": 0 + }, + "store_coupon_code": { + "type": "string", + "description": "Código de cupom" + }, + "customizations": { + "type": "array", + "description": "[Personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) do produto", + "items": { + "properties": { + "Customization": { + "type": "string", + "description": "[Personalização](http://ajuda.vnda.com.br/pt-BR/articles/1763398-funcionalidades-produtos-personalizados) incluídas no Admin da loja. \nSe por exemplo a customização do produto é a cor, o parâmetro para a requisição deve ser `Color` ao invés de `CUstomization`. " + } + } + }, + "required": [ + "sku", + "quantity" + ] + } + } + }, + "Shipping_address": { + "title": "Endereço de envio", + "description": "Modelo de endereço de envio para carrinho e pedido", + "type": "object", + "properties": { + "first_name": { + "type": "string", + "description": "Nome do cliente" + }, + "last_name": { + "type": "string", + "description": "Sobrenome do cliente" + }, + "company_name": { + "type": "string", + "description": "Nome da empresa (para clientes jurídicos)" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email do cliente" + }, + "first_phone_area": { + "type": "string", + "description": "Código de Discagem Direta à Distância (DDD)" + }, + "first_phone": { + "type": "string", + "description": "Telefone do cliente" + }, + "second_phone_area": { + "type": "string", + "description": "Código de Discagem Direta à Distância (DDD)" + }, + "second_phone": { + "type": "string", + "description": "Telefone do cliente" + }, + "recipient_name": { + "description": "Nome do recebedor" + }, + "street_name": { + "type": "string", + "description": "Logradouro" + }, + "street_number": { + "description": "Número", + "type": "string" + }, + "complement": { + "type": "string", + "description": "Complemento" + }, + "neighborhood": { + "type": "string", + "description": "Bairro" + }, + "reference": { + "type": "string", + "description": "Ponto de referência" + }, + "zip": { + "type": "string", + "description": "Código de Endereçamento Postal (CEP)" + }, + "documents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "cpf": { + "description": "Cadastro de Pessoa Física", + "type": "string" + }, + "rg": { + "description": "Registro Geral", + "type": "string" + }, + "cnpj": { + "description": "Cadastro Nacional de Pessoas Jurídicas", + "type": "string" + }, + "ie": { + "type": "string", + "description": "Inscrição Estadual" + } + } + } + } + }, + "required": [ + "zip" + ] + }, + "Product.order": { + "title": "Produto em um pedido", + "description": "Modelo de produto em um pedido", + "type": "object", + "properties": { + "extra": { + "type": "object", + "description": "Dados extra do produto" + }, + "height": { + "type": "number", + "description": "Altura do produto, em centímetros." + }, + "id": { + "type": "integer", + "description": "código identificador do produto" + }, + "length": { + "type": "number", + "description": "Comprimento do produito, em centímetros." + }, + "original_price": { + "type": "number", + "description": "Preço original" + }, + "package": { + "type": "string", + "description": "Pacote do produto" + }, + "picture_url": { + "type": "string", + "description": "URL da imagem do produto" + }, + "place_city": { + "type": "string", + "nullable": true, + "description": "Cidade que o produto está" + }, + "place_id": { + "type": "integer", + "description": "Código identificador do local do produto" + }, + "place_name": { + "type": "string", + "nullable": true, + "description": "Nome do local do produto" + }, + "price": { + "type": "number", + "description": "Preço do produto" + }, + "product_id": { + "type": "integer" + }, + "product_name": { + "type": "string" + }, + "quantity": { + "type": "integer", + "description": "Unidades do produto" + }, + "reference": { + "type": "string", + "description": "Código de referência do produto" + }, + "sku": { + "type": "string", + "description": "Código SKU da variante do produto" + }, + "total": { + "type": "number", + "minimum": 0, + "description": "Valor total do produto" + }, + "variant_id": { + "type": "integer", + "description": "Código identificador da variante do produto" + }, + "variant_name": { + "type": "string", + "nullable": true, + "description": "Nome da variante do produto" + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + }, + "barcode": { + "type": "string", + "description": "Código de barras do produto" + }, + "has_customizations": { + "type": "boolean", + "description": "Indica se o produto possui customização." + } + }, + "required": [ + "extra", + "product_id", + "product_name", + "quantity", + "reference", + "sku", + "total", + "variant_id", + "variant_name", + "weight", + "width" + ] + } + }, + "securitySchemes": { + "Token": { + "type": "http", + "scheme": "bearer", + "description": "Token que deve ser enviado em todas as requisições. [Este token](http://ajuda.vnda.com.br/pt-BR/articles/1506726-chave-token-de-api) pode ser gerado pelo painel admin e não possui data de expiração. O Token e URLs gerados e utilizados no ambiente de produção serão diferentes dos que foram gerados no [ambiente de testes](http://ajuda.vnda.com.br/pt-BR/articles/3760960-ambiente-de-testes-staging). O valor de Authorization será 'Bearer seu_token'" + }, + "X-Shop-Host": { + "type": "apiKey", + "in": "header", + "name": "X-Shop-Host", + "description": "Domínio da loja como `www.nomedaloja.com.br`" + } + }, + "responses": { + "204": { + "description": "Quando um registro é atualizado", + "content": { + "application/json": { + "schema": { + "nullable": true + } + } + }, + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Id da requisição" + } + } + }, + "404": { + "description": "Quando o registro não é encontrado", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + }, + "examples": { + "Não encontrado": { + "value": { + "error": "not found" + } + } + } + }, + "application/xml": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Id da requisição" + } + } + }, + "422": { + "description": "Quando os parâmetros enviados são inválidos", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/422.v1" + } + } + }, + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Id da requisição" + } + } + }, + "Banners": { + "description": "Resposta que representa uma lista de banners", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Banner" + } + } + } + } + }, + "Banner": { + "description": "Quando um banner é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Banner" + }, + "examples": { + "example-1": { + "value": { + "big_thumb": "//b3.vnda.com.br/200x/demo%2F2019%2F02%2F25%2F19_49_32_754_home1b.jpg", + "color": "", + "description": "[COLEÇAO LA MEDINAi](https://exemplo.com/ovos-mexidos.pdf)", + "end_at": "2019-12-01T08:50:00.000-03:00", + "external": false, + "file_name": "demo%2F2019%2F02%2F25%2F19_49_32_754_home1b.jpg", + "file_uid": "demo%2F2019%2F02%2F25%2F19_49_32_754_home1b.jpg", + "html_description": "

COLEÇAO LA MEDINAi

\n", + "id": 2, + "norder": null, + "plain_description": "COLEÇAO LA MEDINAi (https://exemplo.com/ovos-mexidos.pdf)", + "small_thumb": "//b3.vnda.com.br/26x26/demo%2F2019%2F02%2F25%2F19_49_32_754_home1b.jpg", + "start_at": "2018-12-13T00:00:00.000-02:00", + "subtitle": null, + "tag": "fullbanner", + "title": "Fullbanner 2", + "updated_at": "2021-01-11T17:14:48.999-03:00", + "url": null + } + } + } + } + } + }, + "AllBanners": { + "description": "Quando existem banners dentro do período de validade", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SlimBanner" + } + } + } + } + } + }, + "Carts": { + "description": "Resposta que representa uma lista de carrinhos", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Cart" + }, + "example": [ + { + "updated_at": "2020-07-13T16:35:14.999-03:00", + "has_phone": false, + "shipping_method": null, + "items_count": 1, + "has_payment_responses": false, + "id": 23, + "email": null, + "quotation_responses_count": 0, + "payment_responses_count": 0, + "code": "799594D6EB", + "extra": {}, + "total": 12.1, + "token": "zX89SG241cST3lpzQ2nkkqJXppUJYou1jTgL" + }, + { + "shipping_method": null, + "has_phone": false, + "updated_at": "2020-07-06T22:51:17.171-03:00", + "has_payment_responses": false, + "items_count": 1, + "id": 22, + "quotation_responses_count": 0, + "payment_responses_count": 0, + "email": null, + "code": "FDF4819129", + "extra": {}, + "total": 123.1, + "token": "gt2hvYm96LNzVhcKGbR9BJnQlpaqL4kMVYAs" + } + ] + }, + "examples": { + "example-1": { + "value": [ + { + "updated_at": "2020-07-13T16:35:14.999-03:00", + "has_phone": false, + "shipping_method": null, + "items_count": 1, + "has_payment_responses": false, + "id": 23, + "email": null, + "quotation_responses_count": 0, + "payment_responses_count": 0, + "code": "799594D6EB", + "extra": {}, + "total": 12.1, + "token": "zX89SG241cST3zQ2nkkqJXUJYou1jTgL" + }, + { + "shipping_method": null, + "has_phone": false, + "updated_at": "2020-07-06T22:51:17.171-03:00", + "has_payment_responses": false, + "items_count": 1, + "id": 22, + "quotation_responses_count": 0, + "payment_responses_count": 0, + "email": null, + "code": "FDF4819129", + "extra": {}, + "total": 123.1, + "token": "gt2hvYmLNzVhcKGbR9BJnQaqL4kMVYAs" + } + ] + } + } + } + } + }, + "Channels": { + "description": "Resposta que representa uma lista de channels", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Lista de channels": { + "value": [ + "direct", + "direct_app", + "ecommerce" + ] + } + } + } + }, + "headers": {} + }, + "CartItemCustomizationList": { + "description": "Resposta que representa uma lista de customizações de um item", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CartItemCustomization" + } + }, + "items": { + "$ref": "#/components/schemas/CartItemCustomization" + } + } + } + } + }, + "States": { + "description": "Resposta que representa uma lista de estados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Lista de estados": { + "value": [ + "RS", + "PE", + "MG" + ] + } + } + } + } + }, + "ProductPrice": { + "description": "Resposta que representa os dados de preços de um produto e suas variantes", + "content": { + "application/json": { + "schema": { + "description": "", + "type": "object", + "x-examples": { + "example-1": { + "available": true, + "on_sale": false, + "price": 10, + "sale_price": 10, + "intl_price": 0, + "discount_rule": null, + "installments": [ + { + "number": 1, + "price": 10, + "interest": false, + "interest_rate": 0, + "total": 10 + }, + { + "number": 2, + "price": 5.15, + "interest": true, + "interest_rate": 1.5, + "total": 10.3 + } + ], + "updated_at": "", + "variants": [ + { + "main": true, + "sku": "CSMT-1", + "price": 10, + "on_sale": false, + "sale_price": 10, + "intl_price": 0, + "available": true, + "properties": {}, + "stock": 1, + "installments": [ + { + "number": 1, + "price": 10, + "interest": false, + "interest_rate": 0, + "total": 10 + }, + { + "number": 2, + "price": 5.15, + "interest": true, + "interest_rate": 1.5, + "total": 10.3 + } + ] + }, + { + "main": false, + "sku": "CSMT-2", + "price": 10, + "on_sale": false, + "sale_price": 10, + "intl_price": 0, + "available": true, + "properties": {}, + "stock": 1, + "installments": [ + { + "number": 1, + "price": 10, + "interest": false, + "interest_rate": 0, + "total": 10 + }, + { + "number": 2, + "price": 5.15, + "interest": true, + "interest_rate": 1.5, + "total": 10.3 + } + ] + } + ] + } + }, + "properties": { + "available": { + "type": "boolean" + }, + "on_sale": { + "type": "boolean" + }, + "price": { + "type": "number" + }, + "sale_price": { + "type": "number" + }, + "intl_price": { + "type": "number" + }, + "discount_rule": {}, + "installments": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/components/schemas/ProductInstallment" + } + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização" + }, + "variants": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "$ref": "#/components/schemas/ProductPriceVariant" + } + } + }, + "required": [ + "available", + "on_sale", + "price", + "sale_price", + "intl_price", + "installments", + "updated_at", + "variants" + ] + } + } + }, + "headers": {} + }, + "Mappings": { + "description": "Quando os mapeamentos são listados", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mapping" + } + } + } + } + }, + "Mapping": { + "description": "Quando o mapeamento é encontrado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Mapping" + } + } + } + }, + "MappingCreate": { + "description": "Quando o mapeamento é criado", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Mapping" + } + } + } + }, + "Packages": { + "description": "Resposta que representa uma lista de pacotes", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Package" + } + } + } + } + }, + "ProductImage": { + "description": "Resposta que representa uma imagem de produto", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductImage" + } + } + } + } + }, + "ProductImages": { + "description": "Resposta que representa uma lista de imagens de produto", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductImage" + } + } + } + } + }, + "VariantImages": { + "description": "Quando as imagens são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Data e horário da última atualização da imagem do produto" + } + }, + "required": [ + "url", + "updated_at" + ] + } + }, + "examples": { + "Lista de imagens": { + "value": [ + { + "url": "//b1.vnda.com.br/demo/2020/01/01/001-produto-teste-01-nome-do-produto-123.jpg?v=1234567890", + "updated_at": "2020-01-01T00:00:00.000-03:00" + }, + { + "url": "//b1.vnda.com.br/demo/2020/01/01/002-produto-teste-02-nome-do-produto-456.jpg?v=1357902468", + "updated_at": "2020-01-01T01:01:01.001-03:00" + } + ] + } + } + } + } + }, + "VariantShippings": { + "description": "Quando as formas de entrega são listadas", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "price": { + "type": "number" + }, + "description": { + "type": "string" + }, + "delivery_days": { + "type": "integer" + }, + "value_needed_to_discount": { + "type": "number", + "nullable": true + }, + "shipping_method_id": { + "type": "integer", + "nullable": true + }, + "notice": { + "type": "string", + "nullable": true + }, + "fulfillment_company": { + "type": "string", + "nullable": true + }, + "countries": { + "type": "array", + "nullable": true, + "items": { + "type": "object", + "properties": { + "country": { + "type": "string" + }, + "price": { + "type": "string" + } + } + } + } + }, + "required": [ + "name", + "value", + "price", + "description", + "delivery_days", + "value_needed_to_discount", + "shipping_method_id", + "notice", + "fulfillment_company", + "countries" + ] + } + }, + "examples": { + "Lista de formas de entrega": { + "value": [ + { + "name": "Normal", + "value": "normal", + "price": 1.25, + "description": "Prazo de até 7 dias corridos para a entrega do pedido", + "delivery_days": 7, + "value_needed_to_discount": 2.2, + "shipping_method_id": 1, + "notice": "Notice 1", + "fulfillment_company": "olist_envios", + "countries": [ + { + "country": "ARG", + "price": "1.0" + }, + { + "country": "RUS", + "price": "2.0" + } + ] + }, + { + "name": "Expressa", + "value": "expressa", + "price": 2.25, + "description": "Prazo de 1 dia corrido para a entrega do pedido", + "delivery_days": 1, + "value_needed_to_discount": null, + "shipping_method_id": null, + "fulfillment_company": null, + "notice": null, + "countries": null + } + ] + } + } + } + } + }, + "Orders": { + "description": "Lista de pedidos", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order.v1" + } + } + } + }, + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "ID da requisição" + }, + "X-Pagination": { + "schema": { + "type": "string" + }, + "description": "JSON com dados da paginação" + } + } + } + }, + "parameters": { + "only_valid": { + "name": "only_valid", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "true" + }, + "description": "Booleano indicando para filtrar banners fora do prazo de validade" + }, + "only_scheduled": { + "name": "only_scheduled", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "true" + }, + "description": "Booleano indicando para filtrar banners agendados" + }, + "only_expired": { + "name": "only_expired", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "true" + }, + "description": "Booleano indicando para filtrar banners com prazo de validade expirados" + }, + "tag": { + "name": "tag", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Lista separada por vírgula com nomes de tags" + }, + "title": { + "name": "title", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Texto livre que permite filtrar os banners pelo título" + }, + "no_paginate": { + "name": "no_paginate", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Booleano indicando para não fazer paginação dos resultados" + }, + "page": { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + }, + "description": "Número da página atual. Os dados de paginação estarão disponíveis, em formato JSON, no header X-Pagination no response da API, caso exista paginação" + }, + "per_page": { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 100, + "maximum": 100 + }, + "description": "Número máximo de registros que deve ser retornado por página" + }, + "coupon_codes": { + "name": "coupon_codes", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowEmptyValue": true, + "description": "Array com os códigos de cupons" + }, + "start": { + "name": "start", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Retorna os resultados a partir desta data, no formato 'yyyy-mm-dd'" + }, + "finish": { + "name": "finish", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Retorna os resultados até esta data, no formato 'yyyy-mm-dd'" + }, + "status": { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "example": "confirmed" + } + }, + "sort": { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "example": "updated_at,desc", + "enum": [ + "newest" + ] + }, + "description": "Ordena o resultado da busca de produtos em ordem crescente de cadastro " + }, + "order_code": { + "name": "order_code", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Código do pedido" + }, + "invoiced": { + "name": "invoiced", + "in": "query", + "schema": { + "type": "boolean" + }, + "description": "Se \"true\" retorna somente os pedidos que tenham nota fiscal. Se \"false\" retorna somente os pedidos que não tenham nota fiscal" + }, + "product_id": { + "name": "product_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID do produto" + }, + "cart_id": { + "name": "cart_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "ID do carrinho" + }, + "sku": { + "name": "sku", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "SKU da variante" + }, + "include_customizations_in_total": { + "name": "include_customizations_in_total", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "description": "Se \"true\" inclui o preço dos produtos customizados no total do pedido. Se \"false\" retorna o total do pedido sem a somatória do preço de produtos customizados.", + "required": false, + "allowEmptyValue": true + }, + "limit": { + "schema": { + "type": "integer" + }, + "in": "query", + "name": "limit", + "description": "Limite da quantidade de itens retornados" + }, + "reference": { + "schema": { + "type": "string" + }, + "in": "query", + "name": "reference", + "description": "Código de referência do produto" + }, + "ids": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "query", + "name": "ids", + "description": "Filtra pelo `ID` de produtos" + }, + "updated_after": { + "schema": { + "type": "string" + }, + "in": "query", + "name": "updated_after", + "description": "Filtra produtos alterados depois da data" + }, + "include_inactive": { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "include_inactive", + "description": "Inclui na requisição os produtos inativos" + }, + "include_images": { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "include_images", + "description": "Inclui na requisição se deseja que venham todas as imagens do produto" + }, + "image_id": { + "schema": { + "type": "string" + }, + "name": "image_id", + "in": "path", + "required": true, + "description": "Código identificador `ID` da imagem" + }, + "Cart.id": { + "in": "path", + "schema": { + "type": "string" + }, + "name": "cart_id", + "required": true, + "description": "Código identificador `ID` ou `token` do carrinho" + }, + "Order.code": { + "in": "path", + "schema": { + "type": "string", + "minLength": 10, + "maxLength": 64 + }, + "name": "order_code", + "required": true, + "description": "Código identificador (`code`) ou `token` do pedido" + }, + "Package.code": { + "in": "path", + "schema": { + "type": "string", + "minLength": 1 + }, + "name": "package_code", + "required": true, + "description": "Código identificador do pacote" + } + }, + "requestBodies": { + "Product": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reference": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "active": { + "type": "boolean", + "default": true + }, + "product_type": { + "type": "string", + "enum": [ + "product", + "sample", + "subscription" + ], + "default": "product" + } + }, + "required": [ + "reference", + "name" + ] + } + } + }, + "description": "" + }, + "Variant": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "main": { + "type": "boolean" + }, + "width": { + "type": "number", + "description": "Largura do produto, em centímetros" + }, + "height": { + "type": "number", + "description": "Altura do produto, em centímetros" + }, + "length": { + "type": "number", + "description": "Comprimento do produito, em centímetros" + }, + "weight": { + "type": "number", + "description": "Massa do produto, em gramas" + }, + "handling_days": { + "type": "integer", + "description": "Dias de manuseio da variante" + }, + "price": { + "type": "number", + "description": "Preço do item" + }, + "custom_attributes": { + "type": "object", + "description": "Customização da variante" + }, + "min_quantity": { + "type": "integer" + }, + "norder": { + "type": "integer" + }, + "property1": { + "$ref": "#/components/schemas/VariantProperty" + }, + "property2": { + "$ref": "#/components/schemas/VariantProperty" + }, + "property3": { + "$ref": "#/components/schemas/VariantProperty" + }, + "barcode": { + "type": "string" + }, + "quantity_sold": { + "type": "integer", + "description": "Quantidade de itens vendidos" + } + }, + "required": [ + "sku", + "quantity", + "price" + ] + } + } + } + }, + "Orders": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "orders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Código do pedido" + } + }, + "required": [ + "code" + ] + } + } + } + }, + "examples": { + "example-1": { + "value": { + "orders": [ + { + "code": "A1B2C3D4E5" + }, + { + "code": "5E4D3C2B1A" + } + ] + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "Usuários" + }, + { + "name": "Créditos" + }, + { + "name": "Pedidos" + }, + { + "name": "Pacotes" + }, + { + "name": "Produtos" + }, + { + "name": "Variantes" + }, + { + "name": "Estoque" + }, + { + "name": "Tags" + }, + { + "name": "Templates" + }, + { + "name": "Carrinhos" + }, + { + "name": "Itens do carrinho" + }, + { + "name": "Locais" + }, + { + "name": "Notas fiscais" + }, + { + "name": "Recebedores" + }, + { + "name": "Pagamentos" + }, + { + "name": "Público" + }, + { + "name": "Clientes" + }, + { + "name": "Rastreios" + }, + { + "name": "Promoções" + }, + { + "name": "Regras de desconto" + }, + { + "name": "Cupons de desconto" + }, + { + "name": "Menus" + }, + { + "name": "Mensagens do site" + }, + { + "name": "Loja" + }, + { + "name": "Personalizações" + }, + { + "name": "Mapeamentos" + }, + { + "name": "Mídias" + }, + { + "name": "Eventos" + } + ], + "security": [ + { + "Token": [] + } + ], + "x-readme": { + "explorer-enabled": true, + "proxy-enabled": true, + "samples-enabled": true + }, + "_id": "6387a48156e90b009a6ce710", + "x-stoplight": { + "id": "baml4bcuaujvs" + } +} diff --git a/vnda/utils/paths.ts b/vnda/utils/paths.ts deleted file mode 100644 index 73b6d93a7..000000000 --- a/vnda/utils/paths.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Account } from "../accounts/vnda.ts"; - -const PATHS = [ - "/carrinho", - "/carrinho/adicionar", - "/carrinho/quantidade/atualizar", - "/carrinho/produtos-sugeridos/relacionados-carrinho", - "/carrinho/remover", - "/cep", - "/cupom/ajax", -] as const; - -type Paths = (typeof PATHS)[number]; - -export const paths = ({ internalDomain: base }: Account) => { - if (!base) { - throw new Error( - "Missing `internalDomain` configuration. Open your deco admin and fill the VNDA account block", - ); - } - - return PATHS.reduce( - (acc, path) => { - acc[path] = new URL(path, base).href; - - return acc; - }, - {} as Record, - ); -}; diff --git a/vnda/utils/queryBuilder.ts b/vnda/utils/queryBuilder.ts deleted file mode 100644 index ab897a45a..000000000 --- a/vnda/utils/queryBuilder.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ProductSearchParams } from "./client/types.ts"; - -export const paramsToQueryString = ( - params: ProductSearchParams | { key: string; value: string }[], -) => { - const keys = Object.keys(params) as Array; - - const transformedValues = keys.map((_key) => { - const value = params[_key]; - const key = Array.isArray(value) ? `${_key}[]` : _key; - - if (!value) { - return []; - } - - if (Array.isArray(value)) { - return value.flatMap((v) => [key, v.toString()]); - } - - return [key, value?.toString()]; - }).filter((v) => v.length); - - return new URLSearchParams(transformedValues); -}; diff --git a/vnda/utils/segment.ts b/vnda/utils/segment.ts new file mode 100644 index 000000000..9a6b7d4b0 --- /dev/null +++ b/vnda/utils/segment.ts @@ -0,0 +1,51 @@ +import { getCookies, setCookie } from "std/http/mod.ts"; +import { AppContext } from "../mod.ts"; + +interface Segment { + agent: string; +} + +export const SEGMENT_COOKIE_NAME = "vnda_segment"; +const SEGMENT = Symbol("segment"); +const SIXTYDAYS = new Date(Date.now() + 60 * 24 * 60 * 60 * 1000); + +export const getSegmentFromBag = (ctx: AppContext): string => + ctx.bag?.get(SEGMENT); +export const setSegmentInBag = (ctx: AppContext, segment: string) => + ctx.bag?.set(SEGMENT, segment); + +export const parse = (cookie: string) => JSON.parse(atob(cookie)); + +export const buildSegmentCookie = (req: Request): string | null => { + const url = new URL(req.url); + const param = url.searchParams.get("agent"); + if (param) { + const partialSegment: string = param; + return partialSegment; + } + return null; +}; + +export const getSegmentFromCookie = ( + req: Request, +): string | undefined => { + const cookies = getCookies(req.headers); + const cookie = cookies[SEGMENT_COOKIE_NAME]; + return cookie; +}; + +export const setSegmentCookie = ( + segment: string, + headers: Headers = new Headers(), +): Headers => { + setCookie(headers, { + value: segment, + name: SEGMENT_COOKIE_NAME, + path: "/", + secure: true, + httpOnly: true, + expires: SIXTYDAYS, + }); + + return headers; +}; diff --git a/vnda/utils/transform.ts b/vnda/utils/transform.ts index 2dde95a4c..84133429f 100644 --- a/vnda/utils/transform.ts +++ b/vnda/utils/transform.ts @@ -1,43 +1,80 @@ import { Filter, + ImageObject, Offer, Product, PropertyValue, Seo, UnitPriceSpecification, } from "../../commerce/types.ts"; +import { STALE } from "../../utils/fetch.ts"; +import { AppContext } from "../mod.ts"; +import { ProductGroup, ProductPrice, SEO } from "./client/types.ts"; import { - Installment, - ProductGroup, - ProductSearchResult, + OpenAPI, + Product as OProduct, + ProductInstallment, + ProductSearch, ProductVariant, - SEO, -} from "./client/types.ts"; + VariantProductSearch, +} from "./openapi/vnda.openapi.gen.ts"; +import { getSegmentFromCookie, parse } from "./segment.ts"; + +export type VNDAProductGroup = ProductSearch | OProduct; +type VNDAProduct = VariantProductSearch | ProductVariant; interface ProductOptions { url: URL; /** Price coded currency, e.g.: USD, BRL */ priceCurrency: string; + productPrice?: ProductPrice | null; } +type TypeTags = (string | { + key: string; + value: string; + isProperty: boolean; +})[]; + export const getProductCategoryTag = ({ tags }: ProductGroup) => tags?.filter(({ type }) => type === "categoria")[0]; +export const canonicalFromTags = ( + tags: Pick[], + url: URL, +) => { + const pathname = tags.map((t) => t.name).join("/"); + return new URL(`/${pathname}`, url); +}; + export const getSEOFromTag = ( - tag: Pick, - req: Request, -): Seo => ({ - title: tag.title || "", - description: tag.description || "", - canonical: req.url, -}); + tags: Pick[], + url: URL, + seo: OpenAPI["GET /api/v2/seo_data"]["response"][0] | undefined, + hasTypeTags: boolean, + isSearchPage?: boolean, +): Seo => { + const tag = tags.at(-1); + const canonical = canonicalFromTags(tags, url); + + if (url.searchParams.has("page")) { + canonical.searchParams.set("page", url.searchParams.get("page")!); + } + + return { + title: isSearchPage ? "" : seo?.title || tag?.title || "", + description: isSearchPage ? "" : seo?.description || tag?.description || "", + canonical: canonical.href, + noIndexing: hasTypeTags, + }; +}; export const parseSlug = (slug: string) => { const segments = slug.split("-"); const id = Number(segments.at(-1)); if (!id) { - throw new Error("Malformed slug. Expecting {slug}-{id} format"); + return null; } return { @@ -46,15 +83,21 @@ export const parseSlug = (slug: string) => { }; }; -const pickVariant = (product: ProductGroup, variantId: string | null) => { - const variants = normalizeVariants(product.variants); - const [head] = variants; +export const pickVariant = ( + variants: VNDAProductGroup["variants"], + variantId: string | null, + normalize = true, +) => { + const normalizedVariants = normalize + ? normalizeVariants(variants) + : variants as VariantProductSearch[]; + const [head] = normalizedVariants; let [target, main, available]: Array< - ProductVariant | null + VNDAProduct | null > = [null, head, null]; - for (const variant of variants) { + for (const variant of normalizedVariants) { if (variant.sku === variantId) target = variant; else if (variant.main) main = variant; else if (variant.available && !available) available = variant; @@ -65,7 +108,9 @@ const pickVariant = (product: ProductGroup, variantId: string | null) => { return target || fallback || head; }; -const normalizeInstallments = (installments: Installment[] | number[] = []) => { +const normalizeInstallments = ( + installments: ProductInstallment[] | number[] = [], +) => { if (typeof installments[0] === "number") { const total = (installments as number[]).reduce((acc, curr) => acc + curr); @@ -76,7 +121,9 @@ const normalizeInstallments = (installments: Installment[] | number[] = []) => { }]; } - return (installments as Installment[]).map(({ number, price, total }) => ({ + return (installments as ProductInstallment[]).map(( + { number, price, total }, + ) => ({ number, price, total, @@ -85,15 +132,16 @@ const normalizeInstallments = (installments: Installment[] | number[] = []) => { const toURL = (src: string) => src.startsWith("//") ? `https:${src}` : src; -const toOffer = ({ +export const toOffer = ({ price, sale_price, + intl_price, available_quantity, available, installments = [], -}: ProductVariant): Offer | null => { +}: VNDAProduct & { intl_price?: number }): Offer[] => { if (!price || !sale_price) { - return null; + return []; } const priceSpecification: UnitPriceSpecification[] = [{ @@ -123,8 +171,8 @@ const toOffer = ({ }); } - return { - "@type": "Offer", + const offers: Offer[] = [{ + "@type": "Offer" as const, seller: "VNDA", price, priceSpecification, @@ -134,10 +182,33 @@ const toOffer = ({ availability: available ? "https://schema.org/InStock" : "https://schema.org/OutOfStock", - }; + }]; + + if (intl_price) { + offers.push({ + "@type": "Offer", + seller: "VNDA_INTL", + price: intl_price, + priceSpecification: [{ + "@type": "UnitPriceSpecification", + priceType: "https://schema.org/SalePrice", + price: intl_price, + }], + inventoryLevel: { + value: available_quantity, + }, + availability: available + ? "https://schema.org/InStock" + : "https://schema.org/OutOfStock", + // Static since VNDA only have a BRL price and USD when intl_price is available + priceCurrency: "USD", + }); + } + + return offers; }; -const toPropertyValue = (variant: ProductVariant): PropertyValue[] => +const toPropertyValue = (variant: VNDAProduct): PropertyValue[] => Object.values(variant.properties ?? {}) .filter(Boolean) .map(({ value, name }) => @@ -149,36 +220,89 @@ const toPropertyValue = (variant: ProductVariant): PropertyValue[] => } as PropertyValue) ).filter((x): x is PropertyValue => Boolean(x)); +const toPropertyValueTags = (tags: ProductSearch["tags"]): PropertyValue[] => + tags?.map((tag) => + tag && ({ + "@type": "PropertyValue", + name: tag.name, + value: JSON.stringify(tag), + valueReference: "TAGS", + } as PropertyValue) + ); + +const toPropertyValueCategoryTags = ( + categoryTags: OProduct["category_tags"], +) => { + if (!categoryTags) return []; + + return categoryTags.map((tag) => { + return { + "@type": "PropertyValue", + name: tag.tag_type, + value: tag.name, + description: tag.title, + valueReference: "TAGS", + } as PropertyValue; + }); +}; + // deno-lint-ignore no-explicit-any -const isProductVariant = (p: any): p is ProductVariant => +const isProductVariant = (p: any): p is VariantProductSearch => typeof p.id === "number"; const normalizeVariants = ( - variants: ProductGroup["variants"] = [], -): ProductVariant[] => - variants.flatMap((v) => isProductVariant(v) ? [v] : Object.values(v)); + variants: VNDAProductGroup["variants"] = [], +): VNDAProduct[] => + variants.flatMap((v) => + isProductVariant(v) ? [v] : Object.values(v) as VNDAProduct[] + ); + +const toImageObjectVideo = ( + video: OpenAPI["GET /api/v2/products/:productId/videos"]["response"], +): ImageObject[] => + video?.map(({ url, embed_url, thumbnail_url }) => ({ + "@type": "ImageObject", + encodingFormat: "video", + contentUrl: url, + thumbnailUrl: thumbnail_url, + embedUrl: embed_url, + } as ImageObject)); export const toProduct = ( - product: ProductGroup, + product: VNDAProductGroup, variantId: string | null, options: ProductOptions, level = 0, ): Product => { - const { url, priceCurrency } = options; - const variant = pickVariant(product, variantId); + const { url, priceCurrency, productPrice } = options; + const variant = pickVariant(product.variants, variantId); const variants = normalizeVariants(product.variants); + const variantPrices = productPrice?.variants + ? pickVariant( + productPrice.variants as VNDAProductGroup["variants"], + variantId, + false, + ) + : null; + const offers = toOffer(variantPrices ?? variant); + const variantUrl = new URL( `/produto/${product.slug}-${product.id}?skuId=${variant.sku}`, url.origin, ).href; + const productUrl = new URL( `/produto/${product.slug}-${product.id}`, url.origin, ).href; + const productID = `${variant.sku}`; const productGroupID = `${product.id}`; - const offer = toOffer(variant); - const offers = offer ? [offer] : []; + + const myTags = "tags" in product ? product.tags : []; + const myCategoryTags = "category_tags" in product + ? product.category_tags + : []; return { "@type": "Product", @@ -187,7 +311,11 @@ export const toProduct = ( url: variantUrl, name: product.name, description: product.description, - additionalProperty: toPropertyValue(variant), + additionalProperty: [ + ...toPropertyValue(variant), + ...toPropertyValueTags(myTags), + ...toPropertyValueCategoryTags(myCategoryTags), + ], inProductGroupWithID: productGroupID, gtin: product.reference, isVariantOf: { @@ -201,16 +329,27 @@ export const toProduct = ( ? variants.map((v) => toProduct(product, v.sku!, options, 1)) : [], }, - image: [{ - "@type": "ImageObject", - alternateName: product.name ?? "", - url: toURL(product.image_url ?? ""), - }], + image: product.images?.length ?? 0 > 1 + ? product.images?.map((img) => ({ + "@type": "ImageObject" as const, + encodingFormat: "image", + alternateName: `${img.url}`, + url: toURL(img.url!), + })) + : [ + { + "@type": "ImageObject", + encodingFormat: "image", + alternateName: product.name ?? "", + url: toURL(product.image_url ?? ""), + }, + ], + // images: offers: { "@type": "AggregateOffer", priceCurrency: priceCurrency, - highPrice: product.price!, - lowPrice: product.sale_price!, + highPrice: productPrice?.price ?? product.price!, + lowPrice: productPrice?.sale_price ?? product.sale_price!, offerCount: offers.length, offers: offers, }, @@ -236,27 +375,60 @@ const removeFilter = ( filter: { key: string; value: string }, ) => typeTagsInUse.filter((inUse) => - inUse.key !== filter.key && - inUse.value !== filter.value + !(inUse.key === filter.key && inUse.value === filter.value) ); export const toFilters = ( - aggregations: ProductSearchResult["aggregations"], + aggregations: + OpenAPI["GET /api/v2/products/search"]["response"]["aggregations"], typeTagsInUse: { key: string; value: string }[], cleanUrl: URL, ): Filter[] => { + if (!aggregations) { + return []; + } + const priceRange = { "@type": "FilterRange" as const, label: "Valor", key: "price_range", values: { - min: aggregations.min_price, - max: aggregations.max_price, + min: aggregations.min_price!, + max: aggregations.max_price!, }, }; - const types = Object.keys(aggregations.types).map((typeKey) => { - const typeValues = aggregations.types[typeKey]; + const combinedFiltersKeys = Object.keys(aggregations.types ?? {}).concat( + ...Object.keys(aggregations.properties ?? {}), + ); + + const types = combinedFiltersKeys.map((typeKey) => { + const isProperty = typeKey.includes("property"); + + interface AggregationType { + name: string; + title: string; + count: number; + value: string; + } + const typeValues: AggregationType[] = isProperty + // deno-lint-ignore no-explicit-any + ? ((aggregations.properties as any)[ + typeKey as string + ] as AggregationType[]) + // deno-lint-ignore no-explicit-any + : ((aggregations.types as any)[ + typeKey as string + ] as AggregationType[]); + + if (isProperty) { + typeValues.forEach((obj) => { + if (obj.value) { + obj.title = obj.value; + obj.name = obj.value; + } + }); + } return { "@type": "FilterToggle" as const, @@ -293,25 +465,93 @@ export const toFilters = ( ]; }; -export const typeTagExtractor = (url: URL) => { +export const typeTagExtractor = (url: URL, tags: { type?: string }[]) => { + const cleanUrl = new URL(url); const keysToDestroy: string[] = []; - const typeTags: { key: string; value: string }[] = []; - const typeTagRegex = /\btype_tags\[(\S+)\]\[\]/; + const typeTags: { key: string; value: string; isProperty: boolean }[] = []; + const typeTagRegex = /\btype_tags\[(.*?)\]\[\]/; - url.searchParams.forEach((value, key) => { + cleanUrl.searchParams.forEach((value, key) => { const match = typeTagRegex.exec(key); if (match) { - keysToDestroy.push(key); - typeTags.push({ key, value }); + const tagValue = match[1]; + const isProperty = tagValue.includes("property"); + if (tags.some((tag) => tag.type === tagValue) || isProperty) { + keysToDestroy.push(key); + typeTags.push({ key, value, isProperty }); + } } }); - // it can't be done inside the forEach instruction above - typeTags.forEach((tag) => url.searchParams.delete(tag.key)); + keysToDestroy.forEach((key) => cleanUrl.searchParams.delete(key)); + + cleanUrl.searchParams.delete("page"); return { typeTags, - cleanUrl: url, + cleanUrl, }; }; + +export const addVideoToProduct = ( + product: Product, + video: OpenAPI["GET /api/v2/products/:productId/videos"]["response"] | null, +): Product => ({ + ...product, + image: [ + ...(product?.image ?? []), + ...(video ? toImageObjectVideo(video) : []), + ], +}); + +export const fetchAndApplyPrices = async ( + products: Product[], + priceCurrency: string, + req: Request, + ctx: AppContext, +): Promise => { + const segmentCookie = getSegmentFromCookie(req); + const segment = segmentCookie ? parse(segmentCookie) : null; + + const pricePromises = products.map((product) => + ctx.api["GET /api/v2/products/:productId/price"]({ + productId: product.sku, + coupon_codes: segment?.cc ? [segment.cc] : [], + }, STALE) + ); + + const priceResults = await Promise.all(pricePromises); + + return products.map((product) => { + const matchingPriceInfo = priceResults.find((priceResult) => + (priceResult as unknown as ProductPrice).variants.some((variant) => + variant.sku === product.sku + ) + ) as unknown as ProductPrice; + + const variantPrices = matchingPriceInfo?.variants + ? pickVariant( + matchingPriceInfo.variants as VNDAProductGroup["variants"], + product.sku, + false, + ) + : null; + + if (!variantPrices) return product; + + const offers = toOffer(variantPrices); + + return { + ...product, + offers: { + "@type": "AggregateOffer" as const, + priceCurrency: priceCurrency, + highPrice: variantPrices?.price ?? product.offers?.highPrice ?? 0, + lowPrice: variantPrices?.sale_price ?? product.offers?.lowPrice ?? 0, + offerCount: offers.length, + offers: offers, + }, + }; + }); +}; diff --git a/vtex/README.md b/vtex/README.md new file mode 100644 index 000000000..c3faab1e3 --- /dev/null +++ b/vtex/README.md @@ -0,0 +1,6 @@ +VTEX is a cloud-based e-commerce platform and digital commerce company that provides a comprehensive set of tools and services for businesses looking to establish and manage their online retail operations. + +This app wrapps VTEX API into a comprehensive set of loaders/actions/workflows +empowering non technical users to interact and act upon their headless commerce. + +If you want to use a custom search engine (Algolia, Typesense etc), you will need to fill the App Key & App Token properties. For these, follow this guide diff --git a/vtex/actions/address/createAddress.ts b/vtex/actions/address/createAddress.ts new file mode 100644 index 000000000..af849c53a --- /dev/null +++ b/vtex/actions/address/createAddress.ts @@ -0,0 +1,67 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +interface AddressInput { + name?: string; + addressName: string; + addressType?: string; + city?: string; + complement?: string; + country?: string; + geoCoordinates?: number[]; + neighborhood?: string; + number?: string; + postalCode?: string; + receiverName?: string; + reference?: string; + state?: string; + street?: string; +} + +interface SavedAddress { + id: string; + cacheId: string; +} + +async function action( + props: AddressInput, + req: Request, + ctx: AppContext, +): Promise< + | SavedAddress + | null +> { + const { io } = ctx; + const { cookie } = parseCookie(req.headers, ctx.account); + + const mutation = ` + mutation SaveAddress($address: AddressInput!) { + saveAddress(address: $address) @context(provider: "vtex.store-graphql") { + id + cacheId + } + }`; + + try { + const { saveAddress: savedAddress } = await io.query< + { saveAddress: SavedAddress }, + { address: AddressInput } + >( + { + query: mutation, + operationName: "SaveAddress", + variables: { + address: props, + }, + }, + { headers: { cookie } }, + ); + + return savedAddress; + } catch (error) { + console.error("Error saving address:", error); + return null; + } +} + +export default action; diff --git a/vtex/actions/address/deleteAddress.ts b/vtex/actions/address/deleteAddress.ts new file mode 100644 index 000000000..760afd133 --- /dev/null +++ b/vtex/actions/address/deleteAddress.ts @@ -0,0 +1,58 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +interface DeleteAddress { + addressId: string; +} + +interface AddressInput { + addressId: string; +} + +async function action( + { addressId }: AddressInput, + req: Request, + ctx: AppContext, +) { + const { io } = ctx; + const { cookie } = parseCookie(req.headers, ctx.account); + + const mutation = ` + mutation DeleteAddress($addressId: String) { + deleteAddress(id: $addressId) { + cacheId + addresses: address { + addressId: id + addressType + addressName + city + complement + country + neighborhood + number + postalCode + geoCoordinates + receiverName + reference + state + street + } + } + }`; + + try { + return await io.query( + { + query: mutation, + operationName: "DeleteAddress", + variables: { addressId }, + }, + { headers: { cookie } }, + ); + } catch (error) { + console.error("Error deleting address:", error); + return null; + } +} + +export default action; diff --git a/vtex/actions/address/updateAddress.ts b/vtex/actions/address/updateAddress.ts new file mode 100644 index 000000000..136ed41bc --- /dev/null +++ b/vtex/actions/address/updateAddress.ts @@ -0,0 +1,92 @@ +import { PostalAddressVTEX } from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +interface Address { + name?: string; + addressName?: string; + addressType?: string; + city?: string; + complement: string | null; + country?: string; + geoCoordinates?: number[]; + neighborhood?: string; + number?: string; + postalCode?: string; + receiverName: string | null; + reference?: string; + state?: string; + street?: string; + addressId: string; +} + +async function action( + props: Address, + req: Request, + ctx: AppContext, +): Promise< + | PostalAddressVTEX + | null +> { + const { io } = ctx; + const { cookie } = parseCookie(req.headers, ctx.account); + const { addressId, ...addressFields } = props; + + const mutation = ` + mutation UpdateAddress($addressId: String!, $addressFields: AddressInput) { + updateAddress(id: $addressId, fields: $addressFields) + @context(provider: "vtex.store-graphql") { + cacheId + addresses: address { + addressId: id + addressType + addressName + city + complement + country + neighborhood + number + postalCode + geoCoordinates + receiverName + reference + state + street + } + } + } + `; + + try { + const { updateAddress: updatedAddress } = await io.query< + { updateAddress: Address }, + { addressId: string; addressFields: Omit } + >( + { + query: mutation, + operationName: "UpdateAddress", + variables: { + addressId, + addressFields, + }, + }, + { headers: { cookie } }, + ); + + return { + "@type": "PostalAddress", + addressCountry: updatedAddress?.country, + addressLocality: updatedAddress?.city, + addressRegion: updatedAddress?.state, + postalCode: updatedAddress?.postalCode, + streetAddress: updatedAddress?.street, + receiverName: updatedAddress?.receiverName, + complement: updatedAddress?.complement, + addressId: updatedAddress?.addressId, + }; + } catch (error) { + console.error("Error updating address:", error); + return null; + } +} +export default action; diff --git a/vtex/actions/analytics/sendEvent.ts b/vtex/actions/analytics/sendEvent.ts new file mode 100644 index 000000000..061cb3488 --- /dev/null +++ b/vtex/actions/analytics/sendEvent.ts @@ -0,0 +1,76 @@ +// Intelligent Search analytics integration +import { AppContext } from "../../mod.ts"; +import { getISCookiesFromBag } from "../../utils/intelligentSearch.ts"; + +export type Props = + | { + type: "session.ping"; + url: string; + } + | { + type: "page.cart"; + products: { + productId: string; + quantity: number; + }[]; + } + | { + type: "page.empty_cart"; + products: []; + } + | { + type: "page.confirmation"; + order: string; + products: { + productId: string; + quantity: number; + price: number; + }[]; + } + | { + type: "search.click"; + position: number; + text: string; + productId: string; + url: string; + } + | { + type: "search.query"; + url: string; + text: string; + misspelled: boolean; + match: number; + operator: string; + locale: string; + }; + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#post-/api/checkout/pub/orderForm/-orderFormId-/items + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { sp } = ctx; + const cookies = getISCookiesFromBag(ctx); + + if (!cookies) { + throw new Error("Missing IS Cookies"); + } + + await sp["POST /event-api/v1/:account/event"]({ account: ctx.account }, { + body: { + ...props, + ...cookies, + agent: req.headers.get("user-agent") || "deco-sites/apps", + }, + headers: { + "content-type": "application/json", + }, + }); + + return null; +}; + +export default action; diff --git a/vtex/actions/cart/addItems.ts b/vtex/actions/cart/addItems.ts new file mode 100644 index 000000000..e538cd05c --- /dev/null +++ b/vtex/actions/cart/addItems.ts @@ -0,0 +1,63 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { forceHttpsOnAssets } from "../../utils/transform.ts"; + +export interface Item { + quantity: number; + seller: string; + id: string; + index?: number; + price?: number; +} + +export interface Props { + orderItems: Item[]; + allowedOutdatedData?: Array<"paymentData">; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#post-/api/checkout/pub/orderForm/-orderFormId-/items + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { + orderItems, + allowedOutdatedData = ["paymentData"], + } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + try { + const response = await vcsDeprecated + ["POST /api/checkout/pub/orderForm/:orderFormId/items"]({ + orderFormId, + allowedOutdatedData, + sc: segment?.payload.channel, + }, { + body: { orderItems }, + headers: { + "content-type": "application/json", + accept: "application/json", + cookie, + }, + }); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return forceHttpsOnAssets((await response.json()) as OrderForm); + } catch (error) { + console.error(error); + + throw error; + } +}; + +export default action; diff --git a/vtex/actions/cart/addOfferings.ts b/vtex/actions/cart/addOfferings.ts new file mode 100644 index 000000000..07fe50b88 --- /dev/null +++ b/vtex/actions/cart/addOfferings.ts @@ -0,0 +1,52 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import { forceHttpsOnAssets } from "../../utils/transform.ts"; +import { OrderForm } from "../../utils/types.ts"; +import { DEFAULT_EXPECTED_SECTIONS } from "./updateItemAttachment.ts"; + +export interface Props { + index: number; + id: number; + expectedOrderFormSections?: string[]; +} + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +) => { + const { index, id, expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS } = + props; + const { vcsDeprecated } = ctx; + + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + + try { + const response = await vcsDeprecated + ["POST /api/checkout/pub/orderForm/:orderFormId/items/:index/offerings"]({ + orderFormId, + index, + }, { + body: { + expectedOrderFormSections, + id, + info: null, + }, + headers: { + "content-type": "application/json", + accept: "application/json", + cookie, + }, + }); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return forceHttpsOnAssets((await response.json()) as OrderForm); + } catch (error) { + throw error; + } +}; + +export default action; diff --git a/vtex/actions/cart/clearOrderformMessages.ts b/vtex/actions/cart/clearOrderformMessages.ts new file mode 100644 index 000000000..c93c4c307 --- /dev/null +++ b/vtex/actions/cart/clearOrderformMessages.ts @@ -0,0 +1,30 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; + +const action = async ( + _props: unknown, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + + const response = await vcsDeprecated[ + "POST /api/checkout/pub/orderForm/:orderFormId/messages/clear" + ]( + { orderFormId }, + { + headers: { accept: "application/json", cookie }, + body: {}, + }, + ); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/getInstallment.ts b/vtex/actions/cart/getInstallment.ts new file mode 100644 index 000000000..bd22655d8 --- /dev/null +++ b/vtex/actions/cart/getInstallment.ts @@ -0,0 +1,36 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { InstallmentOption } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +export interface Props { + paymentSystem: number; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#get-/api/checkout/pub/orderForm/-orderFormId-/installments + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { paymentSystem } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["GET /api/checkout/pub/orderForm/:orderFormId/installments"]( + { orderFormId, paymentSystem, sc: segment?.payload.channel }, + { headers: { accept: "application/json", cookie } }, + ); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/removeItemAttachment.ts b/vtex/actions/cart/removeItemAttachment.ts new file mode 100644 index 000000000..f0e5f14bd --- /dev/null +++ b/vtex/actions/cart/removeItemAttachment.ts @@ -0,0 +1,70 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +export interface Props { + /** @description index of the item in the cart.items array you want to edit */ + index: number; + /** @description attachment name */ + attachment: string; + content: Record; + expectedOrderFormSections?: string[]; + noSplitItem?: boolean; +} + +export const DEFAULT_EXPECTED_SECTIONS = [ + "items", + "totalizers", + "clientProfileData", + "shippingData", + "paymentData", + "sellers", + "messages", + "marketingData", + "clientPreferencesData", + "storePreferencesData", + "giftRegistryData", + "ratesAndBenefitsData", + "openTextField", + "commercialConditionData", + "customData", +]; + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { + index, + attachment, + content, + noSplitItem = true, + expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS, + } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["DELETE /api/checkout/pub/orderForm/:orderFormId/items/:index/attachments/:attachment"]( + { orderFormId, attachment, index, sc: segment?.payload.channel }, + { + body: { content, noSplitItem, expectedOrderFormSections }, + headers: { + accept: "application/json", + "content-type": "application/json", + cookie, + }, + }, + ); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/removeItems.ts b/vtex/actions/cart/removeItems.ts new file mode 100644 index 000000000..d2e9c2d82 --- /dev/null +++ b/vtex/actions/cart/removeItems.ts @@ -0,0 +1,37 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#post-/api/checkout/pub/orderForm/-orderFormId-/items/removeAll + */ +const action = async ( + _props: unknown, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["POST /api/checkout/pub/orderForm/:orderFormId/items/removeAll"]( + { orderFormId, sc: segment?.payload.channel }, + { + headers: { + "content-type": "application/json", + accept: "application/json", + cookie, + }, + }, + ); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/removeOffering.ts b/vtex/actions/cart/removeOffering.ts new file mode 100644 index 000000000..393cb84d8 --- /dev/null +++ b/vtex/actions/cart/removeOffering.ts @@ -0,0 +1,54 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import { forceHttpsOnAssets } from "../../utils/transform.ts"; +import { OrderForm } from "../../utils/types.ts"; +import { DEFAULT_EXPECTED_SECTIONS } from "./updateItemAttachment.ts"; + +export interface Props { + index: number; + id: number; + expectedOrderFormSections?: string[]; +} + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +) => { + const { index, id, expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS } = + props; + const { vcsDeprecated } = ctx; + + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + + try { + const response = await vcsDeprecated + ["POST /api/checkout/pub/orderForm/:orderFormId/items/:index/offerings/:id/remove"]( + { + orderFormId, + id, + index: index, + }, + { + body: { + expectedOrderFormSections, + }, + headers: { + "content-type": "application/json", + accept: "application/json", + cookie, + }, + }, + ); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return forceHttpsOnAssets((await response.json()) as OrderForm); + } catch (error) { + throw error; + } +}; + +export default action; diff --git a/vtex/actions/cart/simulation.ts b/vtex/actions/cart/simulation.ts new file mode 100644 index 000000000..046d91280 --- /dev/null +++ b/vtex/actions/cart/simulation.ts @@ -0,0 +1,51 @@ +import { AppContext } from "../../mod.ts"; +import type { SimulationOrderForm } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +export interface Item { + id: number; + quantity: number; + seller: string; +} + +export interface Props { + items: Item[]; + postalCode: string; + country: string; + RnbBehavior?: 0 | 1; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#post-/api/checkout/pub/orderForms/simulation + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const cookie = req.headers.get("cookie") ?? ""; + const { vcsDeprecated } = ctx; + const { items, postalCode, country, RnbBehavior = 1 } = props; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated[ + "POST /api/checkout/pub/orderForms/simulation" + ]( + { + RnbBehavior, + sc: segment?.payload.channel, + }, + { + body: { items, country, postalCode }, + headers: { + accept: "application/json", + "content-type": "application/json", + cookie, + }, + }, + ); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/updateAttachment.ts b/vtex/actions/cart/updateAttachment.ts new file mode 100644 index 000000000..4b5e7c014 --- /dev/null +++ b/vtex/actions/cart/updateAttachment.ts @@ -0,0 +1,49 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { DEFAULT_EXPECTED_SECTIONS } from "./updateItemAttachment.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +export interface Props { + attachment: string; + expectedOrderFormSections?: string[]; + // deno-lint-ignore no-explicit-any + body: any; +} + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { + attachment, + body, + expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS, + } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["POST /api/checkout/pub/orderForm/:orderFormId/attachments/:attachment"]({ + orderFormId, + attachment, + sc: segment?.payload.channel, + }, { + body: { expectedOrderFormSections, ...body }, + headers: { + accept: "application/json", + "content-type": "application/json", + cookie, + }, + }); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/updateCoupons.ts b/vtex/actions/cart/updateCoupons.ts new file mode 100644 index 000000000..79ebce2a5 --- /dev/null +++ b/vtex/actions/cart/updateCoupons.ts @@ -0,0 +1,43 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +export interface Props { + text: string; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#post-/api/checkout/pub/orderForm/-orderFormId-/coupons + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { text } = props; + const cookie = req.headers.get("cookie") ?? ""; + const { orderFormId } = parseCookie(req.headers); + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["POST /api/checkout/pub/orderForm/:orderFormId/coupons"]({ + orderFormId, + sc: segment?.payload.channel, + }, { + body: { text }, + headers: { + accept: "application/json", + "content-type": "application/json", + cookie, + }, + }); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/updateGifts.ts b/vtex/actions/cart/updateGifts.ts new file mode 100644 index 000000000..a5b4c8e99 --- /dev/null +++ b/vtex/actions/cart/updateGifts.ts @@ -0,0 +1,40 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm, SelectableGifts } from "../../utils/types.ts"; +import { DEFAULT_EXPECTED_SECTIONS } from "./updateItemAttachment.ts"; + +export interface Props extends SelectableGifts { + expectedOrderFormSections?: string[]; +} + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { + expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS, + id, + selectedGifts, + } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + + const response = await vcsDeprecated[ + "POST /api/checkout/pub/orderForm/:orderFormId/selectable-gifts/:giftId" + ]( + { orderFormId, giftId: id }, + { + headers: { accept: "application/json", cookie }, + body: { expectedOrderFormSections, selectedGifts, id }, + }, + ); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/updateItemAttachment.ts b/vtex/actions/cart/updateItemAttachment.ts new file mode 100644 index 000000000..534c5487b --- /dev/null +++ b/vtex/actions/cart/updateItemAttachment.ts @@ -0,0 +1,75 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +export interface Props { + /** @description index of the item in the cart.items array you want to edit */ + index: number; + /** @description attachment name */ + attachment: string; + content: Record; + expectedOrderFormSections?: string[]; + noSplitItem?: boolean; +} + +export const DEFAULT_EXPECTED_SECTIONS = [ + "items", + "totalizers", + "clientProfileData", + "shippingData", + "paymentData", + "sellers", + "messages", + "marketingData", + "clientPreferencesData", + "storePreferencesData", + "giftRegistryData", + "ratesAndBenefitsData", + "openTextField", + "commercialConditionData", + "customData", +]; + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { + index, + attachment, + content, + noSplitItem = true, + expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS, + } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["POST /api/checkout/pub/orderForm/:orderFormId/items/:index/attachments/:attachment"]( + { + orderFormId, + attachment, + index, + sc: segment?.payload.channel, + }, + { + body: { content, noSplitItem, expectedOrderFormSections }, + headers: { + accept: "application/json", + "content-type": "application/json", + cookie, + }, + }, + ); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/updateItemPrice.ts b/vtex/actions/cart/updateItemPrice.ts new file mode 100644 index 000000000..32108d754 --- /dev/null +++ b/vtex/actions/cart/updateItemPrice.ts @@ -0,0 +1,48 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +export interface Props { + itemIndex: number; + price: number; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#put-/api/checkout/pub/orderForm/-orderFormId-/items/-itemIndex-/price + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { + itemIndex, + price, + } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["PUT /api/checkout/pub/orderForm/:orderFormId/items/:index/price"]({ + orderFormId, + index: itemIndex, + sc: segment?.payload.channel, + }, { + body: { price }, + headers: { + accept: "application/json", + "content-type": "application/json", + cookie, + }, + }); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/updateItems.ts b/vtex/actions/cart/updateItems.ts new file mode 100644 index 000000000..6bcc59613 --- /dev/null +++ b/vtex/actions/cart/updateItems.ts @@ -0,0 +1,53 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; +import type { OrderForm } from "../../utils/types.ts"; + +export interface Item { + quantity: number; + index: number; +} + +export interface Props { + orderItems: Item[]; + allowedOutdatedData?: Array<"paymentData">; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#post-/api/checkout/pub/orderForm/-orderFormId-/items/update + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { + orderItems, + allowedOutdatedData = ["paymentData"], + } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["POST /api/checkout/pub/orderForm/:orderFormId/items/update"]({ + orderFormId, + allowedOutdatedData, + sc: segment?.payload.channel, + }, { + body: { orderItems }, + headers: { + "content-type": "application/json", + accept: "application/json", + cookie, + }, + }); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/updateProfile.ts b/vtex/actions/cart/updateProfile.ts new file mode 100644 index 000000000..ae54aeefe --- /dev/null +++ b/vtex/actions/cart/updateProfile.ts @@ -0,0 +1,43 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +export interface Props { + ignoreProfileData: boolean; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#patch-/api/checkout/pub/orderForm/-orderFormId-/profile + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { ignoreProfileData } = props; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["PATCH /api/checkout/pub/orderForm/:orderFormId/profile"]({ + orderFormId, + sc: segment?.payload.channel, + }, { + body: { ignoreProfileData }, + headers: { + "content-type": "application/json", + accept: "application/json", + cookie, + }, + }); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/cart/updateUser.ts b/vtex/actions/cart/updateUser.ts new file mode 100644 index 000000000..267088306 --- /dev/null +++ b/vtex/actions/cart/updateUser.ts @@ -0,0 +1,36 @@ +import { AppContext } from "../../mod.ts"; +import { proxySetCookie } from "../../utils/cookies.ts"; +import { parseCookie } from "../../utils/orderForm.ts"; +import type { OrderForm } from "../../utils/types.ts"; +import { getSegmentFromBag } from "../../utils/segment.ts"; + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#get-/checkout/changeToAnonymousUser/-orderFormId- + */ +const action = async ( + _props: unknown, + req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const { orderFormId } = parseCookie(req.headers); + const cookie = req.headers.get("cookie") ?? ""; + const segment = getSegmentFromBag(ctx); + + const response = await vcsDeprecated + ["GET /api/checkout/changeToAnonymousUser/:orderFormId"]({ + orderFormId, + sc: segment?.payload.channel, + }, { + headers: { + accept: "application/json", + cookie, + }, + }); + + proxySetCookie(response.headers, ctx.response.headers, req.url); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/masterdata/createDocument.ts b/vtex/actions/masterdata/createDocument.ts new file mode 100644 index 000000000..46619a3fd --- /dev/null +++ b/vtex/actions/masterdata/createDocument.ts @@ -0,0 +1,47 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; +import type { CreateNewDocument } from "../../utils/types.ts"; + +export interface Props { + data: Record; + acronym: string; + isPrivateEntity?: boolean; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/masterdata-api#post-/api/dataentities/-acronym-/documents + */ +const action = async ( + props: Props, + req: Request, + ctx: AppContext, + /* no-explicit-any */ +): Promise => { + const { vcs, vcsDeprecated } = ctx; + const { data, acronym, isPrivateEntity } = props; + const { cookie } = parseCookie(req.headers, ctx.account); + + const requestOptions = { + body: data, + headers: { + accept: "application/json", + "content-type": "application/json", + cookie, + }, + }; + + const response = + await (isPrivateEntity + ? vcs[`POST /api/dataentities/:acronym/documents`]( + { acronym }, + requestOptions, + ) + : vcsDeprecated[`POST /api/dataentities/:acronym/documents`]( + { acronym }, + requestOptions, + )); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/newsletter/subscribe.ts b/vtex/actions/newsletter/subscribe.ts new file mode 100644 index 000000000..7195bbbba --- /dev/null +++ b/vtex/actions/newsletter/subscribe.ts @@ -0,0 +1,37 @@ +import { AppContext } from "../../mod.ts"; + +export interface Props { + email: string; + name?: string; + page?: string; + part?: string; + campaing?: string; +} + +const action = async ( + props: Props, + _req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const form = new FormData(); + const { + email, + name = "", + part = "newsletter", + page = "_", + campaing = "newsletter:opt-in", + } = props; + + form.append("newsletterClientName", name); + form.append("newsletterClientEmail", email); + form.append("newsInternalPage", page); + form.append("newsInternalPart", part); + form.append("newsInternalCampaign", campaing); + + await vcsDeprecated["POST /no-cache/Newsletter.aspx"]({}, { + body: form, + }); +}; + +export default action; diff --git a/vtex/actions/notifyme.ts b/vtex/actions/notifyme.ts new file mode 100644 index 000000000..07d626bd5 --- /dev/null +++ b/vtex/actions/notifyme.ts @@ -0,0 +1,28 @@ +import { AppContext } from "../mod.ts"; + +export interface Props { + email: string; + skuId: string; + name?: string; +} + +/** + * @docs https://developers.vtex.com/docs/api-reference/checkout-api#post-/api/checkout/pub/orderForm/-orderFormId-/items + */ +const action = async ( + props: Props, + _req: Request, + ctx: AppContext, +): Promise => { + const { vcsDeprecated } = ctx; + const form = new FormData(); + const { email, skuId, name = "" } = props; + + form.append("notifymeClientName", name); + form.append("notifymeClientEmail", email); + form.append("notifymeIdSku", skuId); + + await vcsDeprecated["POST /no-cache/AviseMe.aspx"]({}, { body: form }); +}; + +export default action; diff --git a/vtex/actions/payments/delete.ts b/vtex/actions/payments/delete.ts new file mode 100644 index 000000000..341ecee37 --- /dev/null +++ b/vtex/actions/payments/delete.ts @@ -0,0 +1,39 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +export interface DeleteCard { + deletePaymentToken: boolean; +} + +interface Props { + id: string; +} + +async function loader( + { id }: Props, + req: Request, + ctx: AppContext, +): Promise { + const { io } = ctx; + const { cookie, payload } = parseCookie(req.headers, ctx.account); + + if (!payload?.sub || !payload?.userId) { + return null; + } + + const mutation = `mutation DeleteCreditCardToken($tokenId: ID!) { + deletePaymentToken(tokenId: $tokenId) @context(provider: "vtex.my-cards-graphql@2.x") + }`; + + try { + return await io.query({ + query: mutation, + variables: { tokenId: id }, + }, { headers: { cookie } }); + } catch (e) { + console.error(e); + return null; + } +} + +export default loader; diff --git a/vtex/actions/profile/newsletterProfile.ts b/vtex/actions/profile/newsletterProfile.ts new file mode 100644 index 000000000..1f14b6403 --- /dev/null +++ b/vtex/actions/profile/newsletterProfile.ts @@ -0,0 +1,60 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +interface NewsletterInput { + email: string; + isNewsletterOptIn: boolean; +} + +const newsletterProfile = async ( + props: NewsletterInput, + req: Request, + ctx: AppContext, +): Promise => { + const { io } = ctx; + const { cookie } = parseCookie(req.headers, ctx.account); + + if (!props?.email) { + console.error("User profile not found or email is missing:", props.email); + return null; + } + + const mutation = ` + mutation SubscribeNewsletter($email: String!, $isNewsletterOptIn: Boolean!) { + subscribeNewsletter(email: $email, isNewsletterOptIn: $isNewsletterOptIn) + @context(provider: "vtex.store-graphql@2.x") + } + `; + + const variables = { + email: props.email, + isNewsletterOptIn: props.isNewsletterOptIn, + }; + + try { + await io.query<{ subscribeNewsletter: boolean }, unknown>( + { + query: mutation, + operationName: "SubscribeNewsletter", + variables, + }, + { + headers: { + cookie, + }, + }, + ); + + const result = await ctx.invoke("vtex/loaders/user.ts"); + const newsletterField = result?.customFields?.find((field) => + field.key === "isNewsletterOptIn" + ); + + return newsletterField?.value === "true"; + } catch (error) { + console.error("Error subscribing to newsletter:", error); + return null; + } +}; + +export default newsletterProfile; diff --git a/vtex/actions/profile/updateProfile.ts b/vtex/actions/profile/updateProfile.ts new file mode 100644 index 000000000..17c4f724b --- /dev/null +++ b/vtex/actions/profile/updateProfile.ts @@ -0,0 +1,93 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; +import { Person } from "../../../commerce/types.ts"; +import type { User } from "../../loaders/user.ts"; + +export interface UserMutation { + firstName?: string; + lastName?: string; + email?: string; + homePhone?: string | null; + gender?: string | null; + birthDate?: string | null; + corporateName?: string | null; + tradeName?: string | null; + businessPhone?: string | null; + isCorporate?: boolean; +} + +const updateProfile = async ( + props: UserMutation, + req: Request, + ctx: AppContext, +): Promise => { + const { io } = ctx; + const { cookie } = parseCookie(req.headers, ctx.account); + + if (!props?.email) { + console.error("User profile not found or email is missing:", props.email); + return null; + } + const mutation = ` + mutation UpdateProfile($input: ProfileInput!) { + updateProfile(fields: $input) @context(provider: "vtex.store-graphql") { + cacheId + firstName + lastName + birthDate + gender + homePhone + businessPhone + document + email + tradeName + corporateName + corporateDocument + stateRegistration + isCorporate + } + } + `; + + try { + const { updateProfile: updatedUser } = await io.query< + { updateProfile: User }, + { input: UserMutation } + >( + { + query: mutation, + operationName: "UpdateProfile", + variables: { + input: { + ...props, + email: props.email, + }, + }, + }, + { headers: { cookie } }, + ); + + return { + "@id": updatedUser?.userId ?? updatedUser.id, + email: updatedUser.email, + givenName: updatedUser?.firstName, + familyName: updatedUser?.lastName, + taxID: updatedUser?.document?.replace(/[^\d]/g, ""), + gender: updatedUser?.gender === "female" + ? "https://schema.org/Female" + : "https://schema.org/Male", + telephone: updatedUser?.homePhone, + birthDate: updatedUser?.birthDate, + corporateName: updatedUser?.tradeName, + corporateDocument: updatedUser?.corporateDocument, + businessPhone: updatedUser?.businessPhone, + isCorporate: updatedUser?.isCorporate, + customFields: updatedUser?.customFields, + }; + } catch (error) { + console.error("Error updating user profile:", error); + return null; + } +}; + +export default updateProfile; diff --git a/vtex/actions/review/submit.ts b/vtex/actions/review/submit.ts new file mode 100644 index 000000000..a42f0b617 --- /dev/null +++ b/vtex/actions/review/submit.ts @@ -0,0 +1,44 @@ +import { getCookies } from "std/http/cookie.ts"; +import { AppContext } from "../../../vtex/mod.ts"; +import { VTEX_ID_CLIENT_COOKIE } from "../../utils/vtexId.ts"; + +export interface Props { + data: { + productId: string; + rating: number; + title: string; + text: string; + reviewerName: string; + approved: boolean; + }; +} + +// docs https://developers.vtex.com/docs/api-reference/reviews-and-ratings-api#post-/reviews-and-ratings/api/review?endpoint=post-/reviews-and-ratings/api/review + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +) => { + const { data } = props; + const cookies = getCookies(req.headers); + const authCookie = cookies[VTEX_ID_CLIENT_COOKIE] || + cookies[`${VTEX_ID_CLIENT_COOKIE}_${ctx.account}`]; + + const requestOptions = { + body: data, + headers: { + "accept": "application/json", + "content-type": "application/json", + "VtexidClientAutCookie": authCookie, + }, + }; + + const response = await ( + ctx.my[`POST /reviews-and-ratings/api/review`]({}, requestOptions) + ); + + return response.json(); +}; + +export default action; diff --git a/vtex/actions/sessions/delete.ts b/vtex/actions/sessions/delete.ts new file mode 100644 index 000000000..e55b77e31 --- /dev/null +++ b/vtex/actions/sessions/delete.ts @@ -0,0 +1,39 @@ +import { AppContext } from "../../mod.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +export interface DeleteSession { + logOutFromSession: string; +} + +interface Props { + sessionId: string; +} + +async function loader( + { sessionId }: Props, + req: Request, + ctx: AppContext, +): Promise { + const { io } = ctx; + const { cookie, payload } = parseCookie(req.headers, ctx.account); + + if (!payload?.sub || !payload?.userId) { + return null; + } + + const mutation = `mutation LogOutFromSession($sessionId: ID) { + logOutFromSession(sessionId: $sessionId) @context(provider: "vtex.store-graphql@2.x") + }`; + + try { + return await io.query({ + query: mutation, + variables: { sessionId }, + }, { headers: { cookie } }); + } catch (e) { + console.error(e); + return null; + } +} + +export default loader; diff --git a/vtex/actions/trigger.ts b/vtex/actions/trigger.ts new file mode 100644 index 000000000..a69b78828 --- /dev/null +++ b/vtex/actions/trigger.ts @@ -0,0 +1,46 @@ +import { AppContext } from "../mod.ts"; + +export interface VTEXNotificationPayload { + /** @description SKU ID in VTEX **/ + IdSku: string; + /** @description Seller’s account name in VTEX, shown in the store’s VTEX Admin url. **/ + An: string; + /** @description Affiliate ID generated automatically in the configuration. **/ + IdAffiliate: string; + /** @description Product ID in VTEX **/ + ProductId: number; + /** @description Date when the item was updated **/ + DateModified: string; + /** @description Identifies whether the product is active or not. In case it is “false”, it means the product was deactivated in VTEX and should be blocked in the marketplace. We recommend that the inventory level is zeroed in the marketplace, and the product is blocked. In case the marketplace doesn’t allow it to be deactivated, the product should be excluded, along with any existing correspondences in the connector. **/ + IsActive: boolean; + /** @description Identifies that the inventory level has been altered. Connectors should send an Fulfillment Simulation request to collect updated information. **/ + StockModified: boolean; + /** @description Identifies that the price has been altered. Connectors should send an Fulfillment Simulation request to collect updated information. **/ + PriceModified: boolean; + /** @description Identifies that the product/SKU registration data has changed, like name, description, weight, etc **/ + HasStockKeepingUnitModified: boolean; + /** @description Identifies that the product is no longer associated with the trade policy. In case the marketplace doesn’t allow it to be deactivated, the product should be excluded, along with any existing correspondences in the connector. **/ + HasStockKeepingUnitRemovedFromAffiliate: boolean; +} + +const action = async ( + props: VTEXNotificationPayload, + _req: Request, + ctx: AppContext, +): Promise<{ id: string }> => { + const { IdSku } = props; + + if (!IdSku) { + throw new Error("Missing idSKU"); + } + + const response = await ctx.invoke("workflows/actions/start.ts", { + // @ts-expect-error vtex trigger is on generated type + key: "vtex-trigger", + args: [props], + }); + + return { id: response.id }; +}; + +export default action; diff --git a/vtex/actions/wishlist/addItem.ts b/vtex/actions/wishlist/addItem.ts new file mode 100644 index 000000000..fdc2ce1d2 --- /dev/null +++ b/vtex/actions/wishlist/addItem.ts @@ -0,0 +1,37 @@ +import { AppContext } from "../../mod.ts"; +import type { WishlistItem } from "../../utils/types.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +export interface Props { + productId: string; + sku: string; +} + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { io } = ctx; + const { cookie, payload } = parseCookie(req.headers, ctx.account); + const user = payload?.sub; + + if (!user) { + return []; + } + + await io.query({ + operationName: "AddToWishlist", + variables: { + name: "Wishlist", + shopperId: user, + listItem: props, + }, + query: + `mutation AddToWishlist($listItem: ListItemInputType!, $shopperId: String!, $name: String!, $public: Boolean) { addToList(listItem: $listItem, shopperId: $shopperId, name: $name, public: $public) @context(provider: "vtex.wish-list@1.x") }`, + }, { headers: { cookie } }); + + return ctx.invoke.vtex.loaders.wishlist({ allRecords: true }); +}; + +export default action; diff --git a/vtex/actions/wishlist/removeItem.ts b/vtex/actions/wishlist/removeItem.ts new file mode 100644 index 000000000..ecdb51987 --- /dev/null +++ b/vtex/actions/wishlist/removeItem.ts @@ -0,0 +1,36 @@ +import wishlistLoader from "../../loaders/wishlist.ts"; +import { AppContext } from "../../mod.ts"; +import type { WishlistItem } from "../../utils/types.ts"; +import { parseCookie } from "../../utils/vtexId.ts"; + +export type Props = { id: string }; + +const action = async ( + props: Props, + req: Request, + ctx: AppContext, +): Promise => { + const { io } = ctx; + const { cookie, payload } = parseCookie(req.headers, ctx.account); + const user = payload?.sub; + const { id } = props; + + if (!user) { + return []; + } + + await io.query({ + operationName: "RemoveFromList", + variables: { + name: "Wishlist", + shopperId: user, + id, + }, + query: + `mutation RemoveFromList($id: ID!, $shopperId: String!, $name: String) { removeFromList(id: $id, shopperId: $shopperId, name: $name) @context(provider: "vtex.wish-list@1.x") }`, + }, { headers: { cookie } }); + + return wishlistLoader({ count: Infinity }, req, ctx); +}; + +export default action; diff --git a/vtex/components/VTEXPortalDataLayerCompatibility.tsx b/vtex/components/VTEXPortalDataLayerCompatibility.tsx new file mode 100644 index 000000000..d9ca176d3 --- /dev/null +++ b/vtex/components/VTEXPortalDataLayerCompatibility.tsx @@ -0,0 +1,200 @@ +import { type JSX } from "preact"; +import { Product } from "../../commerce/types.ts"; +import { useScriptAsDataURI } from "@deco/deco/hooks"; +declare global { + interface Window { + // deno-lint-ignore no-explicit-any + datalayer_product: any; + shelfProductIds: string[]; + skuJson: ProductSKUJsonProps; + dataLayer: unknown[]; + } +} +type ScriptProps = JSX.HTMLAttributes; +function addVTEXPortalDataSnippet(accountName: string) { + performance.mark("start-vtex-dl"); + const url = new URL(globalThis.window.location.href); + const structuredDataScripts = + document.querySelectorAll('script[type="application/ld+json"]') || []; + // deno-lint-ignore no-explicit-any + const structuredDatas: Record[] = []; + // deno-lint-ignore no-explicit-any + structuredDataScripts.forEach((v: any) => { + structuredDatas.push(JSON.parse(v.text)); + }); + const breadcrumbSD = structuredDatas.find((s) => + s["@type"] === "BreadcrumbList" + ); + performance.mark("end-sd"); + // deno-lint-ignore no-explicit-any + const getPageType = (structuredData: undefined | Record) => { + if (url.pathname === "/") { + return "homeView"; + } + const isProductPage = structuredDatas.some((s) => s["@type"] === "Product"); + if (isProductPage) { + return "productView"; + } + const isSearchPage = url.pathname === "/s"; + if (isSearchPage) { + return "internalSiteSearchView"; + } + if (structuredData?.itemList?.length === 1) { + return "departmentView"; + } + if (structuredData?.itemList?.length >= 2) { + return "categoryView"; + } + return "otherView"; + }; + const pageType = getPageType(breadcrumbSD); + // deno-lint-ignore no-explicit-any + const props: Record = { + pageCategory: "Home", + pageDepartment: null, + pageUrl: globalThis.window.location.href, + pageTitle: document.title, + skuStockOutFromShelf: [], + skuStockOutFromProductDetail: [], + accountName: `${accountName}`, + pageFacets: [], + shelfProductIds: [], + }; + const department = breadcrumbSD?.itemListElement?.[0]; + if (pageType === "productView") { + props.pageCategory = "Product"; + props.pageDepartment = department?.name || null; + const product = globalThis.window.datalayer_product || {}; + Object.assign(props, product); + } + if (pageType === "departmentView") { + props.pageCategory = "Department"; + props.pageDepartment = department?.name || null; + props.departmentName = department?.name || null; + props.categoryName = department?.name || null; + } + const category = breadcrumbSD?.itemListElement?.[1]; + if (pageType === "categoryView") { + props.pageCategory = "Category"; + props.pageDepartment = department?.name || null; + props.categoryName = category?.name || null; + } + if (pageType === "internalSiteSearchView") { + props.pageCategory = "InternalSiteSearch"; + props.siteSearchTerm = url.searchParams.get("q"); + } + if (pageType === "otherView") { + const pathNames = url.pathname.split("/").filter(Boolean); + props.pageCategory = pathNames.pop() || null; + } + props.shelfProductIds = globalThis.window.shelfProductIds || []; + globalThis.window.dataLayer = globalThis.window.dataLayer || []; + // VTEX Default position is first... + globalThis.window.dataLayer.unshift(props); + // But GTM handles .push function + globalThis.window.dataLayer.push(props); + globalThis.window.dataLayer.push({ event: pageType }); + performance.mark("end-vtex-dl"); + performance.measure("vtex-dl-qs-ld-json", "start-vtex-dl", "end-sd"); + performance.measure("vtex-dl-compat", "start-vtex-dl", "end-vtex-dl"); +} +interface AddVTEXPortalData extends ScriptProps { + accountName: string; +} +export function AddVTEXPortalData( + { accountName, ...props }: AddVTEXPortalData, +) { + return ( +