diff --git a/docs/content/guides/fulfillment-api.md b/docs/content/guides/fulfillment-api.md new file mode 100644 index 0000000000..93108e558e --- /dev/null +++ b/docs/content/guides/fulfillment-api.md @@ -0,0 +1,71 @@ +--- +title: Fulfillment API +--- + +## **Introduction** + +This guide will give an overview of Medusa's Fulfillment API and how it can be implemented to work with different fulfillment providers. Before digging deeper into the API it should be clarified what is meant by a fulfillment provider; in Medusa a fulfillment provider is typically a 3rd party service that can handle order data for the purpose of shipping the items in the order to a customer. Examples of fulfillment providers are: a carrier like UPS, a logistics platform like ShipBob or a 3PL solution. + +Implementations of the Fulfillment API can be distributed as npm packages for easy installation through the plugin system, there are already a couple of examples of fulfillment plugins in the Medusa monorepo, you can identify these by looking for `medusa-fulfillment-*`. For further details on building and publishing plugins [please check this guide](https://docs.medusa-commerce.com/how-to/plugins). + +## Fulfillment Service Interface + +The fulfillment service interface can be found in the [`medusa-interfaces` package](https://github.com/medusajs/medusa/blob/master/packages/medusa-interfaces/src/fulfillment-service.js) where you can see the full list of methods that can be implemented. The methods in the interface allow you to implement plugins that can calculate shipping rates, create fulfillments in other systems, create return labels, retrieve customs documents and more. + +In this guide we will focus on the methods involved in a typical fulfillment flow i.e. creating a shipping option, adding a shipping method to a cart, creating a fulfillment of items in a cart and finally marking the fulfillment as shipped. The methods involved in this flow are: + +```jsx +getFulfillmentOptions() +validateOption(optionData) +validateFulfillmentData(fulfillmentData, cart) +createFulfillment(fulfillmentData, fulfillmentItems, order, fulfillment) +``` + +The figure below shows at what points in the flow the Fulfillment API is called. + +![Fulfillment Flow](https://user-images.githubusercontent.com/7554214/133107092-981505ea-230a-4399-9d95-3f2ec59a7dcc.png) + +- **Get fulfillment options for Region** + + In Medusa Shipping Options are defined and configured for each region - this allows store operators to granularly control how orders can be fulfilled in different countries. When creating a shipping option on a Region the Fulfillment API calls `getFulfillmentOptions` which will return the ways in which a fulfillment provider can process an order. Let's imagine that you have built a UPS plugin that can be used to fulfill orders with UPS; in this case `getFulfillmentOptions` might respond with UPS Express Shipping and UPS Access Point. + +- **Create Shipping Option** + + When creating the Shipping Option in Medusa, a store operator selects which underlying fulfillment option will be used when customers create Orders with the Shipping Option. To build from the UPS example a store operator may select the UPS Access Point option from the list of fulfillment options, she will then add an appropriate name for the Shipping Option, set the Shipping Option's price and if needed adjust the requirements that must be met for the Shipping Option to be active (e.g. minimum cart subtotal). Before the Shipping Option is created, `validateOption` will be called to ensure that the selected fulfillment option is in fact valid and correctly formatted. `validateOption` should respond with the validated fulfillment option; this also provides a point at which you may add additional details to the fulfillment data before the Shipping Option is created. + +- **Get Shipping Options for Cart** + + Moving to the customer perspective it is assumed that a cart has been created, items have been added to the cart and the customer is now looking towards selecting the Shipping Option they wish to have their Order fulfilled with. The first step from the storefront perspective is to retrieve the list of Shipping Options that are available for the Cart; these will be filtered based on the Region the Cart belongs to and the items that are in the cart. There are no calls to the Fulfillment API associated with the retrieval. + +- **Add Shipping Method to Cart** + + At this point the customer has selected a Shipping Option and may have configured additional details about the fulfillment. For example, building on the UPS example, the customer may have selected a Shipping Option that uses the UPS Access Point fulfillment option; in this case the customer sends an `access_point_id` in the `data` object of the `POST /store/carts/:id/shipping-methods` payload. At this point the Shipping Option becomes a Shipping Method, the distinction between the two is that a Shipping Option is a template for how a cart may be shipped whereas a Shipping Method is the instantiated way that the cart will be shipped (which may include specific details like the `access_point_id`). In order to ensure that the details that the customer selects are valid the Fulfillment API's `validateFulfillmentData` is called, this is where a fulfillment implementation may check that the `access_point_id` is in fact a valid value, etc. + +- **Create Fulfillment** + + It is now assumed that the customer has completed their purchase with a given Shipping Method and the order has been confirmed. At this point we are ready to create the fulfillment in the 3rd party system. When a store operator (or an automation) attempts to fulfill an order the `createFulfillment` method will be called in the Fulfillment API, in our UPS example this method should then book a shipment via UPS's API. + Note: in Medusa you can make partial fulfillments of an order it is therefore important that the id sent to the 3rd party is unique by fulfillment and not only order. + +- **Mark as shipped** + + The final step of the fulfillment process is to mark the fulfillment as shipped. It is also at this stage that you would record a tracking number that can be shared with the customer. This step typically happens asynchronously through a webhook. + +## Other useful methods + +The flow above describes the Fulfillment API in high level terms; check out the `[medusa-fulfillment-webshipper](https://github.com/medusajs/medusa/blob/master/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js)` plugin for a full implementation of the Fulfillment API. In the implementation you will find examples of all the method described above. + +### `createReturn` + +You will find that the Webshipper plugin has an implementation for `createReturn` which allows fulfillment providers to implement a way for generating return labels. When implemented this can be used to allow customers to create self managed returns or to integrate automatic return label generation in the admin dashboard. + +### `canCalculate(fulfillmentOption)` + +If implemented this method can be used to dynamically calcluate prices based on a cart's contents or details. The method returns a boolean value indicating if a given fulfillment option can have a dynamically calculated price or not. + +### `calculatePrice(fulfillmentOption, fulfillmentData, cart)` + +If the shipping option is configured to dynamically calculate the price of the this method will be called when Shipping Options are fetched for the Cart and when Shipping Methods are created on a Cart. + +## What's next? + +Now that you have an overview of the Fulfillment API you can start developing your own fulfillment plugin. For a guide on how to create plugins [check this guide](https://docs.medusa-commerce.com/how-to/plugins). If you have questions or issues please feel free to [join our Discord server](https://discord.gg/H6naACAK) for direct access to the engineering team. diff --git a/packages/medusa-cli/yarn.lock b/packages/medusa-cli/yarn.lock index 73ff3335db..7719cbaeba 100644 --- a/packages/medusa-cli/yarn.lock +++ b/packages/medusa-cli/yarn.lock @@ -1300,6 +1300,13 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-align@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" + integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== + dependencies: + string-width "^3.0.0" + ansi-escapes@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -1466,12 +1473,19 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== -axios@^0.21.1: - version "0.21.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" - integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== +axios-retry@^3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.1.9.tgz#6c30fc9aeb4519aebaec758b90ef56fa03fe72e8" + integrity sha512-NFCoNIHq8lYkJa6ku4m+V1837TP6lCa7n79Iuf8/AqATAHYB0ISaAS1eyIenDOfHOLtym34W65Sjke2xjg2fsA== dependencies: - follow-redirects "^1.10.0" + is-retry-allowed "^1.1.0" + +axios@^0.21.1: + version "0.21.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" + integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== + dependencies: + follow-redirects "^1.14.0" babel-jest@^25.5.1: version "25.5.1" @@ -1589,6 +1603,20 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +boxen@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.1.tgz#4faca6a437885add0bf8d99082e272d480814cd4" + integrity sha512-JtIQYts08AFAYGF4eSh3pUt3NQkYV/e75pRtQmAVTLNWR/1L7Bsswxlgzgk8nmLEM+gFszsIlA9BgD3XnSqp3g== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1693,6 +1721,11 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + caniuse-lite@^1.0.30001219: version "1.0.30001236" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001236.tgz#0a80de4cdf62e1770bb46a30d884fc8d633e3958" @@ -1780,6 +1813,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6" + integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -1797,6 +1835,11 @@ clean-stack@^3.0.0: dependencies: escape-string-regexp "4.0.0" +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -2660,10 +2703,10 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== -follow-redirects@^1.10.0: - version "1.13.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" - integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== +follow-redirects@^1.14.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" + integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== for-in@^1.0.2: version "1.0.2" @@ -3164,7 +3207,7 @@ is-docker@^2.0.0: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== -is-docker@^2.1.1: +is-docker@^2.1.1, is-docker@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== @@ -3263,6 +3306,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-retry-allowed@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -4043,6 +4091,20 @@ medusa-core-utils@^0.1.27: "@hapi/joi" "^16.1.8" joi-objectid "^3.0.1" +medusa-telemetry@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/medusa-telemetry/-/medusa-telemetry-0.0.3.tgz#c11e5e0f3cc969f3eaee41d1c24f78a5c0715362" + integrity sha512-Qb/sgOwO8t2Sjjo4nKyBa6hKZ/SjniT4eEWenygEaJDqXZhfogVYGhWc5gn4tLlFFNEHXzDTlrqX2LvzfEJWIw== + dependencies: + axios "^0.21.1" + axios-retry "^3.1.9" + boxen "^5.0.1" + ci-info "^3.2.0" + configstore "5.0.1" + is-docker "^2.2.1" + remove-trailing-slash "^0.1.1" + uuid "^8.3.2" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -4837,6 +4899,11 @@ remove-trailing-separator@^1.0.1: resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= +remove-trailing-slash@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d" + integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== + repeat-element@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" @@ -5324,7 +5391,7 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.0.0: +string-width@^4.0.0, string-width@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== @@ -5608,6 +5675,11 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -5733,6 +5805,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index 016a92fa55..20ed1bbe4c 100644 --- a/www/docs/sidebars.js +++ b/www/docs/sidebars.js @@ -77,5 +77,15 @@ module.exports = { }, ], }, + { + type: "category", + label: "Guides", + items: [ + { + type: "doc", + id: "guides/fulfillment-api", + }, + ], + }, ], }