diff --git a/README.md b/README.md
index f7786cdc8d..ac5f3b2616 100644
--- a/README.md
+++ b/README.md
@@ -49,24 +49,6 @@ Medusa is an open-source headless commerce engine that enables developers to cre
After these four steps and only a couple of minutes, you now have a complete commerce engine running locally. You may now explore [the documentation](https://docs.medusa-commerce.com/api) to learn how to interact with the Medusa API. You may also add [plugins](https://github.com/medusajs/medusa/tree/master/packages) to your Medusa store by specifying them in your `medusa-config.js` file.
-## ⭐️ Features
-Medusa comes with a set of building blocks that allow you to create amazing digital commerce experiences, below is a list of some of the features that Medusa come with out of the box:
-- **Headless**: Medusa is a highly customizable commerce API which means that you may use any presentation layer such as a website, app, chat bots, etc.
-- **Regions** allow you to specify currencies, payment providers, shipping providers, tax rates and more for one or more countries for truly international sales.
-- **Orders** come with all the functionality necessary to perform powerful customer service operations with ease.
-- **Carts** allow customers to collect products for purchase, add shipping details and complete payments.
-- **Products** come with relevant fields for customs, stock keeping and sales. Medusa supports multiple options and unlimited variants.
-- **Swaps** allow customers to exchange products after purchase (e.g. for incorrect sizes). Accounting, payment and fulfillment plugins handle all the tedious work for you for automated customer service.
-- **Claims** can be created if customers experience problems with one of their products. Plugins make sure to automate sending out replacements, handling refunds and collecting valuable data for analysis.
-- **Returns** allow customers to send back products and can be configured to function in a 100% automated flow through accounting and payment plugins.
-- **Fulfillment API** makes it easy to integrate with any fulfillment provider by creating fulfillment plugins, check the `/packages` directory for a full list of plugins.
-- **Payments API** makes it easy to integrate with any payment provider by creatingn payment plugins, we already support Stripe, Paypal and Klarna.
-- **Notification API** allow integrations with email providers, chat bots, Slack channels etc.
-- **Customer Login** to give customers a way of managing their data, viewing their orders and saving payment details.
-- **Shipping Options & Profiles** enable powerful rules for free shipping limits, multiple fulfillment methods and more.
-- **Medusa's Plugin Architecture** makes it intuitive and easy to manage your integrations, switch providers and grow with ease.
-- **Customization** is supported for those special use cases that all the other ecommerce platforms can't accommodate.
-
## 🛒 Setting up a storefront for your Medusa project
Medusa is a headless commerce engine which means that it can be used for any type of digital commerce experience - you may use it as the backend for an app, a voice application, social commerce experiences or a traditional e-commerce website, you may even want to integrate Medusa into your own software to enable commerce functionality. All of these are use cases that Medusa supports - to learn more read the documentation or reach out.
@@ -84,19 +66,23 @@ To provide a quick way to get you started with a storefront install one of our t
With your starter and your Medusa store running you can open http://localhost:8000 (for Gatsby) or http://localhost:3000 (for Nextjs) in your browser and view the products in your store, build a cart, add shipping details and pay and complete an order.
-## ☁️ Linking development to Medusa Cloud
-With your project in local development you can link your Medusa instance to Medusa Cloud - this will allow you to manage your store, view orders and test out the amazing functionalities that you are building. Linking your project to Medusa Cloud requires that you have a Medusa Cloud account.
-
-1. **Authenticate your CLI with Medusa Cloud:**
- ```
- medusa login
- ```
-2. **Link project**
- ```
- medusa link --develop
- ```
-
-You can now navigate to Orders in Medusa Cloud to view the orders in your local Medusa project, just like you would if your store was running in production.
+## ⭐️ Features
+Medusa comes with a set of building blocks that allow you to create amazing digital commerce experiences, below is a list of some of the features that Medusa come with out of the box:
+- **Headless**: Medusa is a highly customizable commerce API which means that you may use any presentation layer such as a website, app, chatbots, etc.
+- **Regions** allow you to specify currencies, payment providers, shipping providers, tax rates and more for one or more countries for truly international sales.
+- **Orders** come with all the functionality necessary to perform powerful customer service operations with ease.
+- **Carts** allow customers to collect products for purchase, add shipping details and complete payments.
+- **Products** come with relevant fields for customs, stock keeping and sales. Medusa supports multiple options and unlimited variants.
+- **Swaps** allow customers to exchange products after purchase (e.g. for incorrect sizes). Accounting, payment and fulfillment plugins handle all the tedious work for you for automated customer service.
+- **Claims** can be created if customers experience problems with one of their products. Plugins make sure to automate sending out replacements, handling refunds and collecting valuable data for analysis.
+- **Returns** allow customers to send back products and can be configured to function in a 100% automated flow through accounting and payment plugins.
+- **Fulfillment API** makes it easy to integrate with any fulfillment provider by creating fulfillment plugins, check the `/packages` directory for a full list of plugins.
+- **Payments API** makes it easy to integrate with any payment provider by creating payment plugins, we already support Stripe, Paypal and Klarna.
+- **Notification API** allow integrations with email providers, chatbots, Slack channels, etc.
+- **Customer Login** to give customers a way of managing their data, viewing their orders and saving payment details.
+- **Shipping Options & Profiles** enable powerful rules for free shipping limits, multiple fulfillment methods and more.
+- **Medusa's Plugin Architecture** makes it intuitive and easy to manage your integrations, switch providers and grow with ease.
+- **Customization** is supported for those special use cases that all the other e-commerce platforms can't accommodate.
## Database support
In production Medusa requires Postgres and Redis, but SQLite is supported for development and testing purposes. If you plan on using Medusa for a project it is recommended that you install Postgres and Redis on your dev machine.
diff --git a/docs-util/helpers/test-server.js b/docs-util/helpers/test-server.js
index 785c39fd7d..fcda9df353 100644
--- a/docs-util/helpers/test-server.js
+++ b/docs-util/helpers/test-server.js
@@ -1,34 +1,34 @@
-const path = require("path");
-const express = require("express");
-const getPort = require("get-port");
-const importFrom = require("import-from");
+const path = require("path")
+const express = require("express")
+const getPort = require("get-port")
+const importFrom = require("import-from")
const initialize = async () => {
- const app = express();
+ const app = express()
- const cwd = process.cwd();
- const loaders = importFrom(cwd, "@medusajs/medusa/dist/loaders").default;
+ const cwd = process.cwd()
+ const loaders = importFrom(cwd, "@medusajs/medusa/dist/loaders").default
const { dbConnection } = await loaders({
directory: path.resolve(process.cwd()),
expressApp: app,
- });
+ })
- const PORT = await getPort();
+ const PORT = await getPort()
return {
db: dbConnection,
app,
port: PORT,
- };
-};
+ }
+}
const setup = async () => {
- const { app, port } = await initialize();
+ const { app, port } = await initialize()
app.listen(port, (err) => {
- process.send(port);
- });
-};
+ process.send(port)
+ })
+}
-setup();
+setup()
diff --git a/docs-util/helpers/use-db.js b/docs-util/helpers/use-db.js
index d533ae5ba2..c451fe0cb0 100644
--- a/docs-util/helpers/use-db.js
+++ b/docs-util/helpers/use-db.js
@@ -1,26 +1,26 @@
-const { dropDatabase, createDatabase } = require("pg-god");
-const { createConnection } = require("typeorm");
+const { dropDatabase, createDatabase } = require("pg-god")
+const { createConnection } = require("typeorm")
-const path = require("path");
+const path = require("path")
const DbTestUtil = {
db_: null,
setDb: function (connection) {
- this.db_ = connection;
+ this.db_ = connection
},
clear: function () {
- return this.db_.synchronize(true);
+ return this.db_.synchronize(true)
},
shutdown: async function () {
- await this.db_.close();
- return dropDatabase({ databaseName });
+ await this.db_.close()
+ return dropDatabase({ databaseName })
},
-};
+}
-const instance = DbTestUtil;
+const instance = DbTestUtil
module.exports = {
initDb: async function ({ cwd }) {
@@ -33,19 +33,19 @@ module.exports = {
`dist`,
`migrations`
)
- );
+ )
- const databaseName = "medusa-fixtures";
- await createDatabase({ databaseName });
+ const databaseName = "medusa-fixtures"
+ await createDatabase({ databaseName })
const connection = await createConnection({
type: "postgres",
url: "postgres://localhost/medusa-fixtures",
migrations: [`${migrationDir}/*.js`],
- });
+ })
- await connection.runMigrations();
- await connection.close();
+ await connection.runMigrations()
+ await connection.close()
const modelsLoader = require(path.join(
cwd,
@@ -55,19 +55,19 @@ module.exports = {
`dist`,
`loaders`,
`models`
- )).default;
+ )).default
- const entities = modelsLoader({}, { register: false });
+ const entities = modelsLoader({}, { register: false })
const dbConnection = await createConnection({
type: "postgres",
url: "postgres://localhost/medusa-fixtures",
entities,
- });
+ })
- instance.setDb(dbConnection);
- return dbConnection;
+ instance.setDb(dbConnection)
+ return dbConnection
},
useDb: function () {
- return instance;
+ return instance
},
-};
+}
diff --git a/docs/content/guides/carts-in-medusa.md b/docs/content/guides/carts-in-medusa.md
new file mode 100644
index 0000000000..7d7e673954
--- /dev/null
+++ b/docs/content/guides/carts-in-medusa.md
@@ -0,0 +1,163 @@
+---
+title: Carts in Medusa
+---
+
+# Carts in Medusa
+
+In Medusa a Cart serves the purpose of collecting the information needed to create an Order, including what products to purchase, what address to send the products to and which payment method the purchase will be processed by.
+
+To create a cart using the `@medusajs/medusa-js` SDK you can use:
+
+```javascript
+const client = new Medusa({ baseUrl: "http://localhost:9000" })
+const { cart } = await client.carts.create()
+```
+
+A Cart will always belong to a Region and you may provide a `region_id` upon Cart creation. If no `region_id` is specified Medusa will assign the Cart to a random Region. Regions specify information about how the Cart should be taxed, what currency the Cart should be paid with and what payment and fulfillment options will be available at checkout. Below are some of the properties that can be found on the Cart response. For a full example of a Cart response [check our fixtures](https://github.com/medusajs/medusa/blob/docs/api/docs/api/fixtures/store/GetCartsCart.json).
+
+```json
+ "cart": {
+ "id": "cart_01FEWZSRFWT8QWMHJ7ZCPRP3BZ",
+ "email": null,
+ "billing_address": null,
+ "shipping_address": null,
+ "items": [ ... ],
+ "region": {
+ "id": "reg_01FEWZSRD7HVHBSQRC4KYMG5XM",
+ "name": "United States",
+ "currency_code": "usd",
+ "tax_rate": "0",
+ ...
+ },
+ "discounts": [],
+ "gift_cards": [],
+ "customer_id": null,
+ "payment_sessions": [],
+ "payment": null,
+ "shipping_methods": [],
+ "type": "default",
+ "metadata": null,
+ "shipping_total": 0,
+ "discount_total": 0,
+ "tax_total": 0,
+ "gift_card_total": 0,
+ "subtotal": 1000,
+ "total": 1000,
+ ...
+ }
+```
+
+## Adding products to the Cart
+
+Customers can add products to the Cart in order to start gathering the items that will eventually be purchased. In Medusa adding a product to a Cart will result in a _Line Item_ being generated. To add a product using the SDK use:
+
+```javascript
+const { cart } = await client.carts.lineItems.create(cartId, {
+ variant_id: "[id-of-variant-to-add]",
+ quantity: 1,
+})
+```
+
+The resulting response will look something like this:
+
+```json
+{
+ "cart": {
+ "id": "cart_01FEWZSRFWT8QWMHJ7ZCPRP3BZ",
+ "items": [
+ {
+ "id": "item_01FEWZSRMBAN85SKPCRMM30N6W",
+ "cart_id": "cart_01FEWZSRFWT8QWMHJ7ZCPRP3BZ",
+ "title": "Basic Tee",
+ "description": "Small",
+ "thumbnail": null,
+ "is_giftcard": false,
+ "should_merge": true,
+ "allow_discounts": true,
+ "has_shipping": false,
+ "unit_price": 1000,
+ "variant": {
+ "id": "variant_01FEWZSRDNWABVFZTZ21JWKHRG",
+ "title": "Small",
+ "product_id": "prod_01FEWZSRDHDDSHQV6ATG6MS2MF",
+ "sku": null,
+ "barcode": null,
+ "ean": null,
+ "upc": null,
+ "allow_backorder": false,
+ "hs_code": null,
+ "origin_country": null,
+ "mid_code": null,
+ "material": null,
+ "weight": null,
+ "length": null,
+ "height": null,
+ "width": null,
+ "metadata": null,
+ ...
+ },
+ "quantity": 1,
+ "metadata": {},
+ ...
+ }
+ ],
+ ...
+ }
+}
+```
+
+The properties stored on a Line Item are useful for explaining and displaying the contents of the Cart. For example, Line Items can have a thumbnail assigned which can be used to display a pack shot of the product that is being purchased, a title to show name the products in the cart and a description to give further details about the product. By default the Line Item will be generated with properties inherited from the Product that is being added to the Cart, but the behaviour can be customized for other purposes as well.
+
+## Adding Customer information to a Cart
+
+After adding products to the Cart, you should gather information about where to send the products, this is done using the `update` method in the SDK.
+
+```javascript
+const { cart } = await client.carts.update(cartId, {
+ email: "jane.doe@mail.com",
+ shipping_address: {
+ first_name: "Jane",
+ last_name: "Doe",
+ address_1: "4242 Hollywood Dr",
+ postal_code: "12345",
+ country_code: "us",
+ city: "Los Angeles",
+ region: "CA",
+ },
+})
+```
+
+Note that the country code in the shipping address must be the country code for one of the countries in a Region - otherwise this method will fail.
+
+## Initializing Payment Sessions
+
+In Medusa payments are handled through the long lived entities called _Payment Sessions_. Payment Sessions cary provider specific data that can later be used to authorize the payments, which is the step required before an order can be created. The SDK provides a `createPaymentSessions` method that can be used to initialize the payment sessions with the Payment Providers available in the Region.
+
+```javascript
+const { cart } = await client.carts.createPaymentSessions(cartId)
+```
+
+You can read more about Payment Sessions in our [guide to checkouts](https://docs.medusa-commerce.com/guides/checkouts).
+
+## Changing the Cart region
+
+To update the Region that the cart belongs to you should also use the `update` method from the SDK.
+
+```javascript
+const { cart } = await client.carts.update(cartId, {
+ region_id: "[id-of-region-to-switch-to]",
+})
+```
+
+When changing the Cart region you should be aware of a couple of things:
+
+- If switching to a Region with a different currency the line item prices and cart totals will change
+- If switching to a Region with a different tax rate prices and totals will change
+- If switching to a Region serving only one country the `shipping_address.country_code` will automatically be set to that country
+- If the Cart already has initialized payment sessions all of these will be canceled and a new call to `createPaymentSessions` will have to be made
+
+## What's next?
+
+Carts are at the core of the shopping process in Medusa and provide all the necessary functionality to gather products for purchase. If you want to read a more detailed guide about how to complete checkouts please go to our [Checkout Guide](https://docs.medusa-commerce.com/guides/checkout).
+
+If you have questions or issues feel free to reach out via our [Discord server](https://discord.gg/xpCwq3Kfn8) for direct access to the Medusa engineering team.
diff --git a/docs/content/guides/checkouts.md b/docs/content/guides/checkouts.md
new file mode 100644
index 0000000000..6773952146
--- /dev/null
+++ b/docs/content/guides/checkouts.md
@@ -0,0 +1,107 @@
+---
+title: Checkouts
+---
+
+# Checkouts
+
+## Introduction
+The purpose of this guide is to describe a checkout flow in Medusa. It is assumed that you've completed our [Quickstart](https://docs.medusa-commerce.com/quickstart/quick-start) or [Tutorial](https://docs.medusa-commerce.com/tutorial/set-up-your-development-environment) and are familiar with the technologies we use in our stack. Additionally, having an understanding of the [core API](https://docs.medusa-commerce.com/api/store/auth) would serve as a great foundation for this walkthrough.
+> All code snippets in the following guide, use the JS SDK distributed through **npm**. To install it, run `yarn add @medusajs/medusa-js` or `npm install @medusajs/medusa-js`.
+
+## Glossary
+- **Cart**: The Cart contains all the information needed for customers to complete an Order. In the Cart customers gather the items they wish to purchase, they add shipping and billing details and complete payment information.
+- **LineItem**: Line Items represent an expense added to a Cart. Typically this will be a Product Variant and a certain quantity of the same variant. Line Items hold descriptive fields that help communicate its contents and price.
+- **PaymentSession**: A Payment Session is a Medusa abstraction that unifies the APIs of multiple payment gateways. Payment Sessions are _initialized_ when the customer begins a checkout and are _authorized_ prior to an Order being placed. Payment Sessions are created based on the available Payment Providers configured in a Cart's region.
+- **ShippingOption**: A Shipping Option represents a way in which an Order can be fulfilled. Shipping Options have a price and are associated with a Fulfillment Provider that will handle the shipment later in the Order flow. Once a customer selects a Shipping Option it becomes a Shipping Method.
+- **ShippingMethod**: Shipping Methods are unique to each Cart and can thereby hold either overwrites for fields in a Shipping Option (e.g. price) or additional details (e.g. an id representing a parcel pickup location).
+
+## Checkout flow
+To create an order from a cart, we go through the following flow.
+> At this point, it assumed that the customer has created a cart, added items and is now at the initial step of the checkout flow.
+
+#### Initializing the checkout
+The first step in the flow is to _initialize_ the configured Payment Sessions for the Cart. If you are using the `medusa-starter-default` starter, this call will result in the `cart.payment_sessions` array being filled with one Payment Session for the manual payment provider.
+
+```javascript=
+const { cart } = await medusa.carts.createPaymentSessions("cart_id")
+```
+
+To give a more real life example, it is assumed that `medusa-payment-stripe` is installed in your project in which case the call will result in a [Stripe PaymentIntent](https://stripe.com/docs/api/payment_intents) being created. The unique provider data for each Payment Session can be found in the Payment Session's `data` field; this data can be used in front end implementations e.g. if using Stripe Elements the `client_secret` can be retrieved through `session.data.client_secret`.
+
+#### Adding customer information
+After initializing the checkout flow, you would typically have one big step or several smaller steps for gathering user information; email, address, country, and more. To store this data you may update the cart with each of field or all fields at the same time.
+
+```javascript=
+const { cart } = await medusa.carts.update("cart_id", {
+ email: "lebron@james.com",
+ shipping_address: {
+ first_name: "",
+ last_name: "",
+ ...
+ }
+})
+```
+
+#### Selecting payment provider
+This step is only applicable, if you have multiple Payment Sessions installed in your project. In cases where only one Payment Provider is configured the Payment Session will be preselected. In all other cases your implementations should call:
+
+```javascript=
+const { cart } = await medusa.carts.setPaymentSession("cart_id", {
+ provider_id: "stripe"
+})
+```
+
+#### Choosing a shipping method
+Before reaching the payment step, you would typically require the customer to choose a Shipping Method from a number of options. In Medusa you can set rules that must be met for a Shipping Option to be available for a Cart. To get the available shipping options for a Cart you should call:
+```javascript=
+const { shipping_options } = await medusa.carts.listCartOptions("cart_id")
+```
+
+Choosing a Shipping Option, will create a Shipping Method and attach it to the Cart. The second argument to the function in the snippet below holds the id of the selected option.
+```javascript=
+const { cart } = await medusa.carts.addShippingMethod("cart_id", { option_id: "option_id"})
+```
+
+#### Collecting payment details
+The following snippet shows, how we use Stripe to collect payment details from the customer. Note that we are using the `client_secret` from the Stripe PaymentIntent in `data` on the payment session as this is required by Stripe Elements.
+```javascript=
+import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
+
+...
+
+const stripe = useStripe();
+const elements = useElements();
+
+
+const handleSubmit = () => {
+ ...
+ const { paymentIntent, error } = await stripe.confirmCardPayment(
+ cart.payment_session.data.client_secret,
+ {
+ payment_method: {
+ card: elements.getElement(CardElement),
+ },
+ }
+ );
+ ...
+}
+
+return
+```
+After collecting the payment details, the customer can complete the checkout flow.
+
+#### Completing the cart
+When all relevant customer information has been captured, your implementation should proceed to the final step; completing the cart.
+```javascript=
+const { order } = await medusa.carts.complete("cart_id")
+```
+If all information is collected correctly throughout the checkout flow, the call will place an Order based on the details gathered in the Cart.
+
+## Summary
+You now have a solid foundation for creating your own checkout flows using Medusa. Throughout this guide, we've used Stripe as a Payment Provider. Stripe is one of the most popular providers and we have an official plugin, that you can easily install in your project.
+
+## What's next?
+See the checkout flow, explained in the previous sections, in one of our frontend starters:
+- [Nextjs Starter](https://github.com/medusajs/nextjs-starter-medusa)
+- [Gatsby Starter](https://github.com/medusajs/gatsby-starter-medusa)
+
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.
+
+
+
+- **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/docs/content/how-to/create-medusa-app.md b/docs/content/how-to/create-medusa-app.md
new file mode 100644
index 0000000000..a3394df3c0
--- /dev/null
+++ b/docs/content/how-to/create-medusa-app.md
@@ -0,0 +1,117 @@
+---
+title: Using create-medusa-app
+---
+# Using create-medusa-app
+With the new `create-medusa-app` tool you will get your [Medusa](https://github.com/medusajs/medusa) development environment ready within a couple of minutes. After completion, you will have a Medusa backend, a Gatsby or Next.js storefront, and an admin dashboard up and running on your local machine.
+
+Starting a new e-commerce project just got easier, now with one command.
+
+## Getting started with `create-medusa-app`
+
+Use `create-medusa-app` with your preferred package manager:
+
+```bash
+yarn create medusa-app
+
+npx create-medusa-app
+```
+
+Behind the scenes, `create-medusa-app` is populating your database with some initial set of mock data, which helps to interact with Medusa setup intuitively straight away.
+
+Right after hitting one of those commands, the multistep installation process will be initiated, so the starter can be shaped right for the specific needs.
+
+### Destination folder
+
+Enter the path to the directory that will become the root of your Medusa project:
+
+```bash
+? Where should your project be installed? › my-medusa-store
+```
+
+### Pick the starter you prefer
+
+```bash
+? Which Medusa starter would you like to install? …
+❯ medusa-starter-default
+ medusa-starter-contentful
+ Other
+```
+
+You will be presented with three options:
+
+- `medusa-starter-default` is the most lightweight version of a Medusa project
+- `medusa-starter-contentful` almost like the default starter, but with `medusa-plugin-contentful` preinstalled
+- `Other` if you have a different starter that you would wish to install from `Other` will give you the option of providing a URL to that starter. An additional question will be asked if you choose this option:
+
+ ```bash
+ Where is the starter located? (URL or path) › https://github.com/somecoolusername/my-custom-medusa-starter
+ ```
+
+For the walkthrough purposes, we assume that the selected starter is `medusa-starter-default` and proceed to the next step.
+
+### Selecting a Storefront
+
+After selecting your Medusa starter you will be given the option to install one of our storefront starters. At the moment we have starters for Gatsby and Next.js:
+
+```bash
+Which storefront starter would you like to install? …
+❯ Gatsby Starter
+ Next.js Starter
+ None
+```
+
+You may also select `None` if the choice is to craft a custom storefront for your product.
+
+`create-medusa-app` now has all of the info necessary for the installation to begin.
+
+```bash
+Creating new project from git: https://github.com/medusajs/medusa-starter-default.git
+✔ Created starter directory layout
+Installing packages...
+```
+
+Once the installation has been completed you will have a Medusa backend, a demo storefront, and an admin dashboard.
+
+## What's inside
+
+Inside the root folder which was specified at the beginning of the installation process the following structure could be found:
+
+```bash
+/my-medusa-store
+ /storefront // Medusa storefront starter
+ /backend // Medusa starter as a backend option
+ /admin // Medusa admin panel
+```
+
+`create-medusa-app` prints out the commands that are available to you after installation. When each project is started you can visit your storefront, complete the order, and view the order in Medusa admin.
+
+```bash
+⠴ Installing packages...
+✔ Packages installed
+Initialising git in my-medusa-store/admin
+Create initial git commit in my-medusa-store/admin
+
+ Your project is ready 🚀. The available commands are:
+
+ Medusa API
+ cd my-medusa-store/backend
+ yarn start
+
+ Admin
+ cd my-medusa-store/admin
+ yarn start
+
+ Storefront
+ cd my-medusa-store/storefront
+ yarn start
+```
+
+## **What's next?**
+
+To learn more about Medusa to go through our docs to get some inspiration and guidance for the next steps and further development:
+
+- [Find out how to set up a Medusa project with Gatsby and Contentful](https://docs.medusa-commerce.com/how-to/headless-ecommerce-store-with-gatsby-contentful-medusa)
+- [Move your Medusa setup to the next level with some custom functionality](https://docs.medusa-commerce.com/tutorial/adding-custom-functionality)
+- [Create your own Medusa plugin](https://docs.medusa-commerce.com/how-to/plugins)
+
+If you have any follow-up questions or want to chat directly with our engineering team we are always happy to meet you at our [Discord](https://discord.gg/DSHySyMu).
diff --git a/docs/content/how-to/plugins.md b/docs/content/how-to/plugins.md
index 83dd41d30c..8d8c837123 100644
--- a/docs/content/how-to/plugins.md
+++ b/docs/content/how-to/plugins.md
@@ -10,7 +10,7 @@ The purpose of this guide is to give an introduction to the structure of a plugi
Plugins offer a way to extend and integrate the core functionality of Medusa.
-In most commerce solutions, you can extend the basic features but it often comes with the expense of having to build standalone web applications. Our architecture is built such that plugins run within the same process as the core eliminating the need for extra server capacaity, infrastructure and maintenance. As a result, the plugins can use all other services as dependencies and access the database.
+In most commerce solutions, you can extend the basic features but it often comes with the expense of having to build standalone web applications. Our architecture is built such that plugins run within the same process as the core eliminating the need for extra server capacity, infrastructure and maintenance. As a result, the plugins can use all other services as dependencies and access the database.
> You will notice that plugins vary in naming. The name should signal what functionality they provide.
diff --git a/docs/content/how-to/setting-up-a-nextjs-storefront-for-your-medusa-project.md b/docs/content/how-to/setting-up-a-nextjs-storefront-for-your-medusa-project.md
new file mode 100644
index 0000000000..e0b8b8b891
--- /dev/null
+++ b/docs/content/how-to/setting-up-a-nextjs-storefront-for-your-medusa-project.md
@@ -0,0 +1,54 @@
+---
+title: Setting up a Next.js storefront for your Medusa project
+---
+
+# Setting up a Next.js storefront for your Medusa project
+
+> Medusa is a headless open source commerce platform giving engineers the foundation for building unique and scaleable digital commerce projects through our API-first engine.
+> Being headless, our starters serve as a good foundation for you to get coupled with a frontend in a matter of minutes.
+
+This article assumes you already have the Medusa project created and ready to be linked to your Next.js starter.
+
+## Getting started
+
+In order to get started let's open the terminal and use the following command to create an instance of your storefront:
+
+```zsh
+ npx create-next-app -e https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront
+```
+
+Now we have a storefront codebase that is ready to be used with our Medusa server.
+
+Next, we have to complete two steps to make our new shiny storefront to speak with our server: **link storefront to a server** and **update the `STORE_CORS` variable**.
+
+Let's jump to these two.
+
+## Link storefront to a server
+
+For this part, we should navigate to a `client.js` file which you can find in the utils folder.
+
+We don't need to do much in here, but to make sure that our storefront is pointing to the port, where the server is running
+
+```js
+import Medusa from "@medusajs/medusa-js"
+const BACKEND_URL = process.env.GATSBY_STORE_URL || "http://localhost:9000" // <--- That is the line we are looking for
+export const createClient = () => new Medusa({ baseUrl: BACKEND_URL })
+```
+
+By default the Medusa server is running at port 9000, so if you didn't change that we are good to go to our next step.
+
+## Update the `STORE_CORS` variable
+
+Here let's navigate to your Medusa server and open `medusa-config.js`
+
+Let's locate the `STORE_CORS` variable and make sure it's the right port (which is 3000 by default for Next.js projects)
+
+```js
+/*
+ * CORS to avoid issues when consuming Medusa from a client.
+ * Should be pointing to the port where the storefront is running.
+ */
+const STORE_CORS = process.env.STORE_CORS || "http://localhost:3000"
+```
+
+Now we have a storefront that interacts with our Medusa server and with that we have a sweet and complete e-commerce setup with a Next.js storefront.
diff --git a/docs/content/quickstart/quick-start.md b/docs/content/quickstart/quick-start.md
index e39c21faf8..fcc16e9475 100644
--- a/docs/content/quickstart/quick-start.md
+++ b/docs/content/quickstart/quick-start.md
@@ -32,6 +32,6 @@ We have created two starters for you that can help you lay a foundation for your
- [Nextjs Starter](https://github.com/medusajs/nextjs-starter-medusa)
- [Gatsby Starter](https://github.com/medusajs/gatsby-starter-medusa)
-### Link you local development to Medusa Cloud (Coming soon!)
+
diff --git a/docs/content/tutorial/0-set-up-your-development-environment.md b/docs/content/tutorial/0-set-up-your-development-environment.md
index a93bccea3c..48583f1b00 100644
--- a/docs/content/tutorial/0-set-up-your-development-environment.md
+++ b/docs/content/tutorial/0-set-up-your-development-environment.md
@@ -10,7 +10,7 @@ Welcome to Medusa - we are so excited to get you on board!
This tutorial will walk you through the steps to take to set up your local development environment. You will familiarize yourself with some of the core parts that make Medusa work and learn how to configure your development environment. Furthermore you will be introduced to how the plugin architecture works and how to customize your commerce functionalities with custom logic and endpoints.
-As a final part of the tutorial you will be linking your local project to Medusa Cloud where you can leverage advanced tools that make it easy to develop, test and deploy your Medusa project.
+
## Background Knowledge and Prerequisites
@@ -107,11 +107,11 @@ If you don't already have a text editor of choice you should find one you like -
It is not important which editor you use as long as you feel comfortable working with it.
-## Medusa Cloud account
+
## Summary
diff --git a/docs/content/tutorial/2-adding-custom-functionality.md b/docs/content/tutorial/2-adding-custom-functionality.md
index 5e11de11f0..e04d84303a 100644
--- a/docs/content/tutorial/2-adding-custom-functionality.md
+++ b/docs/content/tutorial/2-adding-custom-functionality.md
@@ -6,7 +6,7 @@ title: 2. Adding custom functionality
## Introduction
-In the previous part of the tutorial we set up your Medusa project using the `medusa new` and started your Medusa server locally. In this part we will start adding some custom functionality that extends the core. In particular this tutorial will take you through adding custom serverices, custom endpoints and subscribers. The custom functionality that we will be adding will create an endpoint called `/welcome/:cart_id` which customers of your store can use to opt-in to receiving a welcome in their email inbox after completing their order.
+In the previous part of the tutorial we set up your Medusa project using the `medusa new` and started your Medusa server locally. In this part we will start adding some custom functionality that extends the core. In particular this tutorial will take you through adding custom services, custom endpoints and subscribers. The custom functionality that we will be adding will create an endpoint called `/welcome/:cart_id` which customers of your store can use to opt-in to receiving a welcome in their email inbox after completing their order.
The custom functionality will do a number of things:
@@ -56,7 +56,7 @@ In the constructor we specify that our `WelcomeService` will depend upon the `ca
### `registerOptin`
-The `registerOption` function will take to arguments: `cartId` and `optin`, where `cartId` holds the id of the cart that we wish to register optin for and `optin` is a boolean to indicate if the customer has accepted or optin or not. We will save the `optin` preferences in the cart's `metadata` field, so that it can be persisted for the future when we need to evaluate if we should send the welcome or not.
+The `registerOption` function will take two arguments: `cartId` and `optin`, where `cartId` holds the id of the cart that we wish to register optin for and `optin` is a boolean to indicate if the customer has accepted or optin or not. We will save the `optin` preferences in the cart's `metadata` field, so that it can be persisted for the future when we need to evaluate if we should send the welcome or not.
```javascript
async registerOptin(cartId, optin) {
@@ -111,7 +111,7 @@ In the above implementation we are first retrieving the order that we need to ch
After retrieving the order we list all orders that have the same `customer_id` as the order we just retrieved. We are only interested in the count of these orders so it is sufficient for us to just select the ids of these orders.
-We then check if the number of previous orders is 0, indicating that the customer has not previously purchased anything from our store. If the number of previous orders is greater than 0 we can exit our function prematurely as we only send welcomes to new customers.
+We then check if the number of previous orders is greater than 1, indicating that the customer has previously purchased anything from our store. If the number of previous orders is greater than 1 we can exit our function prematurely as we only send welcomes to new customers.
The final part of the implementation checks if the `welcome_optin` metadata has been set to true. If the customer has opted in we use `someEmailService.send` to trigger and email dispatch to the email stored on the order. In this case `someEmailSender` could be any email service for example Sendgrid, SES, Mailgun, etc.
@@ -252,4 +252,4 @@ You have now learned how to add custom functionality to your Medusa server, whic
You have now been introduced to many of the key parts of Medusa and with your knowledge of customization you can now begin creating some really powerful commerce experiences. If you have an idea for a cool customization go ahead and make it right now! If you are not completely ready yet you can browse the reference docs further.
-In the next part of this tutorial we will look into linking your local project with Medusa Cloud to make develpment smoother while leveraging the powerful management tools that merchants use to manage their Medusa store.
+
diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap
new file mode 100644
index 0000000000..8fc7342918
--- /dev/null
+++ b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap
@@ -0,0 +1,384 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`/admin/products GET /admin/products returns a list of products with child entities 1`] = `
+Array [
+ Object {
+ "collection": Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "handle": "test-collection",
+ "id": StringMatching /\\^test-\\*/,
+ "metadata": null,
+ "title": "Test collection",
+ "updated_at": Any,
+ },
+ "collection_id": "test-collection",
+ "created_at": Any,
+ "deleted_at": null,
+ "description": "test-product-description",
+ "discountable": true,
+ "handle": "test-product",
+ "height": null,
+ "hs_code": null,
+ "id": StringMatching /\\^test-\\*/,
+ "images": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-\\*/,
+ "metadata": null,
+ "updated_at": Any,
+ "url": "test-image.png",
+ },
+ ],
+ "is_giftcard": false,
+ "length": null,
+ "material": null,
+ "metadata": null,
+ "mid_code": null,
+ "options": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-\\*/,
+ "metadata": null,
+ "product_id": StringMatching /\\^test-\\*/,
+ "title": "test-option",
+ "updated_at": Any,
+ },
+ ],
+ "origin_country": null,
+ "profile_id": StringMatching /\\^sp_\\*/,
+ "status": "draft",
+ "subtitle": null,
+ "tags": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^tag\\*/,
+ "metadata": null,
+ "updated_at": Any,
+ "value": "123",
+ },
+ ],
+ "thumbnail": null,
+ "title": "Test product",
+ "type": Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-\\*/,
+ "metadata": null,
+ "updated_at": Any,
+ "value": "test-type",
+ },
+ "type_id": "test-type",
+ "updated_at": Any,
+ "variants": Array [
+ Object {
+ "allow_backorder": false,
+ "barcode": "test-barcode",
+ "created_at": Any,
+ "deleted_at": null,
+ "ean": "test-ean",
+ "height": null,
+ "hs_code": null,
+ "id": "test-variant",
+ "inventory_quantity": 10,
+ "length": null,
+ "manage_inventory": true,
+ "material": null,
+ "metadata": null,
+ "mid_code": null,
+ "options": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-variant-option\\*/,
+ "metadata": null,
+ "option_id": StringMatching /\\^test-opt\\*/,
+ "updated_at": Any,
+ "value": "Default variant",
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "origin_country": null,
+ "prices": Array [
+ Object {
+ "amount": 100,
+ "created_at": Any,
+ "currency_code": "usd",
+ "deleted_at": null,
+ "id": StringMatching /\\^test-price\\*/,
+ "region_id": null,
+ "sale_amount": null,
+ "updated_at": Any,
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "product_id": StringMatching /\\^test-\\*/,
+ "sku": "test-sku",
+ "title": "Test variant",
+ "upc": "test-upc",
+ "updated_at": Any,
+ "weight": null,
+ "width": null,
+ },
+ Object {
+ "allow_backorder": false,
+ "barcode": null,
+ "created_at": Any,
+ "deleted_at": null,
+ "ean": "test-ean2",
+ "height": null,
+ "hs_code": null,
+ "id": "test-variant_2",
+ "inventory_quantity": 10,
+ "length": null,
+ "manage_inventory": true,
+ "material": null,
+ "metadata": null,
+ "mid_code": null,
+ "options": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-variant-option\\*/,
+ "metadata": null,
+ "option_id": StringMatching /\\^test-opt\\*/,
+ "updated_at": Any,
+ "value": "Default variant 2",
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "origin_country": null,
+ "prices": Array [
+ Object {
+ "amount": 100,
+ "created_at": Any,
+ "currency_code": "usd",
+ "deleted_at": null,
+ "id": StringMatching /\\^test-price\\*/,
+ "region_id": null,
+ "sale_amount": null,
+ "updated_at": Any,
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "product_id": StringMatching /\\^test-\\*/,
+ "sku": "test-sku2",
+ "title": "Test variant rank (2)",
+ "upc": "test-upc2",
+ "updated_at": Any,
+ "weight": null,
+ "width": null,
+ },
+ Object {
+ "allow_backorder": false,
+ "barcode": "test-barcode 1",
+ "created_at": Any,
+ "deleted_at": null,
+ "ean": "test-ean1",
+ "height": null,
+ "hs_code": null,
+ "id": "test-variant_1",
+ "inventory_quantity": 10,
+ "length": null,
+ "manage_inventory": true,
+ "material": null,
+ "metadata": null,
+ "mid_code": null,
+ "options": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-variant-option\\*/,
+ "metadata": null,
+ "option_id": StringMatching /\\^test-opt\\*/,
+ "updated_at": Any,
+ "value": "Default variant 1",
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "origin_country": null,
+ "prices": Array [
+ Object {
+ "amount": 100,
+ "created_at": Any,
+ "currency_code": "usd",
+ "deleted_at": null,
+ "id": StringMatching /\\^test-price\\*/,
+ "region_id": null,
+ "sale_amount": null,
+ "updated_at": Any,
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "product_id": StringMatching /\\^test-\\*/,
+ "sku": "test-sku1",
+ "title": "Test variant rank (1)",
+ "upc": "test-upc1",
+ "updated_at": Any,
+ "weight": null,
+ "width": null,
+ },
+ ],
+ "weight": null,
+ "width": null,
+ },
+ Object {
+ "collection": Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "handle": "test-collection",
+ "id": StringMatching /\\^test-\\*/,
+ "metadata": null,
+ "title": "Test collection",
+ "updated_at": Any,
+ },
+ "collection_id": "test-collection",
+ "created_at": Any,
+ "deleted_at": null,
+ "description": "test-product-description1",
+ "discountable": true,
+ "handle": "test-product1",
+ "height": null,
+ "hs_code": null,
+ "id": StringMatching /\\^test-\\*/,
+ "images": Array [],
+ "is_giftcard": false,
+ "length": null,
+ "material": null,
+ "metadata": null,
+ "mid_code": null,
+ "options": Array [],
+ "origin_country": null,
+ "profile_id": StringMatching /\\^sp_\\*/,
+ "status": "draft",
+ "subtitle": null,
+ "tags": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^tag\\*/,
+ "metadata": null,
+ "updated_at": Any,
+ "value": "123",
+ },
+ ],
+ "thumbnail": null,
+ "title": "Test product1",
+ "type": Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-\\*/,
+ "metadata": null,
+ "updated_at": Any,
+ "value": "test-type",
+ },
+ "type_id": "test-type",
+ "updated_at": Any,
+ "variants": Array [
+ Object {
+ "allow_backorder": false,
+ "barcode": null,
+ "created_at": Any,
+ "deleted_at": null,
+ "ean": "test-ean4",
+ "height": null,
+ "hs_code": null,
+ "id": "test-variant_4",
+ "inventory_quantity": 10,
+ "length": null,
+ "manage_inventory": true,
+ "material": null,
+ "metadata": null,
+ "mid_code": null,
+ "options": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-variant-option\\*/,
+ "metadata": null,
+ "option_id": StringMatching /\\^test-opt\\*/,
+ "updated_at": Any,
+ "value": "Default variant 4",
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "origin_country": null,
+ "prices": Array [
+ Object {
+ "amount": 100,
+ "created_at": Any,
+ "currency_code": "usd",
+ "deleted_at": null,
+ "id": StringMatching /\\^test-price\\*/,
+ "region_id": null,
+ "sale_amount": null,
+ "updated_at": Any,
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "product_id": StringMatching /\\^test-\\*/,
+ "sku": "test-sku4",
+ "title": "Test variant rank (2)",
+ "upc": "test-upc4",
+ "updated_at": Any,
+ "weight": null,
+ "width": null,
+ },
+ Object {
+ "allow_backorder": false,
+ "barcode": null,
+ "created_at": Any,
+ "deleted_at": null,
+ "ean": "test-ean3",
+ "height": null,
+ "hs_code": null,
+ "id": "test-variant_3",
+ "inventory_quantity": 10,
+ "length": null,
+ "manage_inventory": true,
+ "material": null,
+ "metadata": null,
+ "mid_code": null,
+ "options": Array [
+ Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "id": StringMatching /\\^test-variant-option\\*/,
+ "metadata": null,
+ "option_id": StringMatching /\\^test-opt\\*/,
+ "updated_at": Any,
+ "value": "Default variant 3",
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "origin_country": null,
+ "prices": Array [
+ Object {
+ "amount": 100,
+ "created_at": Any,
+ "currency_code": "usd",
+ "deleted_at": null,
+ "id": StringMatching /\\^test-price\\*/,
+ "region_id": null,
+ "sale_amount": null,
+ "updated_at": Any,
+ "variant_id": StringMatching /\\^test-variant\\*/,
+ },
+ ],
+ "product_id": StringMatching /\\^test-\\*/,
+ "sku": "test-sku3",
+ "title": "Test variant rank (2)",
+ "upc": "test-upc3",
+ "updated_at": Any,
+ "weight": null,
+ "width": null,
+ },
+ ],
+ "weight": null,
+ "width": null,
+ },
+]
+`;
diff --git a/integration-tests/api/__tests__/admin/__snapshots__/return-reason.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/return-reason.js.snap
new file mode 100644
index 0000000000..79d0282927
--- /dev/null
+++ b/integration-tests/api/__tests__/admin/__snapshots__/return-reason.js.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`/admin/return-reasons POST /admin/return-reasons creates a return_reason 1`] = `
+Object {
+ "created_at": Any,
+ "deleted_at": null,
+ "description": "Use this if the size was too big",
+ "id": Any,
+ "label": "Too Big",
+ "parent_return_reason": null,
+ "parent_return_reason_id": null,
+ "return_reason_children": Array [],
+ "updated_at": Any,
+ "value": "too_big",
+}
+`;
diff --git a/integration-tests/api/__tests__/admin/customer.js b/integration-tests/api/__tests__/admin/customer.js
index c208e3086f..510ad13192 100644
--- a/integration-tests/api/__tests__/admin/customer.js
+++ b/integration-tests/api/__tests__/admin/customer.js
@@ -1,50 +1,50 @@
-const { dropDatabase } = require("pg-god");
-const path = require("path");
+const { dropDatabase } = require("pg-god")
+const path = require("path")
-const setupServer = require("../../../helpers/setup-server");
-const { useApi } = require("../../../helpers/use-api");
-const { useDb, initDb } = require("../../../helpers/use-db");
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { useDb, initDb } = require("../../../helpers/use-db")
-const customerSeeder = require("../../helpers/customer-seeder");
-const adminSeeder = require("../../helpers/admin-seeder");
+const customerSeeder = require("../../helpers/customer-seeder")
+const adminSeeder = require("../../helpers/admin-seeder")
-jest.setTimeout(30000);
+jest.setTimeout(30000)
describe("/admin/customers", () => {
- let medusaProcess;
- let dbConnection;
+ let medusaProcess
+ let dbConnection
beforeAll(async () => {
- const cwd = path.resolve(path.join(__dirname, "..", ".."));
- dbConnection = await initDb({ cwd });
- medusaProcess = await setupServer({ cwd });
- });
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
afterAll(async () => {
- const db = useDb();
- await db.shutdown();
+ const db = useDb()
+ await db.shutdown()
- medusaProcess.kill();
- });
+ medusaProcess.kill()
+ })
describe("GET /admin/customers", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await customerSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await customerSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("lists customers and query count", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.get("/admin/customers", {
@@ -53,11 +53,11 @@ describe("/admin/customers", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data.count).toEqual(3);
+ expect(response.status).toEqual(200)
+ expect(response.data.count).toEqual(4)
expect(response.data.customers).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -69,25 +69,28 @@ describe("/admin/customers", () => {
expect.objectContaining({
id: "test-customer-3",
}),
+ expect.objectContaining({
+ id: "test-customer-has_account",
+ }),
])
- );
- });
+ )
+ })
it("lists customers with specific query", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
- .get("/admin/customers?q=test2@email.com", {
+ .get("/admin/customers?q=est2@", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data.count).toEqual(1);
+ expect(response.status).toEqual(200)
+ expect(response.data.count).toEqual(1)
expect(response.data.customers).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -95,11 +98,11 @@ describe("/admin/customers", () => {
email: "test2@email.com",
}),
])
- );
- });
+ )
+ })
it("lists customers with expand query", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.get("/admin/customers?q=test1@email.com&expand=shipping_addresses", {
@@ -108,11 +111,11 @@ describe("/admin/customers", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data.count).toEqual(1);
+ expect(response.status).toEqual(200)
+ expect(response.data.count).toEqual(1)
expect(response.data.customers).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -126,7 +129,54 @@ describe("/admin/customers", () => {
]),
}),
])
- );
- });
- });
-});
+ )
+ })
+ })
+
+ describe("POST /admin/customers/:id", () => {
+ beforeEach(async () => {
+ try {
+ await adminSeeder(dbConnection)
+ await customerSeeder(dbConnection)
+ } catch (err) {
+ console.log(err)
+ throw err
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("Correctly updates customer", async () => {
+ const api = useApi()
+ const response = await api
+ .post(
+ "/admin/customers/test-customer-3",
+ {
+ first_name: "newf",
+ last_name: "newl",
+ email: "new@email.com",
+ },
+ {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ }
+ )
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.customer).toEqual(
+ expect.objectContaining({
+ first_name: "newf",
+ last_name: "newl",
+ email: "new@email.com",
+ })
+ )
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js
index 44bf7f0a1e..cbc124ce53 100644
--- a/integration-tests/api/__tests__/admin/discount.js
+++ b/integration-tests/api/__tests__/admin/discount.js
@@ -1,46 +1,107 @@
-const path = require("path");
-const { Region, DiscountRule, Discount } = require("@medusajs/medusa");
+const path = require("path")
+const { Region, DiscountRule, Discount } = require("@medusajs/medusa")
-const setupServer = require("../../../helpers/setup-server");
-const { useApi } = require("../../../helpers/use-api");
-const { initDb, useDb } = require("../../../helpers/use-db");
-const adminSeeder = require("../../helpers/admin-seeder");
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
+const adminSeeder = require("../../helpers/admin-seeder")
-jest.setTimeout(30000);
+jest.setTimeout(30000)
describe("/admin/discounts", () => {
- let medusaProcess;
- let dbConnection;
+ let medusaProcess
+ let dbConnection
beforeAll(async () => {
- const cwd = path.resolve(path.join(__dirname, "..", ".."));
- dbConnection = await initDb({ cwd });
- medusaProcess = await setupServer({ cwd });
- });
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
afterAll(async () => {
- const db = useDb();
- await db.shutdown();
- medusaProcess.kill();
- });
+ const db = useDb()
+ await db.shutdown()
+ medusaProcess.kill()
+ })
+
+ describe("GET /admin/discounts", () => {
+ beforeEach(async () => {
+ const manager = dbConnection.manager
+ try {
+ await adminSeeder(dbConnection)
+ await manager.insert(DiscountRule, {
+ id: "test-discount-rule",
+ description: "Test discount rule",
+ type: "percentage",
+ value: 10,
+ allocation: "total",
+ })
+ await manager.insert(Discount, {
+ id: "test-discount",
+ code: "TESTING",
+ rule_id: "test-discount-rule",
+ is_dynamic: false,
+ is_disabled: false,
+ })
+ await manager.insert(Discount, {
+ id: "messi-discount",
+ code: "BARCA100",
+ rule_id: "test-discount-rule",
+ is_dynamic: false,
+ is_disabled: false,
+ })
+ } catch (err) {
+ throw err
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("should list discounts that match a specific query in a case insensitive manner", async () => {
+ const api = useApi()
+
+ const response = await api
+ .get("/admin/discounts?q=barca", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
+ expect(response.data.count).toEqual(1)
+ expect(response.data.discounts).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "messi-discount",
+ code: "BARCA100",
+ }),
+ ])
+ )
+ })
+ })
describe("POST /admin/discounts", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
+ await adminSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a discount and updates it", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.post(
@@ -62,16 +123,16 @@ describe("/admin/discounts", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.discount).toEqual(
expect.objectContaining({
code: "HELLOWORLD",
usage_limit: 10,
})
- );
+ )
const updated = await api
.post(
@@ -86,51 +147,51 @@ describe("/admin/discounts", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(updated.status).toEqual(200);
+ expect(updated.status).toEqual(200)
expect(updated.data.discount).toEqual(
expect.objectContaining({
code: "HELLOWORLD",
usage_limit: 20,
})
- );
- });
- });
+ )
+ })
+ })
describe("testing for soft-deletion + uniqueness on discount codes", () => {
- let manager;
+ let manager
beforeEach(async () => {
- manager = dbConnection.manager;
+ manager = dbConnection.manager
try {
- await adminSeeder(dbConnection);
+ await adminSeeder(dbConnection)
await manager.insert(DiscountRule, {
id: "test-discount-rule",
description: "Test discount rule",
type: "percentage",
value: 10,
allocation: "total",
- });
+ })
await manager.insert(Discount, {
id: "test-discount",
code: "TESTING",
rule_id: "test-discount-rule",
is_dynamic: false,
is_disabled: false,
- });
+ })
} catch (err) {
- throw err;
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("successfully creates discount with soft-deleted discount code", async () => {
- const api = useApi();
+ const api = useApi()
// First we soft-delete the discount
await api
@@ -140,8 +201,8 @@ describe("/admin/discounts", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
// Lets try to create a discount with same code as deleted one
const response = await api
@@ -164,51 +225,81 @@ describe("/admin/discounts", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.discount).toEqual(
expect.objectContaining({
code: "TESTING",
usage_limit: 10,
})
- );
- });
- });
+ )
+ })
+
+ it("should fails when creating a discount with already existing code", async () => {
+ const api = useApi()
+
+ // Lets try to create a discount with same code as deleted one
+ try {
+ await api.post(
+ "/admin/discounts",
+ {
+ code: "TESTING",
+ rule: {
+ description: "test",
+ type: "percentage",
+ value: 10,
+ allocation: "total",
+ },
+ usage_limit: 10,
+ },
+ {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ }
+ )
+ } catch (error) {
+ expect(error.response.data.message).toMatch(
+ /duplicate key value violates unique constraint/i
+ )
+ }
+ })
+ })
describe("POST /admin/discounts/:discount_id/dynamic-codes", () => {
beforeEach(async () => {
- const manager = dbConnection.manager;
+ const manager = dbConnection.manager
try {
- await adminSeeder(dbConnection);
+ await adminSeeder(dbConnection)
await manager.insert(DiscountRule, {
id: "test-discount-rule",
description: "Dynamic rule",
type: "percentage",
value: 10,
allocation: "total",
- });
+ })
await manager.insert(Discount, {
id: "test-discount",
code: "DYNAMIC",
is_dynamic: true,
is_disabled: false,
rule_id: "test-discount-rule",
- });
+ })
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a dynamic discount", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.post(
@@ -223,10 +314,10 @@ describe("/admin/discounts", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- });
- });
-});
+ expect(response.status).toEqual(200)
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/draft-order.js b/integration-tests/api/__tests__/admin/draft-order.js
index 89898b10a2..2f267969fb 100644
--- a/integration-tests/api/__tests__/admin/draft-order.js
+++ b/integration-tests/api/__tests__/admin/draft-order.js
@@ -1,49 +1,49 @@
-const path = require("path");
+const path = require("path")
-const setupServer = require("../../../helpers/setup-server");
-const { useApi } = require("../../../helpers/use-api");
-const { initDb, useDb } = require("../../../helpers/use-db");
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
-const draftOrderSeeder = require("../../helpers/draft-order-seeder");
-const adminSeeder = require("../../helpers/admin-seeder");
+const draftOrderSeeder = require("../../helpers/draft-order-seeder")
+const adminSeeder = require("../../helpers/admin-seeder")
-jest.setTimeout(30000);
+jest.setTimeout(30000)
describe("/admin/draft-orders", () => {
- let medusaProcess;
- let dbConnection;
+ let medusaProcess
+ let dbConnection
beforeAll(async () => {
- const cwd = path.resolve(path.join(__dirname, "..", ".."));
- dbConnection = await initDb({ cwd });
- medusaProcess = await setupServer({ cwd });
- });
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
afterAll(async () => {
- const db = useDb();
- await db.shutdown();
+ const db = useDb()
+ await db.shutdown()
- medusaProcess.kill();
- });
+ medusaProcess.kill()
+ })
describe("POST /admin/draft-orders", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await draftOrderSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await draftOrderSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a draft order cart", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
email: "oli@test.dk",
@@ -62,7 +62,7 @@ describe("/admin/draft-orders", () => {
option_id: "test-option",
},
],
- };
+ }
const response = await api
.post("/admin/draft-orders", payload, {
@@ -71,13 +71,53 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
- expect(response.status).toEqual(200);
- });
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
+ })
+
+ it("creates a draft order cart and creates new user", async () => {
+ const api = useApi()
+
+ const payload = {
+ email: "non-existing@test.dk",
+ customer_id: "non-existing",
+ shipping_address: "oli-shipping",
+ items: [
+ {
+ variant_id: "test-variant",
+ quantity: 2,
+ metadata: {},
+ },
+ ],
+ region_id: "test-region",
+ shipping_methods: [
+ {
+ option_id: "test-option",
+ },
+ ],
+ }
+
+ const response = await api
+ .post("/admin/draft-orders", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+
+ const draftOrder = response.data.draft_order
+
+ expect(draftOrder.cart.customer_id).toBeDefined()
+ expect(draftOrder.cart.email).toEqual("non-existing@test.dk")
+ })
it("fails to create a draft order with option requirement", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
email: "oli@test.dk",
@@ -96,7 +136,7 @@ describe("/admin/draft-orders", () => {
option_id: "test-option-req",
},
],
- };
+ }
const response = await api
.post("/admin/draft-orders", payload, {
@@ -105,13 +145,13 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- return err.response;
- });
- expect(response.status).toEqual(400);
- });
+ return err.response
+ })
+ expect(response.status).toEqual(400)
+ })
it("creates a draft order with option requirement", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
email: "oli@test.dk",
@@ -135,7 +175,7 @@ describe("/admin/draft-orders", () => {
option_id: "test-option-req",
},
],
- };
+ }
const response = await api
.post("/admin/draft-orders", payload, {
@@ -144,13 +184,13 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
- expect(response.status).toEqual(200);
- });
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
+ })
it("creates a draft order with custom item", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
email: "oli@test.dk",
@@ -174,7 +214,7 @@ describe("/admin/draft-orders", () => {
option_id: "test-option",
},
],
- };
+ }
const response = await api
.post("/admin/draft-orders", payload, {
@@ -183,13 +223,13 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
- expect(response.status).toEqual(200);
- });
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
+ })
it("creates a draft order with product variant with custom price and custom item price set to 0", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
email: "oli@test.dk",
@@ -215,7 +255,7 @@ describe("/admin/draft-orders", () => {
option_id: "test-option",
},
],
- };
+ }
const response = await api
.post("/admin/draft-orders", payload, {
@@ -224,8 +264,8 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
const created = await api
.get(`/admin/draft-orders/${response.data.draft_order.id}`, {
@@ -234,10 +274,10 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(created.data.draft_order.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -248,17 +288,17 @@ describe("/admin/draft-orders", () => {
unit_price: 0,
}),
])
- );
+ )
// Check that discount is applied
expect(created.data.draft_order.cart.discounts[0]).toEqual(
expect.objectContaining({
code: "TEST",
})
- );
- });
+ )
+ })
it("creates a draft order with created shipping address", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
email: "oli@test.dk",
@@ -289,7 +329,7 @@ describe("/admin/draft-orders", () => {
option_id: "test-option",
},
],
- };
+ }
const response = await api
.post("/admin/draft-orders", payload, {
@@ -298,13 +338,13 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
- expect(response.status).toEqual(200);
- });
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
+ })
it("creates a draft order and registers manual payment", async () => {
- const api = useApi();
+ const api = useApi()
// register system payment for draft order
const orderResponse = await api.post(
@@ -315,7 +355,7 @@ describe("/admin/draft-orders", () => {
Authorization: "Bearer test_token",
},
}
- );
+ )
const createdOrder = await api.get(
`/admin/orders/${orderResponse.data.order.id}`,
@@ -324,7 +364,7 @@ describe("/admin/draft-orders", () => {
Authorization: "Bearer test_token",
},
}
- );
+ )
const updatedDraftOrder = await api.get(
`/admin/draft-orders/test-draft-order`,
@@ -333,40 +373,38 @@ describe("/admin/draft-orders", () => {
Authorization: "Bearer test_token",
},
}
- );
+ )
- expect(orderResponse.status).toEqual(200);
+ expect(orderResponse.status).toEqual(200)
// expect newly created order to have id of draft order and system payment
- expect(createdOrder.data.order.draft_order_id).toEqual(
- "test-draft-order"
- );
+ expect(createdOrder.data.order.draft_order_id).toEqual("test-draft-order")
expect(createdOrder.data.order.payments).toEqual(
expect.arrayContaining([
expect.objectContaining({ provider_id: "system" }),
])
- );
+ )
// expect draft order to be complete
- expect(updatedDraftOrder.data.draft_order.status).toEqual("completed");
- expect(updatedDraftOrder.data.draft_order.completed_at).not.toEqual(null);
- });
- });
+ expect(updatedDraftOrder.data.draft_order.status).toEqual("completed")
+ expect(updatedDraftOrder.data.draft_order.completed_at).not.toEqual(null)
+ })
+ })
describe("GET /admin/draft-orders", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await draftOrderSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await draftOrderSeeder(dbConnection)
} catch (err) {
- throw err;
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("lists draft orders", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.get("/admin/draft-orders", {
@@ -375,20 +413,20 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.draft_orders).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "test-draft-order" }),
])
- );
- });
+ )
+ })
it("lists draft orders with query", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.get("/admin/draft-orders?q=oli@test", {
@@ -397,10 +435,10 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.draft_orders).toEqual(
expect.arrayContaining([
@@ -408,11 +446,11 @@ describe("/admin/draft-orders", () => {
cart: expect.objectContaining({ email: "oli@test.dk" }),
}),
])
- );
- });
+ )
+ })
it("lists no draft orders on query for non-existing email", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.get("/admin/draft-orders?q=heyo@heyo.dk", {
@@ -421,34 +459,34 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
- expect(response.data.draft_orders).toEqual([]);
- expect(response.data.count).toEqual(0);
- });
- });
+ expect(response.data.draft_orders).toEqual([])
+ expect(response.data.count).toEqual(0)
+ })
+ })
describe("DELETE /admin/draft-orders/:id", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await draftOrderSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await draftOrderSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("deletes a draft order", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.delete("/admin/draft-orders/test-draft-order", {
@@ -457,36 +495,36 @@ describe("/admin/draft-orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data).toEqual({
id: "test-draft-order",
object: "draft-order",
deleted: true,
- });
- });
- });
+ })
+ })
+ })
describe("POST /admin/draft-orders/:id/line-items/:line_id", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await draftOrderSeeder(dbConnection, { status: "open" });
+ await adminSeeder(dbConnection)
+ await draftOrderSeeder(dbConnection, { status: "open" })
} catch (err) {
- throw err;
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("updates a line item on the draft order", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.post(
@@ -502,10 +540,10 @@ describe("/admin/draft-orders", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
const updatedDraftOrder = await api.get(
`/admin/draft-orders/test-draft-order`,
@@ -514,16 +552,16 @@ describe("/admin/draft-orders", () => {
Authorization: "Bearer test_token",
},
}
- );
+ )
- const item = updatedDraftOrder.data.draft_order.cart.items[0];
+ const item = updatedDraftOrder.data.draft_order.cart.items[0]
- expect(item.title).toEqual("Update title");
- expect(item.unit_price).toEqual(1000);
- });
+ expect(item.title).toEqual("Update title")
+ expect(item.unit_price).toEqual(1000)
+ })
it("removes the line item, if quantity is 0", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.post(
@@ -539,10 +577,10 @@ describe("/admin/draft-orders", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
const updatedDraftOrder = await api.get(
`/admin/draft-orders/test-draft-order`,
@@ -551,31 +589,31 @@ describe("/admin/draft-orders", () => {
Authorization: "Bearer test_token",
},
}
- );
+ )
- const items = updatedDraftOrder.data.draft_order.cart.items;
+ const items = updatedDraftOrder.data.draft_order.cart.items
- expect(items).toEqual([]);
- });
- });
+ expect(items).toEqual([])
+ })
+ })
describe("POST /admin/draft-orders/:id", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await draftOrderSeeder(dbConnection, { status: "open" });
+ await adminSeeder(dbConnection)
+ await draftOrderSeeder(dbConnection, { status: "open" })
} catch (err) {
- throw err;
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("updates a line item on the draft order", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.post(
@@ -607,10 +645,10 @@ describe("/admin/draft-orders", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
const updatedDraftOrder = await api.get(
`/admin/draft-orders/test-draft-order`,
@@ -619,14 +657,14 @@ describe("/admin/draft-orders", () => {
Authorization: "Bearer test_token",
},
}
- );
+ )
- const dorder = updatedDraftOrder.data.draft_order;
+ const dorder = updatedDraftOrder.data.draft_order
- expect(dorder.cart.email).toEqual("lebron@james.com");
- expect(dorder.cart.billing_address.first_name).toEqual("lebron");
- expect(dorder.cart.shipping_address.last_name).toEqual("james");
- expect(dorder.cart.discounts[0].code).toEqual("TEST");
- });
- });
-});
+ expect(dorder.cart.email).toEqual("lebron@james.com")
+ expect(dorder.cart.billing_address.first_name).toEqual("lebron")
+ expect(dorder.cart.shipping_address.last_name).toEqual("james")
+ expect(dorder.cart.discounts[0].code).toEqual("TEST")
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/gift-cards.js b/integration-tests/api/__tests__/admin/gift-cards.js
index 5b71d5bc5c..219e8b54d1 100644
--- a/integration-tests/api/__tests__/admin/gift-cards.js
+++ b/integration-tests/api/__tests__/admin/gift-cards.js
@@ -1,54 +1,162 @@
-const path = require("path");
-const { Region } = require("@medusajs/medusa");
+const path = require("path")
+const { Region, GiftCard } = require("@medusajs/medusa")
-const setupServer = require("../../../helpers/setup-server");
-const { useApi } = require("../../../helpers/use-api");
-const { initDb, useDb } = require("../../../helpers/use-db");
-const adminSeeder = require("../../helpers/admin-seeder");
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
+const adminSeeder = require("../../helpers/admin-seeder")
-jest.setTimeout(30000);
+jest.setTimeout(30000)
describe("/admin/gift-cards", () => {
- let medusaProcess;
- let dbConnection;
+ let medusaProcess
+ let dbConnection
beforeAll(async () => {
- const cwd = path.resolve(path.join(__dirname, "..", ".."));
- dbConnection = await initDb({ cwd });
- medusaProcess = await setupServer({ cwd });
- });
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
afterAll(async () => {
- const db = useDb();
- await db.shutdown();
+ const db = useDb()
+ await db.shutdown()
- medusaProcess.kill();
- });
+ medusaProcess.kill()
+ })
+
+ describe("GET /admin/gift-cards", () => {
+ beforeEach(async () => {
+ const manager = dbConnection.manager
+ try {
+ await adminSeeder(dbConnection)
+ await manager.insert(Region, {
+ id: "test-region",
+ name: "Test Region",
+ currency_code: "usd",
+ tax_rate: 0,
+ })
+ await manager.insert(GiftCard, {
+ id: "gift_test",
+ code: "GC_TEST",
+ value: 20000,
+ balance: 20000,
+ region_id: "test-region",
+ })
+ await manager.insert(GiftCard, {
+ id: "another_gift_test",
+ code: "CARD_TEST",
+ value: 200000,
+ balance: 200000,
+ region_id: "test-region",
+ })
+ } catch (err) {
+ console.log(err)
+ throw err
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("lists gift cards and query count", async () => {
+ const api = useApi()
+
+ const response = await api
+ .get("/admin/gift-cards", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.gift_cards).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "gift_test",
+ code: "GC_TEST",
+ }),
+ expect.objectContaining({
+ id: "another_gift_test",
+ code: "CARD_TEST",
+ }),
+ ])
+ )
+ })
+
+ it("lists gift cards with specific query", async () => {
+ const api = useApi()
+
+ const response = await api
+ .get("/admin/gift-cards?q=gc", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.gift_cards.length).toEqual(1)
+ expect(response.data.gift_cards).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "gift_test",
+ code: "GC_TEST",
+ }),
+ ])
+ )
+ })
+
+ it("lists no gift cards on query for non-existing gift card code", async () => {
+ const api = useApi()
+
+ const response = await api
+ .get("/admin/gift-cards?q=bla", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.gift_cards.length).toEqual(0)
+ expect(response.data.gift_cards).toEqual([])
+ })
+ })
describe("POST /admin/gift-cards", () => {
beforeEach(async () => {
- const manager = dbConnection.manager;
+ const manager = dbConnection.manager
try {
- await adminSeeder(dbConnection);
+ await adminSeeder(dbConnection)
await manager.insert(Region, {
id: "region",
name: "Test Region",
currency_code: "usd",
tax_rate: 0,
- });
+ })
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a gift card", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.post(
@@ -64,13 +172,13 @@ describe("/admin/gift-cards", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data.gift_card.value).toEqual(1000);
- expect(response.data.gift_card.balance).toEqual(1000);
- expect(response.data.gift_card.region_id).toEqual("region");
- });
- });
-});
+ expect(response.status).toEqual(200)
+ expect(response.data.gift_card.value).toEqual(1000)
+ expect(response.data.gift_card.balance).toEqual(1000)
+ expect(response.data.gift_card.region_id).toEqual("region")
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/note.js b/integration-tests/api/__tests__/admin/note.js
new file mode 100644
index 0000000000..6e3bd05df0
--- /dev/null
+++ b/integration-tests/api/__tests__/admin/note.js
@@ -0,0 +1,268 @@
+const path = require("path")
+const { Note } = require("@medusajs/medusa")
+
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
+
+const adminSeeder = require("../../helpers/admin-seeder")
+
+jest.setTimeout(30000)
+
+const note = {
+ id: "note1",
+ value: "note text",
+ resource_id: "resource1",
+ resource_type: "type",
+ author: { id: "admin_user" },
+}
+
+describe("/admin/notes", () => {
+ let medusaProcess
+ let dbConnection
+
+ beforeAll(async () => {
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
+
+ afterAll(async () => {
+ const db = useDb()
+ await db.shutdown()
+ medusaProcess.kill()
+ })
+
+ describe("GET /admin/notes/:id", () => {
+ beforeEach(async () => {
+ const manager = dbConnection.manager
+ try {
+ await adminSeeder(dbConnection)
+
+ await manager.insert(Note, note)
+ } catch (err) {
+ console.log(err)
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("properly retrieves note", async () => {
+ const api = useApi()
+
+ const response = await api.get("/admin/notes/note1", {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ })
+
+ expect(response.data).toMatchObject({
+ note: {
+ id: "note1",
+ resource_id: "resource1",
+ resource_type: "type",
+ value: "note text",
+ author: { id: "admin_user" },
+ },
+ })
+ })
+ })
+
+ describe("POST /admin/notes", () => {
+ beforeEach(async () => {
+ try {
+ await adminSeeder(dbConnection)
+ } catch (err) {
+ console.log(err)
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("creates a note", async () => {
+ const api = useApi()
+
+ const response = await api
+ .post(
+ "/admin/notes",
+ {
+ resource_id: "resource-id",
+ resource_type: "resource-type",
+ value: "my note",
+ },
+ {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ }
+ )
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.data).toMatchObject({
+ note: {
+ id: expect.stringMatching(/^note_*/),
+ resource_id: "resource-id",
+ resource_type: "resource-type",
+ value: "my note",
+ author_id: "admin_user",
+ },
+ })
+ })
+ })
+
+ describe("GET /admin/notes", () => {
+ beforeEach(async () => {
+ const manager = dbConnection.manager
+ try {
+ await adminSeeder(dbConnection)
+
+ await manager.insert(Note, { ...note, id: "note1" })
+ await manager.insert(Note, { ...note, id: "note2" })
+ await manager.insert(Note, {
+ ...note,
+ id: "note3",
+ resource_id: "resource2",
+ })
+ } catch (err) {
+ console.log(err)
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("lists notes only related to wanted resource", async () => {
+ const api = useApi()
+ const response = await api
+ .get("/admin/notes?resource_id=resource1", {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.data.notes.length).toEqual(2)
+ expect(response.data).toMatchObject({
+ notes: [
+ {
+ id: "note1",
+ resource_id: "resource1",
+ resource_type: "type",
+ value: "note text",
+ author: { id: "admin_user" },
+ },
+ {
+ id: "note2",
+ resource_id: "resource1",
+ resource_type: "type",
+ value: "note text",
+ author: { id: "admin_user" },
+ },
+ ],
+ })
+ })
+ })
+
+ describe("POST /admin/notes/:id", () => {
+ beforeEach(async () => {
+ const manager = dbConnection.manager
+ try {
+ await adminSeeder(dbConnection)
+
+ await manager.insert(Note, note)
+ } catch (err) {
+ console.log(err)
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("updates the content of the note", async () => {
+ const api = useApi()
+
+ await api
+ .post(
+ "/admin/notes/note1",
+ { value: "new text" },
+ {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ }
+ )
+ .catch((err) => {
+ console.log(err)
+ })
+
+ const response = await api
+ .get("/admin/notes/note1", {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.data.note.value).toEqual("new text")
+ })
+ })
+
+ describe("DELETE /admin/notes/:id", () => {
+ beforeEach(async () => {
+ const manager = dbConnection.manager
+ try {
+ await adminSeeder(dbConnection)
+
+ await manager.insert(Note, note)
+ } catch (err) {
+ console.log(err)
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("deletes the wanted note", async () => {
+ const api = useApi()
+
+ await api
+ .delete("/admin/notes/note1", {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ let error
+ await api
+ .get("/admin/notes/note1", {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => (error = err))
+
+ expect(error.response.status).toEqual(404)
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/order.js b/integration-tests/api/__tests__/admin/order.js
index d21591b963..251578cca0 100644
--- a/integration-tests/api/__tests__/admin/order.js
+++ b/integration-tests/api/__tests__/admin/order.js
@@ -1,56 +1,64 @@
-const path = require("path");
+const path = require("path")
const {
ReturnReason,
Order,
LineItem,
ProductVariant,
-} = require("@medusajs/medusa");
+} = require("@medusajs/medusa")
-const setupServer = require("../../../helpers/setup-server");
-const { useApi } = require("../../../helpers/use-api");
-const { initDb, useDb } = require("../../../helpers/use-db");
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
-const orderSeeder = require("../../helpers/order-seeder");
-const swapSeeder = require("../../helpers/swap-seeder");
-const adminSeeder = require("../../helpers/admin-seeder");
+const orderSeeder = require("../../helpers/order-seeder")
+const swapSeeder = require("../../helpers/swap-seeder")
+const adminSeeder = require("../../helpers/admin-seeder")
+const claimSeeder = require("../../helpers/claim-seeder")
-jest.setTimeout(30000);
+const {
+ expectPostCallToReturn,
+ expectAllPostCallsToReturn,
+ callGet,
+ partial,
+} = require("../../helpers/call-helpers")
+
+jest.setTimeout(30000)
describe("/admin/orders", () => {
- let medusaProcess;
- let dbConnection;
+ let medusaProcess
+ let dbConnection
beforeAll(async () => {
- const cwd = path.resolve(path.join(__dirname, "..", ".."));
- dbConnection = await initDb({ cwd });
- medusaProcess = await setupServer({ cwd });
- });
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
afterAll(async () => {
- const db = useDb();
- await db.shutdown();
+ const db = useDb()
+ await db.shutdown()
- medusaProcess.kill();
- });
+ medusaProcess.kill()
+ })
describe("GET /admin/orders", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await orderSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await orderSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("gets orders", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.get("/admin/orders", {
@@ -59,23 +67,25 @@ describe("/admin/orders", () => {
},
})
.catch((err) => {
- console.log(err);
- });
- expect(response.status).toEqual(200);
- });
- });
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
+ })
+ })
describe("GET /admin/orders", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await orderSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await orderSeeder(dbConnection)
+ await swapSeeder(dbConnection)
+ await claimSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- const manager = dbConnection.manager;
+ const manager = dbConnection.manager
const order2 = manager.create(Order, {
id: "test-order-not-payed",
@@ -126,9 +136,9 @@ describe("/admin/orders", () => {
},
],
items: [],
- });
+ })
- await manager.save(order2);
+ await manager.save(order2)
const li2 = manager.create(LineItem, {
id: "test-item",
@@ -141,23 +151,23 @@ describe("/admin/orders", () => {
quantity: 1,
variant_id: "test-variant",
order_id: "test-order-not-payed",
- });
+ })
- await manager.save(li2);
- });
+ await manager.save(li2)
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("cancels an order and increments inventory_quantity", async () => {
- const api = useApi();
- const manager = dbConnection.manager;
+ const api = useApi()
+ const manager = dbConnection.manager
- const initialInventoryRes = await api.get("/store/variants/test-variant");
+ const initialInventoryRes = await api.get("/store/variants/test-variant")
- expect(initialInventoryRes.data.variant.inventory_quantity).toEqual(1);
+ expect(initialInventoryRes.data.variant.inventory_quantity).toEqual(1)
const response = await api
.post(
@@ -170,26 +180,26 @@ describe("/admin/orders", () => {
}
)
.catch((err) => {
- console.log(err);
- });
- expect(response.status).toEqual(200);
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
- const secondInventoryRes = await api.get("/store/variants/test-variant");
+ const secondInventoryRes = await api.get("/store/variants/test-variant")
- expect(secondInventoryRes.data.variant.inventory_quantity).toEqual(2);
- });
+ expect(secondInventoryRes.data.variant.inventory_quantity).toEqual(2)
+ })
it("cancels an order but does not increment inventory_quantity of unmanaged variant", async () => {
- const api = useApi();
- const manager = dbConnection.manager;
+ const api = useApi()
+ const manager = dbConnection.manager
await manager.query(
`UPDATE "product_variant" SET manage_inventory=false WHERE id = 'test-variant'`
- );
+ )
- const initialInventoryRes = await api.get("/store/variants/test-variant");
+ const initialInventoryRes = await api.get("/store/variants/test-variant")
- expect(initialInventoryRes.data.variant.inventory_quantity).toEqual(1);
+ expect(initialInventoryRes.data.variant.inventory_quantity).toEqual(1)
const response = await api
.post(
@@ -202,34 +212,35 @@ describe("/admin/orders", () => {
}
)
.catch((err) => {
- console.log(err);
- });
- expect(response.status).toEqual(200);
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
- const secondInventoryRes = await api.get("/store/variants/test-variant");
+ const secondInventoryRes = await api.get("/store/variants/test-variant")
- expect(secondInventoryRes.data.variant.inventory_quantity).toEqual(1);
- });
- });
+ expect(secondInventoryRes.data.variant.inventory_quantity).toEqual(1)
+ })
+ })
describe("POST /admin/orders/:id/claims", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await orderSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await orderSeeder(dbConnection)
+ await claimSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a claim", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.post(
"/admin/orders/test-order/claims",
@@ -256,30 +267,30 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
- expect(response.status).toEqual(200);
+ )
+ expect(response.status).toEqual(200)
const variant = await api.get("/admin/products", {
headers: {
authorization: "Bearer test_token",
},
- });
+ })
// find test variant and verify that its inventory quantity has changed
const toTest = variant.data.products[0].variants.find(
(v) => v.id === "test-variant"
- );
- expect(toTest.inventory_quantity).toEqual(0);
+ )
+ expect(toTest.inventory_quantity).toEqual(0)
expect(response.data.order.claims[0].shipping_address_id).toEqual(
"test-shipping-address"
- );
+ )
expect(response.data.order.claims[0].shipping_address).toEqual(
expect.objectContaining({
first_name: "lebron",
country_code: "us",
})
- );
+ )
expect(response.data.order.claims[0].claim_items).toEqual(
expect.arrayContaining([
@@ -294,7 +305,7 @@ describe("/admin/orders", () => {
]),
}),
])
- );
+ )
expect(response.data.order.claims[0].additional_items).toEqual(
expect.arrayContaining([
@@ -303,11 +314,11 @@ describe("/admin/orders", () => {
quantity: 1,
}),
])
- );
- });
+ )
+ })
it("creates a claim with a shipping address", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.post(
"/admin/orders/test-order/claims",
@@ -342,8 +353,8 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
- expect(response.status).toEqual(200);
+ )
+ expect(response.status).toEqual(200)
expect(response.data.order.claims[0].shipping_address).toEqual(
expect.objectContaining({
@@ -354,7 +365,7 @@ describe("/admin/orders", () => {
postal_code: "12345",
country_code: "us",
})
- );
+ )
expect(response.data.order.claims[0].claim_items).toEqual(
expect.arrayContaining([
@@ -369,7 +380,7 @@ describe("/admin/orders", () => {
]),
}),
])
- );
+ )
expect(response.data.order.claims[0].additional_items).toEqual(
expect.arrayContaining([
@@ -378,11 +389,11 @@ describe("/admin/orders", () => {
quantity: 1,
}),
])
- );
- });
+ )
+ })
it("creates a claim with return shipping", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.post(
"/admin/orders/test-order/claims",
@@ -410,8 +421,9 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
- expect(response.status).toEqual(200);
+ )
+
+ expect(response.status).toEqual(200)
expect(response.data.order.claims[0].claim_items).toEqual(
expect.arrayContaining([
@@ -426,7 +438,7 @@ describe("/admin/orders", () => {
]),
}),
])
- );
+ )
expect(response.data.order.claims[0].additional_items).toEqual(
expect.arrayContaining([
@@ -435,7 +447,7 @@ describe("/admin/orders", () => {
quantity: 1,
}),
])
- );
+ )
expect(
response.data.order.claims[0].return_order.shipping_method
@@ -444,11 +456,11 @@ describe("/admin/orders", () => {
price: 0,
shipping_option_id: "test-return-option",
})
- );
- });
+ )
+ })
it("updates a claim", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.post(
"/admin/orders/test-order/claims",
@@ -475,10 +487,10 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
- expect(response.status).toEqual(200);
+ )
+ expect(response.status).toEqual(200)
- const cid = response.data.order.claims[0].id;
+ const cid = response.data.order.claims[0].id
const { status, data: updateData } = await api.post(
`/admin/orders/test-order/claims/${cid}`,
{
@@ -493,18 +505,18 @@ describe("/admin/orders", () => {
authorization: "bearer test_token",
},
}
- );
+ )
- expect(status).toEqual(200);
+ expect(status).toEqual(200)
expect(updateData.order.claims[0].shipping_methods).toEqual([
expect.objectContaining({
id: "test-method",
}),
- ]);
- });
+ ])
+ })
it("updates claim items", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.post(
"/admin/orders/test-order/claims",
@@ -531,11 +543,11 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
- expect(response.status).toEqual(200);
+ )
+ expect(response.status).toEqual(200)
- let claim = response.data.order.claims[0];
- const cid = claim.id;
+ let claim = response.data.order.claims[0]
+ const cid = claim.id
const { status, data: updateData } = await api.post(
`/admin/orders/test-order/claims/${cid}`,
{
@@ -558,14 +570,14 @@ describe("/admin/orders", () => {
authorization: "bearer test_token",
},
}
- );
+ )
- expect(status).toEqual(200);
- expect(updateData.order.claims.length).toEqual(1);
+ expect(status).toEqual(200)
+ expect(updateData.order.claims.length).toEqual(1)
- claim = updateData.order.claims[0];
+ claim = updateData.order.claims[0]
- expect(claim.claim_items.length).toEqual(1);
+ expect(claim.claim_items.length).toEqual(1)
expect(claim.claim_items).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -587,11 +599,11 @@ describe("/admin/orders", () => {
// ]),
}),
])
- );
- });
+ )
+ })
it("updates claim items - removes image", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.post(
"/admin/orders/test-order/claims",
@@ -618,11 +630,11 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
- expect(response.status).toEqual(200);
+ )
+ expect(response.status).toEqual(200)
- let claim = response.data.order.claims[0];
- const cid = claim.id;
+ let claim = response.data.order.claims[0]
+ const cid = claim.id
const { status, data: updateData } = await api.post(
`/admin/orders/test-order/claims/${cid}`,
{
@@ -642,14 +654,14 @@ describe("/admin/orders", () => {
authorization: "bearer test_token",
},
}
- );
+ )
- expect(status).toEqual(200);
- expect(updateData.order.claims.length).toEqual(1);
+ expect(status).toEqual(200)
+ expect(updateData.order.claims.length).toEqual(1)
- claim = updateData.order.claims[0];
+ claim = updateData.order.claims[0]
- expect(claim.claim_items.length).toEqual(1);
+ expect(claim.claim_items.length).toEqual(1)
expect(claim.claim_items).toEqual([
expect.objectContaining({
id: claim.claim_items[0].id,
@@ -662,11 +674,11 @@ describe("/admin/orders", () => {
// expect.objectContaining({ value: "tags" }),
// ]),
}),
- ]);
- });
+ ])
+ })
it("fulfills a claim", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.post(
@@ -701,10 +713,10 @@ describe("/admin/orders", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- const cid = response.data.order.claims[0].id;
+ const cid = response.data.order.claims[0].id
const fulRes = await api.post(
`/admin/orders/test-order/claims/${cid}/fulfillments`,
{},
@@ -713,18 +725,18 @@ describe("/admin/orders", () => {
Authorization: "Bearer test_token",
},
}
- );
- expect(fulRes.status).toEqual(200);
+ )
+ expect(fulRes.status).toEqual(200)
expect(fulRes.data.order.claims).toEqual([
expect.objectContaining({
id: cid,
order_id: "test-order",
fulfillment_status: "fulfilled",
}),
- ]);
+ ])
- const fid = fulRes.data.order.claims[0].fulfillments[0].id;
- const iid = fulRes.data.order.claims[0].additional_items[0].id;
+ const fid = fulRes.data.order.claims[0].fulfillments[0].id
+ const iid = fulRes.data.order.claims[0].additional_items[0].id
expect(fulRes.data.order.claims[0].fulfillments).toEqual([
expect.objectContaining({
items: [
@@ -735,11 +747,63 @@ describe("/admin/orders", () => {
},
],
}),
- ]);
- });
+ ])
+ })
+
+ it("Only allow canceling claim after canceling fulfillments", async () => {
+ const order_id = "order-with-claim"
+
+ const order = await callGet({
+ path: `/admin/orders/${order_id}`,
+ get: "order",
+ })
+
+ const claim = order.claims.filter((s) => s.id === "claim-w-f")[0]
+ const claim_id = claim.id
+
+ const expectCancelToReturn = partial(expectPostCallToReturn, {
+ path: `/admin/orders/${order_id}/claims/${claim_id}/cancel`,
+ })
+
+ await expectCancelToReturn({ code: 400 })
+
+ await expectAllPostCallsToReturn({
+ code: 200,
+ col: claim.fulfillments,
+ pathf: (f) =>
+ `/admin/orders/${order_id}/claims/${claim_id}/fulfillments/${f.id}/cancel`,
+ })
+
+ await expectCancelToReturn({ code: 200 })
+ })
+
+ it("Only allow canceling claim after canceling returns", async () => {
+ const order_id = "order-with-claim"
+
+ const order = await callGet({
+ path: `/admin/orders/${order_id}`,
+ get: "order",
+ })
+
+ const claim = order.claims.filter((c) => c.id === "claim-w-r")[0]
+ const claim_id = claim.id
+
+ const expectCancelToReturn = partial(expectPostCallToReturn, {
+ path: `/admin/orders/${order_id}/claims/${claim_id}/cancel`,
+ })
+
+ await expectCancelToReturn({ code: 400 })
+
+ await expectPostCallToReturn({
+ code: 200,
+ path: `/admin/returns/${claim.return_order.id}/cancel`,
+ })
+
+ await expectCancelToReturn({ code: 200 })
+ })
it("fails to creates a claim due to no stock on additional items", async () => {
- const api = useApi();
+ const api = useApi()
try {
await api.post(
"/admin/orders/test-order/claims",
@@ -766,43 +830,43 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
} catch (e) {
- expect(e.response.status).toEqual(400);
+ expect(e.response.status).toEqual(400)
expect(e.response.data.message).toEqual(
"Variant with id: test-variant does not have the required inventory"
- );
+ )
}
- });
- });
+ })
+ })
describe("POST /admin/orders/:id/return", () => {
- let rrId;
+ let rrId
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await orderSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await orderSeeder(dbConnection)
const created = dbConnection.manager.create(ReturnReason, {
value: "too_big",
label: "Too Big",
- });
- const result = await dbConnection.manager.save(created);
+ })
+ const result = await dbConnection.manager.save(created)
- rrId = result.id;
+ rrId = result.id
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a return", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.post(
"/admin/orders/test-order/return",
@@ -821,10 +885,10 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
- expect(response.status).toEqual(200);
+ )
+ expect(response.status).toEqual(200)
- expect(response.data.order.returns[0].refund_amount).toEqual(7200);
+ expect(response.data.order.returns[0].refund_amount).toEqual(7200)
expect(response.data.order.returns[0].items).toEqual([
expect.objectContaining({
item_id: "test-item",
@@ -832,11 +896,11 @@ describe("/admin/orders", () => {
reason_id: rrId,
note: "TOO SMALL",
}),
- ]);
- });
+ ])
+ })
it("increases inventory_quantity when return is received", async () => {
- const api = useApi();
+ const api = useApi()
const returned = await api.post(
"/admin/orders/test-order/return",
@@ -854,24 +918,22 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
//Find variant that should have its inventory_quantity updated
- const toTest = returned.data.order.items.find(
- (i) => i.id === "test-item"
- );
+ const toTest = returned.data.order.items.find((i) => i.id === "test-item")
- expect(returned.status).toEqual(200);
- expect(toTest.variant.inventory_quantity).toEqual(2);
- });
+ expect(returned.status).toEqual(200)
+ expect(toTest.variant.inventory_quantity).toEqual(2)
+ })
it("does not increases inventory_quantity when return is received when inventory is not managed", async () => {
- const api = useApi();
- const manager = dbConnection.manager;
+ const api = useApi()
+ const manager = dbConnection.manager
await manager.query(
`UPDATE "product_variant" SET manage_inventory=false WHERE id = 'test-variant'`
- );
+ )
const returned = await api.post(
"/admin/orders/test-order/return",
@@ -889,57 +951,110 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
//Find variant that should have its inventory_quantity updated
- const toTest = returned.data.order.items.find(
- (i) => i.id === "test-item"
- );
+ const toTest = returned.data.order.items.find((i) => i.id === "test-item")
- expect(returned.status).toEqual(200);
- expect(toTest.variant.inventory_quantity).toEqual(1);
- });
- });
+ expect(returned.status).toEqual(200)
+ expect(toTest.variant.inventory_quantity).toEqual(1)
+ })
+ })
describe("GET /admin/orders", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
+ await adminSeeder(dbConnection)
// Manually insert date for filtering
- const createdAt = new Date("26 January 1997 12:00 UTC");
+ const createdAt = new Date("26 January 1997 12:00 UTC")
await orderSeeder(dbConnection, {
created_at: createdAt.toISOString(),
- });
+ })
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("lists all orders", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.get("/admin/orders?fields=id", {
headers: {
authorization: "Bearer test_token",
},
- });
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.orders).toEqual([
expect.objectContaining({
id: "test-order",
}),
- ]);
- });
+
+ expect.objectContaining({
+ id: "test-order-w-c",
+ }),
+
+ expect.objectContaining({
+ id: "test-order-w-s",
+ }),
+ expect.objectContaining({
+ id: "test-order-w-f",
+ }),
+ expect.objectContaining({
+ id: "test-order-w-r",
+ }),
+ ])
+ })
+
+ it("list all orders with matching order email", async () => {
+ const api = useApi()
+
+ const response = await api.get(
+ "/admin/orders?fields=id,email&q=test@email",
+ {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ }
+ )
+
+ expect(response.status).toEqual(200)
+ expect(response.data.count).toEqual(1)
+ expect(response.data.orders).toEqual([
+ expect.objectContaining({
+ id: "test-order",
+ email: "test@email.com",
+ }),
+ ])
+ })
+
+ it("list all orders with matching shipping_address first name", async () => {
+ const api = useApi()
+
+ const response = await api.get("/admin/orders?q=lebron", {
+ headers: {
+ authorization: "Bearer test_token",
+ },
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.count).toEqual(1)
+ expect(response.data.orders).toEqual([
+ expect.objectContaining({
+ id: "test-order",
+ shipping_address: expect.objectContaining({ first_name: "lebron" }),
+ }),
+ ])
+ })
it("successfully lists orders with greater than", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.get(
"/admin/orders?fields=id&created_at[gt]=01-26-1990",
@@ -948,18 +1063,31 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.orders).toEqual([
expect.objectContaining({
id: "test-order",
}),
- ]);
- });
+ expect.objectContaining({
+ id: "test-order-w-c",
+ }),
+
+ expect.objectContaining({
+ id: "test-order-w-s",
+ }),
+ expect.objectContaining({
+ id: "test-order-w-f",
+ }),
+ expect.objectContaining({
+ id: "test-order-w-r",
+ }),
+ ])
+ })
it("successfully lists no orders with greater than", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.get(
"/admin/orders?fields=id&created_at[gt]=01-26-2000",
@@ -968,14 +1096,14 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(response.status).toEqual(200);
- expect(response.data.orders).toEqual([]);
- });
+ expect(response.status).toEqual(200)
+ expect(response.data.orders).toEqual([])
+ })
it("successfully lists orders with less than", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.get(
"/admin/orders?fields=id&created_at[lt]=01-26-2000",
@@ -984,18 +1112,31 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.orders).toEqual([
expect.objectContaining({
id: "test-order",
}),
- ]);
- });
+ expect.objectContaining({
+ id: "test-order-w-c",
+ }),
+
+ expect.objectContaining({
+ id: "test-order-w-s",
+ }),
+ expect.objectContaining({
+ id: "test-order-w-f",
+ }),
+ expect.objectContaining({
+ id: "test-order-w-r",
+ }),
+ ])
+ })
it("successfully lists no orders with less than", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.get(
"/admin/orders?fields=id&created_at[lt]=01-26-1990",
@@ -1004,14 +1145,14 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(response.status).toEqual(200);
- expect(response.data.orders).toEqual([]);
- });
+ expect(response.status).toEqual(200)
+ expect(response.data.orders).toEqual([])
+ })
it("successfully lists orders using unix (greater than)", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.get(
"/admin/orders?fields=id&created_at[gt]=633351600",
@@ -1020,36 +1161,100 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.orders).toEqual([
expect.objectContaining({
id: "test-order",
}),
- ]);
- });
- });
+ expect.objectContaining({
+ id: "test-order-w-c",
+ }),
+
+ expect.objectContaining({
+ id: "test-order-w-s",
+ }),
+ expect.objectContaining({
+ id: "test-order-w-f",
+ }),
+ expect.objectContaining({
+ id: "test-order-w-r",
+ }),
+ ])
+ })
+
+ it.each([
+ [
+ "returns",
+ "test-order-w-r",
+ (o) => o.returns,
+ (r) => `/admin/returns/${r.id}/cancel`,
+ ],
+ [
+ "swaps",
+ "test-order-w-s",
+ (o) => o.swaps,
+ (s) => `/admin/orders/test-order-w-s/swaps/${s.id}/cancel`,
+ ],
+ [
+ "claims",
+ "test-order-w-c",
+ (o) => o.claims,
+ (c) => `/admin/orders/test-order-w-c/claims/${c.id}/cancel`,
+ ],
+ [
+ "fulfillments",
+ "test-order-w-f",
+ (o) => o.fulfillments,
+ (f) => `/admin/orders/test-order-w-f/fulfillments/${f.id}/cancel`,
+ ],
+ ])(
+ "Only allows canceling order after canceling %s",
+ async (id, o, of, pf) => {
+ const order_id = o
+
+ const order = await callGet({
+ path: `/admin/orders/${order_id}`,
+ get: "order",
+ })
+
+ const expectCanceltoReturn = partial(expectPostCallToReturn, {
+ path: `/admin/orders/${order_id}/cancel`,
+ })
+
+ await expectCanceltoReturn({ code: 400 })
+
+ await expectAllPostCallsToReturn({
+ code: 200,
+ col: of(order),
+ pathf: pf,
+ })
+
+ await expectCanceltoReturn({ code: 200 })
+ }
+ )
+ })
describe("POST /admin/orders/:id/swaps", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await orderSeeder(dbConnection);
- await swapSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await orderSeeder(dbConnection)
+ await swapSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a swap", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api.post(
"/admin/orders/test-order/swaps",
@@ -1067,12 +1272,12 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
- expect(response.status).toEqual(200);
- });
+ )
+ expect(response.status).toEqual(200)
+ })
it("creates a swap and a return", async () => {
- const api = useApi();
+ const api = useApi()
const returnedOrderFirst = await api.post(
"/admin/orders/order-with-swap/return",
@@ -1090,9 +1295,9 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(returnedOrderFirst.status).toEqual(200);
+ expect(returnedOrderFirst.status).toEqual(200)
const returnedOrderSecond = await api.post(
"/admin/orders/order-with-swap/return",
@@ -1110,19 +1315,19 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
// find item to test returned quantiy for
const toTest = returnedOrderSecond.data.order.items.find(
(i) => i.id === "test-item-many"
- );
+ )
- expect(returnedOrderSecond.status).toEqual(200);
- expect(toTest.returned_quantity).toBe(3);
- });
+ expect(returnedOrderSecond.status).toEqual(200)
+ expect(toTest.returned_quantity).toBe(3)
+ })
it("creates a swap and receives the items", async () => {
- const api = useApi();
+ const api = useApi()
const createdSwapOrder = await api.post(
"/admin/orders/test-order/swaps",
@@ -1140,11 +1345,11 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(createdSwapOrder.status).toEqual(200);
+ expect(createdSwapOrder.status).toEqual(200)
- const swap = createdSwapOrder.data.order.swaps[0];
+ const swap = createdSwapOrder.data.order.swaps[0]
const receivedSwap = await api.post(
`/admin/returns/${swap.return_order.id}/receive`,
@@ -1161,14 +1366,14 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(receivedSwap.status).toEqual(200);
- expect(receivedSwap.data.return.status).toBe("received");
- });
+ expect(receivedSwap.status).toEqual(200)
+ expect(receivedSwap.data.return.status).toBe("received")
+ })
it("creates a swap on a swap", async () => {
- const api = useApi();
+ const api = useApi()
const swapOnSwap = await api.post(
"/admin/orders/order-with-swap/swaps",
@@ -1186,13 +1391,13 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(swapOnSwap.status).toEqual(200);
- });
+ expect(swapOnSwap.status).toEqual(200)
+ })
it("receives a swap on swap", async () => {
- const api = useApi();
+ const api = useApi()
const received = await api.post(
`/admin/returns/return-on-swap/receive`,
@@ -1209,13 +1414,13 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(received.status).toEqual(200);
- });
+ expect(received.status).toEqual(200)
+ })
it("creates a return on a swap", async () => {
- const api = useApi();
+ const api = useApi()
const returnOnSwap = await api.post(
"/admin/orders/order-with-swap/return",
@@ -1232,13 +1437,13 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(returnOnSwap.status).toEqual(200);
- });
+ expect(returnOnSwap.status).toEqual(200)
+ })
it("creates a return on an order", async () => {
- const api = useApi();
+ const api = useApi()
const returnOnOrder = await api.post(
"/admin/orders/test-order/return",
@@ -1255,9 +1460,9 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(returnOnOrder.status).toEqual(200);
+ expect(returnOnOrder.status).toEqual(200)
const captured = await api.post(
"/admin/orders/test-order/capture",
@@ -1267,9 +1472,9 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- const returnId = returnOnOrder.data.order.returns[0].id;
+ const returnId = returnOnOrder.data.order.returns[0].id
const received = await api.post(
`/admin/returns/${returnId}/receive`,
@@ -1286,9 +1491,63 @@ describe("/admin/orders", () => {
authorization: "Bearer test_token",
},
}
- );
+ )
- expect(received.status).toEqual(200);
- });
- });
-});
+ expect(received.status).toEqual(200)
+ })
+
+ it("Only allows canceling swap after canceling fulfillments", async () => {
+ try {
+ const swap_id = "swap-w-f"
+
+ const swap = await callGet({
+ path: `/admin/swaps/${swap_id}`,
+ get: "swap",
+ })
+
+ const { order_id } = swap
+
+ const expectCancelToReturn = partial(expectPostCallToReturn, {
+ path: `/admin/orders/${order_id}/swaps/${swap_id}/cancel`,
+ })
+
+ await expectCancelToReturn({ code: 400 })
+
+ await expectAllPostCallsToReturn({
+ code: 200,
+ col: swap.fulfillments,
+ pathf: (f) =>
+ `/admin/orders/${order_id}/swaps/${swap_id}/fulfillments/${f.id}/cancel`,
+ })
+
+ await expectCancelToReturn({ code: 200 })
+ } catch (e) {
+ console.log(e)
+ }
+ })
+
+ it("Only allows canceling swap after canceling return", async () => {
+ const swap_id = "swap-w-r"
+
+ const swap = await callGet({
+ path: `/admin/swaps/${swap_id}`,
+ get: "swap",
+ })
+
+ const { order_id } = swap
+
+ const expectCancelToReturn = partial(expectPostCallToReturn, {
+ path: `/admin/orders/${order_id}/swaps/${swap_id}/cancel`,
+ })
+
+ await expectCancelToReturn({ code: 400 })
+
+ await expectPostCallToReturn({
+ code: 200,
+ path: `/admin/returns/${swap.return_order.id}/cancel`,
+ })
+
+ await expectCancelToReturn({ code: 200 })
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js
index 7b0696e02c..1e398b65c5 100644
--- a/integration-tests/api/__tests__/admin/product.js
+++ b/integration-tests/api/__tests__/admin/product.js
@@ -1,52 +1,336 @@
-const path = require("path");
+const path = require("path")
-const setupServer = require("../../../helpers/setup-server");
-const { useApi } = require("../../../helpers/use-api");
-const { initDb, useDb } = require("../../../helpers/use-db");
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
-const adminSeeder = require("../../helpers/admin-seeder");
-const productSeeder = require("../../helpers/product-seeder");
+const adminSeeder = require("../../helpers/admin-seeder")
+const productSeeder = require("../../helpers/product-seeder")
-jest.setTimeout(30000);
+jest.setTimeout(50000)
describe("/admin/products", () => {
- let medusaProcess;
- let dbConnection;
+ let medusaProcess
+ let dbConnection
beforeAll(async () => {
- const cwd = path.resolve(path.join(__dirname, "..", ".."));
- dbConnection = await initDb({ cwd });
- medusaProcess = await setupServer({ cwd });
- });
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
afterAll(async () => {
- const db = useDb();
- await db.shutdown();
+ const db = useDb()
+ await db.shutdown()
- medusaProcess.kill();
- });
+ medusaProcess.kill()
+ })
+
+ describe("GET /admin/products", () => {
+ beforeEach(async () => {
+ try {
+ await productSeeder(dbConnection)
+ await adminSeeder(dbConnection)
+ } catch (err) {
+ console.log(err)
+ throw err
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("returns a list of products with all statuses when no status or invalid status is provided", async () => {
+ const api = useApi()
+
+ const res = await api
+ .get("/admin/products?status%5B%5D=null", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(res.status).toEqual(200)
+ expect(res.data.products).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "test-product",
+ status: "draft",
+ }),
+ expect.objectContaining({
+ id: "test-product1",
+ status: "draft",
+ }),
+ ])
+ )
+ })
+
+ it("returns a list of products where status is proposed", async () => {
+ const api = useApi()
+
+ const payload = {
+ status: "proposed",
+ }
+
+ //update test-product status to proposed
+ await api
+ .post("/admin/products/test-product", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ const response = await api
+ .get("/admin/products?status%5B%5D=proposed", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.products).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "test-product",
+ status: "proposed",
+ }),
+ ])
+ )
+ })
+
+ it("returns a list of products with child entities", async () => {
+ const api = useApi()
+
+ const response = await api
+ .get("/admin/products", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.data.products).toMatchSnapshot([
+ {
+ id: expect.stringMatching(/^test-*/),
+ created_at: expect.any(String),
+ options: [
+ {
+ id: expect.stringMatching(/^test-*/),
+ product_id: expect.stringMatching(/^test-*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ images: [
+ {
+ id: expect.stringMatching(/^test-*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ variants: [
+ {
+ id: "test-variant", //expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ product_id: expect.stringMatching(/^test-*/),
+ prices: [
+ {
+ id: expect.stringMatching(/^test-price*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ options: [
+ {
+ id: expect.stringMatching(/^test-variant-option*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ option_id: expect.stringMatching(/^test-opt*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ },
+ {
+ id: "test-variant_2", //expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ product_id: expect.stringMatching(/^test-*/),
+ prices: [
+ {
+ id: expect.stringMatching(/^test-price*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ options: [
+ {
+ id: expect.stringMatching(/^test-variant-option*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ option_id: expect.stringMatching(/^test-opt*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ },
+ {
+ id: "test-variant_1", // expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ product_id: expect.stringMatching(/^test-*/),
+ prices: [
+ {
+ id: expect.stringMatching(/^test-price*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ options: [
+ {
+ id: expect.stringMatching(/^test-variant-option*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ option_id: expect.stringMatching(/^test-opt*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ },
+ ],
+ tags: [
+ {
+ id: expect.stringMatching(/^tag*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ type: {
+ id: expect.stringMatching(/^test-*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ collection: {
+ id: expect.stringMatching(/^test-*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ profile_id: expect.stringMatching(/^sp_*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ {
+ id: expect.stringMatching(/^test-*/),
+ created_at: expect.any(String),
+ options: [],
+ variants: [
+ {
+ id: "test-variant_4", //expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ product_id: expect.stringMatching(/^test-*/),
+ prices: [
+ {
+ id: expect.stringMatching(/^test-price*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ options: [
+ {
+ id: expect.stringMatching(/^test-variant-option*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ option_id: expect.stringMatching(/^test-opt*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ },
+ {
+ id: "test-variant_3", //expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ product_id: expect.stringMatching(/^test-*/),
+ prices: [
+ {
+ id: expect.stringMatching(/^test-price*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ options: [
+ {
+ id: expect.stringMatching(/^test-variant-option*/),
+ variant_id: expect.stringMatching(/^test-variant*/),
+ option_id: expect.stringMatching(/^test-opt*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ },
+ ],
+ tags: [
+ {
+ id: expect.stringMatching(/^tag*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ],
+ type: {
+ id: expect.stringMatching(/^test-*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ collection: {
+ id: expect.stringMatching(/^test-*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ profile_id: expect.stringMatching(/^sp_*/),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ },
+ ])
+ })
+ })
describe("POST /admin/products", () => {
beforeEach(async () => {
try {
- await productSeeder(dbConnection);
- await adminSeeder(dbConnection);
+ await productSeeder(dbConnection)
+ await adminSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a product", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
- title: "Test product",
+ title: "Test",
description: "test-product-description",
type: { value: "test-type" },
images: ["test-image.png", "test-image-2.png"],
@@ -61,7 +345,7 @@ describe("/admin/products", () => {
options: [{ value: "large" }, { value: "green" }],
},
],
- };
+ }
const response = await api
.post("/admin/products", payload, {
@@ -70,17 +354,17 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
-
- expect(response.status).toEqual(200);
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
- title: "Test product",
+ title: "Test",
discountable: true,
is_giftcard: false,
- handle: "test-product",
+ handle: "test",
+ status: "draft",
images: expect.arrayContaining([
expect.objectContaining({
url: "test-image.png",
@@ -133,11 +417,77 @@ describe("/admin/products", () => {
}),
],
})
- );
- });
+ )
+ })
+
+ it("Sets variant ranks when creating a product", async () => {
+ const api = useApi()
+
+ const payload = {
+ title: "Test product - 1",
+ description: "test-product-description 1",
+ type: { value: "test-type 1" },
+ images: ["test-image.png", "test-image-2.png"],
+ collection_id: "test-collection",
+ tags: [{ value: "123" }, { value: "456" }],
+ options: [{ title: "size" }, { title: "color" }],
+ variants: [
+ {
+ title: "Test variant 1",
+ inventory_quantity: 10,
+ prices: [{ currency_code: "usd", amount: 100 }],
+ options: [{ value: "large" }, { value: "green" }],
+ },
+ {
+ title: "Test variant 2",
+ inventory_quantity: 10,
+ prices: [{ currency_code: "usd", amount: 100 }],
+ options: [{ value: "large" }, { value: "green" }],
+ },
+ ],
+ }
+
+ const creationResponse = await api
+ .post("/admin/products", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(creationResponse.status).toEqual(200)
+
+ const productId = creationResponse.data.product.id
+
+ const response = await api
+ .get(`/admin/products/${productId}`, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.data.product).toEqual(
+ expect.objectContaining({
+ title: "Test product - 1",
+ variants: [
+ expect.objectContaining({
+ title: "Test variant 1",
+ }),
+ expect.objectContaining({
+ title: "Test variant 2",
+ }),
+ ],
+ })
+ )
+ })
it("creates a giftcard", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
title: "Test Giftcard",
@@ -151,7 +501,7 @@ describe("/admin/products", () => {
options: [{ value: "100" }],
},
],
- };
+ }
const response = await api
.post("/admin/products", payload, {
@@ -160,21 +510,21 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
title: "Test Giftcard",
discountable: false,
})
- );
- });
+ )
+ })
- it("updates a product (update prices, tags, delete collection, delete type, replaces images)", async () => {
- const api = useApi();
+ it("updates a product (update prices, tags, update status, delete collection, delete type, replaces images)", async () => {
+ const api = useApi()
const payload = {
collection_id: null,
@@ -182,13 +532,20 @@ describe("/admin/products", () => {
variants: [
{
id: "test-variant",
- prices: [{ currency_code: "usd", amount: 100, sale_amount: 75 }],
+ prices: [
+ {
+ currency_code: "usd",
+ amount: 100,
+ sale_amount: 75,
+ },
+ ],
},
],
tags: [{ value: "123" }],
images: ["test-image-2.png"],
type: { value: "test-type-2" },
- };
+ status: "published",
+ }
const response = await api
.post("/admin/products/test-product", payload, {
@@ -197,10 +554,10 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
@@ -226,20 +583,94 @@ describe("/admin/products", () => {
}),
],
type: null,
+ status: "published",
collection: null,
type: expect.objectContaining({
value: "test-type-2",
}),
})
- );
- });
+ )
+ })
+
+ it("fails to update product with invalid status", async () => {
+ const api = useApi()
+
+ const payload = {
+ status: null,
+ }
+
+ try {
+ await api.post("/admin/products/test-product", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ } catch (e) {
+ expect(e.response.status).toEqual(400)
+ expect(e.response.data.type).toEqual("invalid_data")
+ }
+ })
+
+ it("updates a product (variant ordering)", async () => {
+ const api = useApi()
+
+ const payload = {
+ collection_id: null,
+ type: null,
+ variants: [
+ {
+ id: "test-variant",
+ },
+ {
+ id: "test-variant_1",
+ },
+ {
+ id: "test-variant_2",
+ },
+ ],
+ }
+
+ const response = await api
+ .post("/admin/products/test-product", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+
+ expect(response.data.product).toEqual(
+ expect.objectContaining({
+ title: "Test product",
+ variants: [
+ expect.objectContaining({
+ id: "test-variant",
+ title: "Test variant",
+ }),
+ expect.objectContaining({
+ id: "test-variant_1",
+ title: "Test variant rank (1)",
+ }),
+ expect.objectContaining({
+ id: "test-variant_2",
+ title: "Test variant rank (2)",
+ }),
+ ],
+ type: null,
+ collection: null,
+ })
+ )
+ })
it("add option", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
title: "should_add",
- };
+ }
const response = await api
.post("/admin/products/test-product/options", payload, {
@@ -248,41 +679,41 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
- options: [
+ options: expect.arrayContaining([
expect.objectContaining({
title: "should_add",
product_id: "test-product",
}),
- ],
+ ]),
})
- );
- });
- });
+ )
+ })
+ })
describe("testing for soft-deletion + uniqueness on handles, collection and variant properties", () => {
beforeEach(async () => {
try {
- await productSeeder(dbConnection);
- await adminSeeder(dbConnection);
+ await productSeeder(dbConnection)
+ await adminSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("successfully deletes a product", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.delete("/admin/products/test-product", {
@@ -291,21 +722,21 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data).toEqual(
expect.objectContaining({
id: "test-product",
deleted: true,
})
- );
- });
+ )
+ })
- it("successfully creates product with soft-deleted product handle", async () => {
- const api = useApi();
+ it("successfully creates product with soft-deleted product handle and deletes it again", async () => {
+ const api = useApi()
// First we soft-delete the product
const response = await api
@@ -315,11 +746,11 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data.id).toEqual("test-product");
+ expect(response.status).toEqual(200)
+ expect(response.data.id).toEqual("test-product")
// Lets try to create a product with same handle as deleted one
const payload = {
@@ -339,20 +770,70 @@ describe("/admin/products", () => {
options: [{ value: "large" }, { value: "green" }],
},
],
- };
+ }
const res = await api.post("/admin/products", payload, {
headers: {
Authorization: "Bearer test_token",
},
- });
+ })
- expect(res.status).toEqual(200);
- expect(res.data.product.handle).toEqual("test-product");
- });
+ expect(res.status).toEqual(200)
+ expect(res.data.product.handle).toEqual("test-product")
+
+ // Delete product again to ensure uniqueness is enforced in all cases
+ const response2 = await api
+ .delete("/admin/products/test-product", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response2.status).toEqual(200)
+ expect(response2.data.id).toEqual("test-product")
+ })
+
+ it("should fail when creating a product with a handle that already exists", async () => {
+ const api = useApi()
+
+ // Lets try to create a product with same handle as deleted one
+ const payload = {
+ title: "Test product",
+ handle: "test-product",
+ description: "test-product-description",
+ type: { value: "test-type" },
+ images: ["test-image.png", "test-image-2.png"],
+ collection_id: "test-collection",
+ tags: [{ value: "123" }, { value: "456" }],
+ options: [{ title: "size" }, { title: "color" }],
+ variants: [
+ {
+ title: "Test variant",
+ inventory_quantity: 10,
+ prices: [{ currency_code: "usd", amount: 100 }],
+ options: [{ value: "large" }, { value: "green" }],
+ },
+ ],
+ }
+
+ try {
+ await api.post("/admin/products", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ } catch (error) {
+ expect(error.response.data.message).toMatch(
+ /duplicate key value violates unique constraint/i
+ )
+ }
+ })
it("successfully deletes product collection", async () => {
- const api = useApi();
+ const api = useApi()
// First we soft-delete the product collection
const response = await api
@@ -362,15 +843,15 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data.id).toEqual("test-collection");
- });
+ expect(response.status).toEqual(200)
+ expect(response.data.id).toEqual("test-collection")
+ })
it("successfully creates soft-deleted product collection", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.delete("/admin/collections/test-collection", {
@@ -379,30 +860,62 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data.id).toEqual("test-collection");
+ expect(response.status).toEqual(200)
+ expect(response.data.id).toEqual("test-collection")
// Lets try to create a product collection with same handle as deleted one
const payload = {
title: "Another test collection",
handle: "test-collection",
- };
+ }
const res = await api.post("/admin/collections", payload, {
headers: {
Authorization: "Bearer test_token",
},
- });
+ })
- expect(res.status).toEqual(200);
- expect(res.data.collection.handle).toEqual("test-collection");
- });
+ expect(res.status).toEqual(200)
+ expect(res.data.collection.handle).toEqual("test-collection")
+ })
+
+ it("should fail when creating a collection with a handle that already exists", async () => {
+ const api = useApi()
+
+ // Lets try to create a collection with same handle as deleted one
+ const payload = {
+ title: "Another test collection",
+ handle: "test-collection",
+ }
+
+ try {
+ await api.post("/admin/collections", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ } catch (error) {
+ expect(error.response.data.message).toMatch(
+ /duplicate key value violates unique constraint/i
+ )
+ }
+ })
it("successfully creates soft-deleted product variant", async () => {
- const api = useApi();
+ const api = useApi()
+
+ const product = await api
+ .get("/admin/products/test-product", {
+ headers: {
+ Authorization: "bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
const response = await api
.delete("/admin/products/test-product/variants/test-variant", {
@@ -411,13 +924,12 @@ describe("/admin/products", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data.variant_id).toEqual("test-variant");
+ expect(response.status).toEqual(200)
+ expect(response.data.variant_id).toEqual("test-variant")
- // Lets try to create a product collection with same handle as deleted one
const payload = {
title: "Second variant",
sku: "test-sku",
@@ -430,19 +942,18 @@ describe("/admin/products", () => {
amount: 100,
},
],
- };
+ options: [{ option_id: "test-option", value: "inserted value" }],
+ }
- const res = await api.post(
- "/admin/products/test-product/variants",
- payload,
- {
+ const res = await api
+ .post("/admin/products/test-product/variants", payload, {
headers: {
Authorization: "Bearer test_token",
},
- }
- );
+ })
+ .catch((err) => console.log(err))
- expect(res.status).toEqual(200);
+ expect(res.status).toEqual(200)
expect(res.data.product.variants).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -453,7 +964,7 @@ describe("/admin/products", () => {
barcode: "test-barcode",
}),
])
- );
- });
- });
-});
+ )
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/return-reason.js b/integration-tests/api/__tests__/admin/return-reason.js
index 6255c3bfac..94e77e0745 100644
--- a/integration-tests/api/__tests__/admin/return-reason.js
+++ b/integration-tests/api/__tests__/admin/return-reason.js
@@ -1,53 +1,55 @@
-const path = require("path");
+const { match } = require("assert")
+const path = require("path")
+const { RepositoryNotTreeError } = require("typeorm")
-const setupServer = require("../../../helpers/setup-server");
-const { useApi } = require("../../../helpers/use-api");
-const { initDb, useDb } = require("../../../helpers/use-db");
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
-const adminSeeder = require("../../helpers/admin-seeder");
+const adminSeeder = require("../../helpers/admin-seeder")
-jest.setTimeout(30000);
+jest.setTimeout(30000)
describe("/admin/return-reasons", () => {
- let medusaProcess;
- let dbConnection;
+ let medusaProcess
+ let dbConnection
beforeAll(async () => {
- const cwd = path.resolve(path.join(__dirname, "..", ".."));
- dbConnection = await initDb({ cwd });
- medusaProcess = await setupServer({ cwd });
- });
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
afterAll(async () => {
- const db = useDb();
- await db.shutdown();
+ const db = useDb()
+ await db.shutdown()
- medusaProcess.kill();
- });
+ medusaProcess.kill()
+ })
describe("POST /admin/return-reasons", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
+ await adminSeeder(dbConnection)
} catch (err) {
- console.log(err);
- throw err;
+ console.log(err)
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("creates a return_reason", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
- };
+ }
const response = await api
.post("/admin/return-reasons", payload, {
@@ -56,10 +58,172 @@ describe("/admin/return-reasons", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
+
+ expect(response.data.return_reason).toMatchSnapshot({
+ id: expect.any(String),
+ created_at: expect.any(String),
+ updated_at: expect.any(String),
+ parent_return_reason: null,
+ parent_return_reason_id: null,
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ })
+ })
+
+ it("creates a nested return reason", async () => {
+ const api = useApi()
+
+ const payload = {
+ label: "Wrong size",
+ description: "Use this if the size was too big",
+ value: "wrong_size",
+ }
+
+ const response = await api
+ .post("/admin/return-reasons", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+
+ expect(response.data.return_reason).toEqual(
+ expect.objectContaining({
+ label: "Wrong size",
+ description: "Use this if the size was too big",
+ value: "wrong_size",
+ })
+ )
+
+ const nested_payload = {
+ parent_return_reason_id: response.data.return_reason.id,
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ }
+
+ const nested_response = await api
+ .post("/admin/return-reasons", nested_payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(nested_response.status).toEqual(200)
+
+ expect(nested_response.data.return_reason).toEqual(
+ expect.objectContaining({
+ parent_return_reason_id: response.data.return_reason.id,
+
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ })
+ )
+ })
+
+ it("fails to create a doubly nested return reason", async () => {
+ expect.assertions(5)
+
+ const api = useApi()
+
+ const payload = {
+ label: "Wrong size",
+ description: "Use this if the size was too big",
+ value: "wrong_size",
+ }
+
+ const response = await api
+ .post("/admin/return-reasons", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+
+ expect(response.data.return_reason).toEqual(
+ expect.objectContaining({
+ label: "Wrong size",
+ description: "Use this if the size was too big",
+ value: "wrong_size",
+ })
+ )
+
+ const nested_payload = {
+ parent_return_reason_id: response.data.return_reason.id,
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ }
+
+ const nested_response = await api
+ .post("/admin/return-reasons", nested_payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ const dbl_nested_payload = {
+ parent_return_reason_id: nested_response.data.return_reason.id,
+ label: "Too large size",
+ description: "Use this if the size was too big",
+ value: "large_size",
+ }
+
+ const dbl_nested_response = await api
+ .post("/admin/return-reasons", dbl_nested_payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ expect(err.response.status).toEqual(400)
+ expect(err.response.data.type).toEqual("invalid_data")
+ expect(err.response.data.message).toEqual(
+ "Doubly nested return reasons is not supported"
+ )
+ })
+ })
+
+ it("deletes a return_reason", async () => {
+ const api = useApi()
+
+ const payload = {
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ }
+
+ const response = await api
+ .post("/admin/return-reasons", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
expect(response.data.return_reason).toEqual(
expect.objectContaining({
@@ -67,17 +231,37 @@ describe("/admin/return-reasons", () => {
description: "Use this if the size was too big",
value: "too_big",
})
- );
- });
+ )
+
+ const deleteResponse = await api
+ .delete(`/admin/return-reasons/${response.data.return_reason.id}`, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+
+ expect(deleteResponse.data).toEqual(
+ expect.objectContaining({
+ id: response.data.return_reason.id,
+ object: "return_reason",
+ deleted: true,
+ })
+ )
+ })
it("update a return reason", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
label: "Too Big Typo",
description: "Use this if the size was too big",
value: "too_big",
- };
+ }
const response = await api
.post("/admin/return-reasons", payload, {
@@ -86,10 +270,10 @@ describe("/admin/return-reasons", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.return_reason).toEqual(
expect.objectContaining({
@@ -97,7 +281,7 @@ describe("/admin/return-reasons", () => {
description: "Use this if the size was too big",
value: "too_big",
})
- );
+ )
const newResponse = await api
.post(
@@ -113,8 +297,8 @@ describe("/admin/return-reasons", () => {
}
)
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
expect(newResponse.data.return_reason).toEqual(
expect.objectContaining({
@@ -122,17 +306,81 @@ describe("/admin/return-reasons", () => {
description: "new desc",
value: "too_big",
})
- );
- });
+ )
+ })
+
+ it("lists nested return reasons", async () => {
+ const api = useApi()
+
+ const payload = {
+ label: "Wrong size",
+ description: "Use this if the size was too big",
+ value: "wrong_size",
+ }
+
+ const response = await api
+ .post("/admin/return-reasons", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ const nested_payload = {
+ parent_return_reason_id: response.data.return_reason.id,
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ }
+
+ const resp = await api
+ .post("/admin/return-reasons", nested_payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ const nested_response = await api
+ .get("/admin/return-reasons", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(nested_response.status).toEqual(200)
+
+ expect(nested_response.data.return_reasons).toEqual([
+ expect.objectContaining({
+ label: "Wrong size",
+ description: "Use this if the size was too big",
+ value: "wrong_size",
+ return_reason_children: expect.arrayContaining([
+ expect.objectContaining({
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ }),
+ ]),
+ }),
+ ])
+ })
it("list return reasons", async () => {
- const api = useApi();
+ const api = useApi()
const payload = {
label: "Too Big Typo",
description: "Use this if the size was too big",
value: "too_big",
- };
+ }
await api
.post("/admin/return-reasons", payload, {
@@ -141,8 +389,8 @@ describe("/admin/return-reasons", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
const response = await api
.get("/admin/return-reasons", {
@@ -151,15 +399,191 @@ describe("/admin/return-reasons", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
+ expect(response.status).toEqual(200)
expect(response.data.return_reasons).toEqual([
expect.objectContaining({
value: "too_big",
}),
- ]);
- });
- });
-});
+ ])
+ })
+ })
+
+ describe("DELETE /admin/return-reasons", () => {
+ beforeEach(async () => {
+ try {
+ await adminSeeder(dbConnection)
+ } catch (err) {
+ console.log(err)
+ throw err
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("deletes single return reason", async () => {
+ expect.assertions(6)
+
+ const api = useApi()
+
+ const payload = {
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ }
+
+ const response = await api
+ .post("/admin/return-reasons", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+
+ expect(response.data.return_reason).toEqual(
+ expect.objectContaining({
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ })
+ )
+
+ const deleteResult = await api.delete(
+ `/admin/return-reasons/${response.data.return_reason.id}`,
+ {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ }
+ )
+
+ expect(deleteResult.status).toEqual(200)
+
+ expect(deleteResult.data).toEqual({
+ id: response.data.return_reason.id,
+ object: "return_reason",
+ deleted: true,
+ })
+
+ const getResult = await api
+ .get(`/admin/return-reasons/${response.data.return_reason.id}`, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ expect(err.response.status).toEqual(404)
+ expect(err.response.data.type).toEqual("not_found")
+ })
+ })
+
+ it("deletes cascade through nested return reasons", async () => {
+ expect.assertions(10)
+
+ const api = useApi()
+
+ const payload = {
+ label: "Wrong Size",
+ description: "Use this if the size was wrong",
+ value: "wrong_size",
+ }
+
+ const response = await api
+ .post("/admin/return-reasons", payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+
+ expect(response.data.return_reason).toEqual(
+ expect.objectContaining({
+ label: "Wrong Size",
+ description: "Use this if the size was wrong",
+ value: "wrong_size",
+ })
+ )
+
+ const payload_child = {
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ parent_return_reason_id: response.data.return_reason.id,
+ }
+
+ const response_child = await api
+ .post("/admin/return-reasons", payload_child, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response_child.status).toEqual(200)
+
+ expect(response_child.data.return_reason).toEqual(
+ expect.objectContaining({
+ label: "Too Big",
+ description: "Use this if the size was too big",
+ value: "too_big",
+ parent_return_reason_id: response.data.return_reason.id,
+ })
+ )
+
+ const deleteResult = await api
+ .delete(`/admin/return-reasons/${response.data.return_reason.id}`, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err.response.data)
+ })
+
+ expect(deleteResult.status).toEqual(200)
+
+ expect(deleteResult.data).toEqual({
+ id: response.data.return_reason.id,
+ object: "return_reason",
+ deleted: true,
+ })
+
+ await api
+ .get(`/admin/return-reasons/${response.data.return_reason.id}`, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ expect(err.response.status).toEqual(404)
+ expect(err.response.data.type).toEqual("not_found")
+ })
+
+ await api
+ .get(`/admin/return-reasons/${response_child.data.return_reason.id}`, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ expect(err.response.status).toEqual(404)
+ expect(err.response.data.type).toEqual("not_found")
+ })
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/shipping-options.js b/integration-tests/api/__tests__/admin/shipping-options.js
new file mode 100644
index 0000000000..4b0f99f8d5
--- /dev/null
+++ b/integration-tests/api/__tests__/admin/shipping-options.js
@@ -0,0 +1,386 @@
+const path = require("path")
+const {
+ Region,
+ ShippingProfile,
+ ShippingOption,
+ ShippingOptionRequirement,
+} = require("@medusajs/medusa")
+
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
+const adminSeeder = require("../../helpers/admin-seeder")
+const shippingOptionSeeder = require("../../helpers/shipping-option-seeder")
+
+jest.setTimeout(30000)
+
+describe("/admin/shipping-options", () => {
+ let medusaProcess
+ let dbConnection
+
+ beforeAll(async () => {
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
+
+ afterAll(async () => {
+ const db = useDb()
+ await db.shutdown()
+ medusaProcess.kill()
+ })
+
+ describe("POST /admin/shipping-options/:id", () => {
+ beforeEach(async () => {
+ try {
+ await adminSeeder(dbConnection)
+ await shippingOptionSeeder(dbConnection)
+ } catch (err) {
+ console.error(err)
+ throw err
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("updates a shipping option with no existing requirements", async () => {
+ const api = useApi()
+
+ const payload = {
+ name: "Test option",
+ amount: 100,
+ requirements: [
+ {
+ type: "min_subtotal",
+ amount: 1,
+ },
+ {
+ type: "max_subtotal",
+ amount: 2,
+ },
+ ],
+ }
+
+ const res = await api.post(`/admin/shipping-options/test-out`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+
+ const requirements = res.data.shipping_option.requirements
+
+ expect(res.status).toEqual(200)
+ expect(requirements.length).toEqual(2)
+ expect(requirements[0]).toEqual(
+ expect.objectContaining({
+ type: "min_subtotal",
+ shipping_option_id: "test-out",
+ amount: 1,
+ })
+ )
+ expect(requirements[1]).toEqual(
+ expect.objectContaining({
+ type: "max_subtotal",
+ shipping_option_id: "test-out",
+ amount: 2,
+ })
+ )
+ })
+
+ it("fails as it is not allowed to set id from client side", async () => {
+ const api = useApi()
+
+ const payload = {
+ name: "Test option",
+ amount: 100,
+ requirements: [
+ {
+ id: "not_allowed",
+ type: "min_subtotal",
+ amount: 1,
+ },
+ {
+ id: "really_not_allowed",
+ type: "max_subtotal",
+ amount: 2,
+ },
+ ],
+ }
+
+ const res = await api
+ .post(`/admin/shipping-options/test-out`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ return err.response
+ })
+
+ expect(res.status).toEqual(400)
+ expect(res.data.message).toEqual("ID does not exist")
+ })
+
+ it("it succesfully updates a set of existing requirements", async () => {
+ const api = useApi()
+
+ const payload = {
+ requirements: [
+ {
+ id: "option-req",
+ type: "min_subtotal",
+ amount: 15,
+ },
+ {
+ id: "option-req-2",
+ type: "max_subtotal",
+ amount: 20,
+ },
+ ],
+ amount: 200,
+ }
+
+ const res = await api
+ .post(`/admin/shipping-options/test-option-req`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err.response.data.message)
+ })
+
+ expect(res.status).toEqual(200)
+ })
+
+ it("it succesfully updates a set of existing requirements by updating one and deleting the other", async () => {
+ const api = useApi()
+
+ const payload = {
+ requirements: [
+ {
+ id: "option-req",
+ type: "min_subtotal",
+ amount: 15,
+ },
+ ],
+ }
+
+ const res = await api
+ .post(`/admin/shipping-options/test-option-req`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err.response.data.message)
+ })
+
+ expect(res.status).toEqual(200)
+ })
+
+ it("succesfully updates a set of requirements because max. subtotal >= min. subtotal", async () => {
+ const api = useApi()
+
+ const payload = {
+ requirements: [
+ {
+ id: "option-req",
+ type: "min_subtotal",
+ amount: 150,
+ },
+ {
+ id: "option-req-2",
+ type: "max_subtotal",
+ amount: 200,
+ },
+ ],
+ }
+
+ const res = await api
+ .post(`/admin/shipping-options/test-option-req`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err.response.data.message)
+ })
+
+ expect(res.status).toEqual(200)
+ expect(res.data.shipping_option.requirements[0].amount).toEqual(150)
+ expect(res.data.shipping_option.requirements[1].amount).toEqual(200)
+ })
+
+ it("fails to updates a set of requirements because max. subtotal <= min. subtotal", async () => {
+ const api = useApi()
+
+ const payload = {
+ requirements: [
+ {
+ id: "option-req",
+ type: "min_subtotal",
+ amount: 1500,
+ },
+ {
+ id: "option-req-2",
+ type: "max_subtotal",
+ amount: 200,
+ },
+ ],
+ }
+
+ const res = await api
+ .post(`/admin/shipping-options/test-option-req`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ return err.response
+ })
+
+ expect(res.status).toEqual(400)
+ expect(res.data.message).toEqual(
+ "Max. subtotal must be greater than Min. subtotal"
+ )
+ })
+ })
+
+ describe("POST /admin/shipping-options", () => {
+ let payload
+
+ beforeEach(async () => {
+ try {
+ await adminSeeder(dbConnection)
+ await shippingOptionSeeder(dbConnection)
+
+ const api = useApi()
+ await api.post(
+ `/admin/regions/region`,
+ {
+ fulfillment_providers: ["test-ful"],
+ },
+ {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ }
+ )
+
+ const manager = dbConnection.manager
+ const defaultProfile = await manager.findOne(ShippingProfile, {
+ type: "default",
+ })
+
+ payload = {
+ name: "Test option",
+ amount: 100,
+ price_type: "flat_rate",
+ region_id: "region",
+ provider_id: "test-ful",
+ data: {},
+ profile_id: defaultProfile.id,
+ }
+ } catch (err) {
+ console.error(err)
+ throw err
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("creates a shipping option with requirements", async () => {
+ const api = useApi()
+ payload.requirements = [
+ {
+ type: "max_subtotal",
+ amount: 2,
+ },
+ {
+ type: "min_subtotal",
+ amount: 1,
+ },
+ ]
+
+ const res = await api.post(`/admin/shipping-options`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+
+ expect(res.status).toEqual(200)
+ expect(res.data.shipping_option.requirements.length).toEqual(2)
+ })
+
+ it("creates a shipping option with no requirements", async () => {
+ const api = useApi()
+ const res = await api.post(`/admin/shipping-options`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+
+ expect(res.status).toEqual(200)
+ expect(res.data.shipping_option.requirements.length).toEqual(0)
+ })
+
+ it("fails on same requirement types", async () => {
+ const api = useApi()
+ payload.requirements = [
+ {
+ type: "max_subtotal",
+ amount: 2,
+ },
+ {
+ type: "max_subtotal",
+ amount: 1,
+ },
+ ]
+
+ try {
+ await api.post(`/admin/shipping-options`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ } catch (error) {
+ expect(error.response.data.message).toEqual(
+ "Only one requirement of each type is allowed"
+ )
+ }
+ })
+
+ it("fails when min_subtotal > max_subtotal", async () => {
+ const api = useApi()
+ payload.requirements = [
+ {
+ type: "max_subtotal",
+ amount: 2,
+ },
+ {
+ type: "min_subtotal",
+ amount: 4,
+ },
+ ]
+
+ try {
+ await api.post(`/admin/shipping-options`, payload, {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ } catch (error) {
+ expect(error.response.data.message).toEqual(
+ "Max. subtotal must be greater than Min. subtotal"
+ )
+ }
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/swaps.js b/integration-tests/api/__tests__/admin/swaps.js
index efcc142e65..78ec6db9e8 100644
--- a/integration-tests/api/__tests__/admin/swaps.js
+++ b/integration-tests/api/__tests__/admin/swaps.js
@@ -1,49 +1,49 @@
-const path = require("path");
+const path = require("path")
-const setupServer = require("../../../helpers/setup-server");
-const { useApi } = require("../../../helpers/use-api");
-const { initDb, useDb } = require("../../../helpers/use-db");
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
-const orderSeeder = require("../../helpers/order-seeder");
-const swapSeeder = require("../../helpers/swap-seeder");
-const adminSeeder = require("../../helpers/admin-seeder");
+const orderSeeder = require("../../helpers/order-seeder")
+const swapSeeder = require("../../helpers/swap-seeder")
+const adminSeeder = require("../../helpers/admin-seeder")
-jest.setTimeout(30000);
+jest.setTimeout(30000)
describe("/admin/swaps", () => {
- let medusaProcess;
- let dbConnection;
+ let medusaProcess
+ let dbConnection
beforeAll(async () => {
- const cwd = path.resolve(path.join(__dirname, "..", ".."));
- dbConnection = await initDb({ cwd });
- medusaProcess = await setupServer({ cwd });
- });
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
afterAll(async () => {
- const db = useDb();
- await db.shutdown();
- medusaProcess.kill();
- });
+ const db = useDb()
+ await db.shutdown()
+ medusaProcess.kill()
+ })
describe("GET /admin/swaps/:id", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await orderSeeder(dbConnection);
- await swapSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await orderSeeder(dbConnection)
+ await swapSeeder(dbConnection)
} catch (err) {
- throw err;
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("gets a swap with cart and totals", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.get("/admin/swaps/test-swap", {
@@ -52,46 +52,46 @@ describe("/admin/swaps", () => {
},
})
.catch((err) => {
- console.log(err);
- });
- expect(response.status).toEqual(200);
+ console.log(err)
+ })
+ expect(response.status).toEqual(200)
expect(response.data.swap).toEqual(
expect.objectContaining({
id: "test-swap",
})
- );
+ )
expect(response.data.swap.cart).toEqual(
expect.objectContaining({
- id: "test-cart",
+ id: "test-cart-w-swap",
shipping_total: 1000,
subtotal: 1000,
total: 2000,
})
- );
- expect(response.data.swap.cart).toHaveProperty("discount_total");
- expect(response.data.swap.cart).toHaveProperty("gift_card_total");
- });
- });
+ )
+ expect(response.data.swap.cart).toHaveProperty("discount_total")
+ expect(response.data.swap.cart).toHaveProperty("gift_card_total")
+ })
+ })
describe("GET /admin/swaps/", () => {
beforeEach(async () => {
try {
- await adminSeeder(dbConnection);
- await orderSeeder(dbConnection);
- await swapSeeder(dbConnection);
+ await adminSeeder(dbConnection)
+ await orderSeeder(dbConnection)
+ await swapSeeder(dbConnection)
} catch (err) {
- throw err;
+ throw err
}
- });
+ })
afterEach(async () => {
- const db = useDb();
- await db.teardown();
- });
+ const db = useDb()
+ await db.teardown()
+ })
it("lists all swaps", async () => {
- const api = useApi();
+ const api = useApi()
const response = await api
.get("/admin/swaps/", {
@@ -100,18 +100,18 @@ describe("/admin/swaps", () => {
},
})
.catch((err) => {
- console.log(err);
- });
+ console.log(err)
+ })
- expect(response.status).toEqual(200);
- expect(response.data).toHaveProperty("count");
- expect(response.data.offset).toBe(0);
- expect(response.data.limit).toBe(50);
+ expect(response.status).toEqual(200)
+ expect(response.data).toHaveProperty("count")
+ expect(response.data.offset).toBe(0)
+ expect(response.data.limit).toBe(50)
expect(response.data.swaps).toContainEqual(
expect.objectContaining({
id: "test-swap",
})
- );
- });
- });
-});
+ )
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/admin/variant.js b/integration-tests/api/__tests__/admin/variant.js
new file mode 100644
index 0000000000..a0244141c1
--- /dev/null
+++ b/integration-tests/api/__tests__/admin/variant.js
@@ -0,0 +1,153 @@
+const path = require("path")
+
+const setupServer = require("../../../helpers/setup-server")
+const { useApi } = require("../../../helpers/use-api")
+const { initDb, useDb } = require("../../../helpers/use-db")
+
+const adminSeeder = require("../../helpers/admin-seeder")
+const productSeeder = require("../../helpers/product-seeder")
+
+jest.setTimeout(30000)
+
+describe("/admin/products", () => {
+ let medusaProcess
+ let dbConnection
+
+ beforeAll(async () => {
+ const cwd = path.resolve(path.join(__dirname, "..", ".."))
+ dbConnection = await initDb({ cwd })
+ medusaProcess = await setupServer({ cwd })
+ })
+
+ afterAll(async () => {
+ const db = useDb()
+ await db.shutdown()
+
+ medusaProcess.kill()
+ })
+
+ describe("GET /admin/product-variants", () => {
+ beforeEach(async () => {
+ try {
+ await productSeeder(dbConnection)
+ await adminSeeder(dbConnection)
+ } catch (err) {
+ console.log(err)
+ throw err
+ }
+ })
+
+ afterEach(async () => {
+ const db = useDb()
+ await db.teardown()
+ })
+
+ it("lists all product variants", async () => {
+ const api = useApi()
+
+ const response = await api
+ .get("/admin/variants/", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.variants).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining(
+ {
+ id: "test-variant",
+ },
+ {
+ id: "test-variant_2",
+ },
+ {
+ id: "test-variant_1",
+ }
+ ),
+ ])
+ )
+ })
+
+ it("lists all product variants matching a specific sku", async () => {
+ const api = useApi()
+ const response = await api
+ .get("/admin/variants?q=sku2", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.variants.length).toEqual(1)
+ expect(response.data.variants).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ sku: "test-sku2",
+ }),
+ ])
+ )
+ })
+
+ it("lists all product variants matching a specific variant title", async () => {
+ const api = useApi()
+ const response = await api
+ .get("/admin/variants?q=rank (1)", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.variants.length).toEqual(1)
+ expect(response.data.variants).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: "test-variant_1",
+ sku: "test-sku1",
+ }),
+ ])
+ )
+ })
+
+ it("lists all product variants matching a specific product title", async () => {
+ const api = useApi()
+ const response = await api
+ .get("/admin/variants?q=Test product1", {
+ headers: {
+ Authorization: "Bearer test_token",
+ },
+ })
+ .catch((err) => {
+ console.log(err)
+ })
+
+ expect(response.status).toEqual(200)
+ expect(response.data.variants.length).toEqual(2)
+ expect(response.data.variants).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ product_id: "test-product1",
+ id: "test-variant_3",
+ sku: "test-sku3",
+ }),
+ expect.objectContaining({
+ product_id: "test-product1",
+ id: "test-variant_4",
+ sku: "test-sku4",
+ }),
+ ])
+ )
+ })
+ })
+})
diff --git a/integration-tests/api/__tests__/store/__snapshots__/cart.js.snap b/integration-tests/api/__tests__/store/__snapshots__/cart.js.snap
index 9f6476ca0a..520fe3a3ae 100644
--- a/integration-tests/api/__tests__/store/__snapshots__/cart.js.snap
+++ b/integration-tests/api/__tests__/store/__snapshots__/cart.js.snap
@@ -8,6 +8,14 @@ Object {
}
`;
+exports[`/store/carts POST /store/carts/:id fails to complete swap cart with items inventory not/partially covered 1`] = `
+Object {
+ "code": "insufficient_inventory",
+ "message": "Variant with id: test-variant-2 does not have the required inventory",
+ "type": "not_allowed",
+}
+`;
+
exports[`/store/carts POST /store/carts/:id returns early, if cart is already completed 1`] = `
Object {
"code": "cart_incompatible_state",
diff --git a/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap b/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap
new file mode 100644
index 0000000000..073655bb2a
--- /dev/null
+++ b/integration-tests/api/__tests__/store/__snapshots__/product-variants.js.snap
@@ -0,0 +1,89 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`/store/variants /test-variant 1`] = `
+Object {
+ "variant": Object {
+ "allow_backorder": false,
+ "barcode": "test-barcode",
+ "created_at": Any,
+ "deleted_at": null,
+ "ean": "test-ean",
+ "height": null,
+ "hs_code": null,
+ "id": "test-variant",
+ "inventory_quantity": 10,
+ "length": null,
+ "manage_inventory": true,
+ "material": null,
+ "metadata": null,
+ "mid_code": null,
+ "origin_country": null,
+ "prices": Array [
+ Object {
+ "amount": 100,
+ "created_at": Any,
+ "currency_code": "usd",
+ "deleted_at": null,
+ "id": "test-price",
+ "region_id": null,
+ "sale_amount": null,
+ "updated_at": Any,
+ "variant_id": "test-variant",
+ },
+ ],
+ "product": Any