Merge pull request #444 from medusajs/feat/rma-shipping-options

feat: add rma shipping options
This commit is contained in:
Sebastian Rindom
2021-10-15 20:01:41 +02:00
committed by GitHub
29 changed files with 1846 additions and 1150 deletions
+5 -3
View File
@@ -6,7 +6,7 @@ title: "Deploying on Heroku"
This is a guide for deploying a Medusa project on Heroku. Heroku is at PaaS that allows you to easily deploy your applications in the cloud.
> We assume, that you are currently running a local instance of Medusa. If not, check out our [Quickstart](https://docs.medusa-commerce.com/quickstart/quick-start) or use `npx create-medusa-app` to set up your application in a matter of minutes. For the latter, see [this guide](https://docs.medusa-commerce.com/how-to/create-medusa-app) for a small walkthrough.
> We assume, that you are currently running a local instance of Medusa. If not, check out our [Quickstart](https://docs.medusa-commerce.com/quickstart/quick-start) or use `npx create-medusa-app` to set up your application in a matter of minutes. For the latter, see [this guide](https://docs.medusa-commerce.com/how-to/create-medusa-app) for a small walkthrough.
### 1. Install the Heroku CLI
@@ -81,14 +81,16 @@ You can find more informations, plans and pricing about Redis To Go [here](https
### 5. Configure environment variables on Heroku
Medusa requires a set of environment variables. From you project repository run the following commands:.
```shell=
heroku config:set NODE_ENV=production
heroku config:set JWT_SECRET=your-super-secret
heroku config:set COOKIE_SECRET=your-super-secret-pt2
heroku config:set NPM_CONFIG_PRODUCTION=false
```
> Make sure to use actual secrets in a production environment.
Additionally, we need to set the buildpack to Node.js
> Additionally, we need to set the buildpack to Node.js
```shell=
heroku buildpacks:set heroku/nodejs
@@ -137,7 +139,7 @@ module.exports = {
database_type: "postgres",
store_cors: STORE_CORS,
admin_cors: ADMIN_CORS,
database_extra:
database_extra:
process.env.NODE_ENV !== "development"
? { ssl: { rejectUnauthorized: false } }
: {},
@@ -1,22 +1,31 @@
# Uploading images to Spaces
In order to work with images in Medusa, you need a file service plugin responsible for hosting. Following this guide will allow you to upload images to DigitalOcean Spaces.
### Before you start
At this point, you should have an instance of our store engine running. If not, we have a [full guide](https://docs.medusa-commerce.com/tutorial/set-up-your-development-environment) for setting up your local environment.
### Set up up DigitalOcean
#### Create a Space
Create an account on DigitalOcean and navigate to Spaces. Create a new Space with the default settings.
#### Generate access keys
Navigate to API in the left sidebar. Generate a new Spaces access key. This should provide you with an access key id and a secret key. Note them both down.
### Installation
First, install the plugin using your preferred package manager:
```
yarn add medusa-file-spaces
```
Then configure your `medusa-config.js` to include the plugin alongside the required options:
```=javascript
{
resolve: `medusa-file-spaces`,
@@ -29,8 +38,11 @@ Then configure your `medusa-config.js` to include the plugin alongside the requi
},
},
```
In the above options, a `spaces_url` is included. This can be found in your Space overview. The `bucket` should point to the name you gave your Space. The `endpoint` identifies the region in which you created the Space. And finally the two keys are the ones created in the previous section.
> Make sure to use an environment variable for the secret key in a live environment.
### Try it out!
Finally, run your Medusa server alongside our admin system to try out your new file service. Upon editing or creating products, you can now upload thumbnails and images, that are stored in DigitalOcean Spaces.
@@ -4,6 +4,7 @@ const {
Order,
LineItem,
ProductVariant,
CustomShippingOption,
} = require("@medusajs/medusa")
const setupServer = require("../../../helpers/setup-server")
@@ -1409,6 +1410,47 @@ describe("/admin/orders", () => {
expect(response.status).toEqual(200)
})
it("creates a swap with custom shipping options", async () => {
const api = useApi()
const response = await api.post(
"/admin/orders/test-order/swaps",
{
return_items: [
{
item_id: "test-item",
quantity: 1,
},
],
additional_items: [{ variant_id: "test-variant-2", quantity: 1 }],
custom_shipping_options: [{ option_id: "test-option", price: 0 }],
},
{
headers: {
authorization: "Bearer test_token",
},
}
)
const swap = response.data.order.swaps[0]
const manager = dbConnection.manager
const customOptions = await manager.find(CustomShippingOption, {
shipping_option_id: "test-option",
})
expect(response.status).toEqual(200)
expect(customOptions).toEqual(
expect.arrayContaining([
expect.objectContaining({
shipping_option_id: "test-option",
price: 0,
cart_id: swap.cart_id,
}),
])
)
})
it("creates a swap and a return", async () => {
const api = useApi()
+81 -3
View File
@@ -1,5 +1,12 @@
const path = require("path")
const { Region, LineItem, GiftCard } = require("@medusajs/medusa")
const {
Region,
LineItem,
GiftCard,
Cart,
CustomShippingOption,
ShippingOption,
} = require("@medusajs/medusa")
const setupServer = require("../../../helpers/setup-server")
const { useApi } = require("../../../helpers/use-api")
@@ -452,14 +459,42 @@ describe("/store/carts", () => {
describe("POST /store/carts/:id/shipping-methods", () => {
beforeEach(async () => {
await cartSeeder(dbConnection)
try {
await cartSeeder(dbConnection)
const manager = dbConnection.manager
const _cart = await manager.create(Cart, {
id: "test-cart-with-cso",
customer_id: "some-customer",
email: "some-customer@email.com",
shipping_address: {
id: "test-shipping-address",
first_name: "lebron",
country_code: "us",
},
region_id: "test-region",
currency_code: "usd",
type: "swap",
})
let cartWithCustomSo = await manager.save(_cart)
await manager.insert(CustomShippingOption, {
id: "another-cso-test",
cart_id: "test-cart-with-cso",
shipping_option_id: "test-option",
price: 5,
})
} catch (err) {
console.log(err)
}
})
afterEach(async () => {
await doAfterEach()
})
it("adds a shipping method to cart", async () => {
it("adds a normal shipping method to cart", async () => {
const api = useApi()
const cartWithShippingMethod = await api.post(
@@ -476,6 +511,49 @@ describe("/store/carts", () => {
expect(cartWithShippingMethod.status).toEqual(200)
})
it("given a cart with custom options and a shipping option already belonging to said cart, then it should add a shipping method based on the given custom shipping option", async () => {
const shippingOptionId = "test-option"
const api = useApi()
const cartWithCustomShippingMethod = await api
.post(
"/store/carts/test-cart-with-cso/shipping-methods",
{
option_id: shippingOptionId,
},
{ withCredentials: true }
)
.catch((err) => err.response)
expect(
cartWithCustomShippingMethod.data.cart.shipping_methods
).toContainEqual(
expect.objectContaining({
shipping_option_id: shippingOptionId,
price: 5,
})
)
expect(cartWithCustomShippingMethod.status).toEqual(200)
})
it("given a cart with custom options and an option id not corresponding to any custom shipping option, then it should throw an invalid error", async () => {
const api = useApi()
try {
await api.post(
"/store/carts/test-cart-with-cso/shipping-methods",
{
option_id: "orphan-so",
},
{ withCredentials: true }
)
} catch (err) {
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual("Wrong shipping option")
}
})
it("adds a giftcard to cart, but ensures discount only applied to discountable items", async () => {
const api = useApi()
@@ -85,6 +85,9 @@ describe("/store/variants", () => {
},
],
product: expect.any(Object),
options: [
{ created_at: expect.any(String), updated_at: expect.any(String) },
],
},
],
})
@@ -139,6 +142,9 @@ describe("/store/variants", () => {
},
],
product: expect.any(Object),
options: [
{ created_at: expect.any(String), updated_at: expect.any(String) },
],
},
})
})
@@ -4,6 +4,8 @@ const { Region, ShippingProfile, ShippingOption } = require("@medusajs/medusa")
const setupServer = require("../../../helpers/setup-server")
const { useApi } = require("../../../helpers/use-api")
const { initDb, useDb } = require("../../../helpers/use-db")
const cartSeeder = require("../../helpers/cart-seeder")
const swapSeeder = require("../../helpers/swap-seeder")
jest.setTimeout(30000)
@@ -128,4 +130,55 @@ describe("/store/shipping-options", () => {
expect(response.data.shipping_options[0].id).toEqual("test-region2")
})
})
describe("GET /store/shipping-options/:cart_id", () => {
beforeEach(async () => {
await cartSeeder(dbConnection)
await swapSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("given a default cart, when user retrieves its shipping options, then should return a list of shipping options", async () => {
const api = useApi()
const response = await api
.get("/store/shipping-options/test-cart-2")
.catch((err) => {
return err.response
})
expect(response.status).toEqual(200)
expect(response.data.shipping_options).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "test-option", amount: 1000 }),
expect.objectContaining({ id: "test-option-2", amount: 500 }),
])
)
})
it("given a cart with custom shipping options, when user retrieves its shipping options, then should return the list of custom shipping options", async () => {
const api = useApi()
const response = await api
.get("/store/shipping-options/test-cart-rma")
.catch((err) => {
return err.response
})
expect(response.status).toEqual(200)
expect(response.data.shipping_options).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "test-option",
amount: 0,
name: "test-option",
}),
])
)
})
})
})
@@ -15,6 +15,9 @@ const {
Cart,
Return,
} = require("@medusajs/medusa")
const {
CustomShippingOption,
} = require("@medusajs/medusa/dist/models/custom-shipping-option")
module.exports = async (connection, data = {}) => {
const manager = connection.manager
@@ -101,6 +104,72 @@ module.exports = async (connection, data = {}) => {
await manager.save(swap)
const cartWithCustomSo = manager.create(Cart, {
id: "test-cart-rma",
customer_id: "test-customer",
email: "test-customer@email.com",
shipping_address_id: "test-shipping-address",
billing_address_id: "test-billing-address",
region_id: "test-region",
type: "swap",
metadata: {
swap_id: "test-swap",
parent_order_id: orderWithSwap.id,
},
})
await manager.save(cartWithCustomSo)
const liRma = manager.create(LineItem, {
id: "test-item-rma",
title: "Line Item RMA",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 1,
variant_id: "test-variant",
cart_id: "test-cart-rma",
})
await manager.save(liRma)
manager.insert(CustomShippingOption, {
id: "cso-test",
cart_id: cartWithCustomSo.id,
price: 0,
shipping_option_id: "test-option",
})
const swapWithRMAMethod = manager.create(Swap, {
id: "test-swap-rma",
order_id: "order-with-swap",
payment_status: "captured",
fulfillment_status: "fulfilled",
cart_id: cartWithCustomSo.id,
payment: {
id: "test-payment-swap",
amount: 10000,
currency_code: "usd",
amount_refunded: 0,
provider_id: "test-pay",
data: {},
},
additional_items: [
{
id: "test-item-swapped",
fulfilled_quantity: 1,
title: "Line Item",
description: "Line Item Desc",
thumbnail: "https://test.js/1234",
unit_price: 9000,
quantity: 1,
variant_id: "test-variant-2",
cart_id: "test-cart",
},
],
})
await manager.save(swapWithRMAMethod)
const cartTemplate = async (cartId) => {
const cart = manager.create(Cart, {
id: cartId,
+3 -3
View File
@@ -8,15 +8,15 @@
"build": "babel src -d dist --extensions \".ts,.js\""
},
"dependencies": {
"@medusajs/medusa": "1.1.41-dev-1634202426468",
"medusa-interfaces": "1.1.23-dev-1634202426468",
"@medusajs/medusa": "1.1.41-dev-1634316075104",
"medusa-interfaces": "1.1.23-dev-1634316075104",
"typeorm": "^0.2.31"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"babel-preset-medusa-package": "1.1.15-dev-1634202426468",
"babel-preset-medusa-package": "1.1.15-dev-1634316075104",
"jest": "^26.6.3"
}
}
+35 -35
View File
@@ -1223,10 +1223,10 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@medusajs/medusa-cli@1.1.18-dev-1634202426468":
version "1.1.18-dev-1634202426468"
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.18-dev-1634202426468.tgz#f5a5d0e0d50810a1bf9720e4239716b852c425df"
integrity sha512-riFgskx0N5zPBgr8E6Pz4n2xpguS89p2+QgQnL8+rlnSXq/xwh9UbExh7+CQ5QT0slR93U0Io7lKjvQcZO63rA==
"@medusajs/medusa-cli@1.1.18-dev-1634316075104":
version "1.1.18-dev-1634316075104"
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.18-dev-1634316075104.tgz#65b891999ae2c3125d6ea8d83e0f28907fed9dfc"
integrity sha512-wfcfCdJn28C69j2r6EBc6YYVd/wwJ605SI9PI7tgE+0PJRo1NxUcV3+5py9YSAj1Yvjj70vy4mPvpjJfdy3P6g==
dependencies:
"@babel/polyfill" "^7.8.7"
"@babel/runtime" "^7.9.6"
@@ -1244,8 +1244,8 @@
is-valid-path "^0.1.1"
joi-objectid "^3.0.1"
meant "^1.0.1"
medusa-core-utils "1.1.22-dev-1634202426468"
medusa-telemetry "0.0.5-dev-1634202426468"
medusa-core-utils "1.1.22-dev-1634316075104"
medusa-telemetry "0.0.5-dev-1634316075104"
netrc-parser "^3.1.6"
open "^8.0.6"
ora "^5.4.1"
@@ -1259,13 +1259,13 @@
winston "^3.3.3"
yargs "^15.3.1"
"@medusajs/medusa@1.1.41-dev-1634202426468":
version "1.1.41-dev-1634202426468"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.41-dev-1634202426468.tgz#cde450f1fdb6d9cad3e8cb70498338b97b2b14ff"
integrity sha512-7AKoJKhP+bl5UH0oEL7FoveBwDVz4TlGR/ALFUraWZHfglHDSRQI6W9qOD+dGmOT/e7HBzBMXuPPhLr2198S0w==
"@medusajs/medusa@1.1.41-dev-1634316075104":
version "1.1.41-dev-1634316075104"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.41-dev-1634316075104.tgz#8b81aa4a1234fdaf486822545cd8a91d079f7e4b"
integrity sha512-Xw5//89QYYmo+jIBntwDJmbUya/JZl8dbMYVBDcLnORlWRGpWuzLdILfIMW0XWkF8aVI829O9goqT+hsQ/cZ5w==
dependencies:
"@hapi/joi" "^16.1.8"
"@medusajs/medusa-cli" "1.1.18-dev-1634202426468"
"@medusajs/medusa-cli" "1.1.18-dev-1634316075104"
"@types/lodash" "^4.14.168"
awilix "^4.2.3"
body-parser "^1.19.0"
@@ -1287,8 +1287,8 @@
joi "^17.3.0"
joi-objectid "^3.0.1"
jsonwebtoken "^8.5.1"
medusa-core-utils "1.1.22-dev-1634202426468"
medusa-test-utils "1.1.25-dev-1634202426468"
medusa-core-utils "1.1.22-dev-1634316075104"
medusa-test-utils "1.1.25-dev-1634316075104"
morgan "^1.9.1"
multer "^1.4.2"
passport "^0.4.0"
@@ -1911,10 +1911,10 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0"
babel-preset-medusa-package@1.1.15-dev-1634202426468:
version "1.1.15-dev-1634202426468"
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.15-dev-1634202426468.tgz#d368063100f23c6b63d871da289c60a0d342fc97"
integrity sha512-xRLA4mMNKzsMmpj8VAdwlS9XD0ndvessYkEe5TVqayTeJ3V2aLplqVq3lYxbX78MYa+4oncObjQX6GQgakCofQ==
babel-preset-medusa-package@1.1.15-dev-1634316075104:
version "1.1.15-dev-1634316075104"
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.15-dev-1634316075104.tgz#1180bb50676ff349fc59b0b9c8bcad97caef85dc"
integrity sha512-BUWSunUpuAbdZyV093TdwEc5DZvrbp3aqQlrouX9HxgqrFeXYFWNcZqZNADS9NHq1LJqNyHuE1Vz+IQnu9lj0g==
dependencies:
"@babel/plugin-proposal-class-properties" "^7.12.1"
"@babel/plugin-proposal-decorators" "^7.12.1"
@@ -5089,25 +5089,25 @@ media-typer@0.3.0:
resolved "http://localhost:4873/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
medusa-core-utils@1.1.22-dev-1634202426468:
version "1.1.22-dev-1634202426468"
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.22-dev-1634202426468.tgz#7e45e38b672466cf06574b8fbc669b96e06d7e4a"
integrity sha512-I7a0HwI5WL39nBjp1Di9hkm9yGqrIRzKogWWzz3npOPLPMbAW4Mik7/E3Ga8fubUfvIdjbivmVQ490PDq8KBVA==
medusa-core-utils@1.1.22-dev-1634316075104:
version "1.1.22-dev-1634316075104"
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.22-dev-1634316075104.tgz#9cffe185fbce1899ffaae0e08e8f0007fca8571e"
integrity sha512-WRkquPYzodg5PSWTxnIDRMcn+N8LiLpq1Yx8DWIsmV/tsULe+Sz1CMJcUFRGF5bLcyN/Za9yIz6GCHmGOB64aw==
dependencies:
joi "^17.3.0"
joi-objectid "^3.0.1"
medusa-interfaces@1.1.23-dev-1634202426468:
version "1.1.23-dev-1634202426468"
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.23-dev-1634202426468.tgz#61774f3f7d3278268607560c889b144cb258f248"
integrity sha512-rRqY8VjgRYXpXYugauv4OyqyigHK8it6/CWwiNXLreN9UfXyut3+dN0zfEJVix+adTXncsA6jZGVNvDJxipPOQ==
medusa-interfaces@1.1.23-dev-1634316075104:
version "1.1.23-dev-1634316075104"
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.23-dev-1634316075104.tgz#7d787951fd4d66b75b3b235cd833b1840506804b"
integrity sha512-+6u2Q2lgALX2RYBjLOTFmCKB2HgarQbRr+u0uG81gyurF2/TxzwGb/hJLZIeS/Elk2HYuIpstswrAet7/9Rupw==
dependencies:
medusa-core-utils "1.1.22-dev-1634202426468"
medusa-core-utils "1.1.22-dev-1634316075104"
medusa-telemetry@0.0.5-dev-1634202426468:
version "0.0.5-dev-1634202426468"
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.5-dev-1634202426468.tgz#3a0741b481ac25880f77ad492ffa577e360b2466"
integrity sha512-MVK+5oQEYUEB8HUy+hDNOot2yPcONKX2af7jXhjvaBSO+GyLKBUT8LYuU6zN+smm2R9OGRQ54lSii7C3J1ZPhA==
medusa-telemetry@0.0.5-dev-1634316075104:
version "0.0.5-dev-1634316075104"
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.5-dev-1634316075104.tgz#c1bd94ab2e2c3d4dc4bc3d664d96a9091a924fd9"
integrity sha512-Lh/izeAaHt/ClugwMi+Gwsa2OAzVuuNW5WhFAiHQhrR0BA4bYee4xLKWqWw+JGfjMfodOCwF+Zl3I5qINXuaFg==
dependencies:
axios "^0.21.1"
axios-retry "^3.1.9"
@@ -5119,13 +5119,13 @@ medusa-telemetry@0.0.5-dev-1634202426468:
remove-trailing-slash "^0.1.1"
uuid "^8.3.2"
medusa-test-utils@1.1.25-dev-1634202426468:
version "1.1.25-dev-1634202426468"
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.25-dev-1634202426468.tgz#9faef68b2bb43e1c3358f6e337cfc11c4ae90a03"
integrity sha512-LFwZ4GJ91iCwob8BDK3J/Klyjp4pO7QfRLAZ4O/7maEa98mckWcVIXVMaYuBGypcwP3ktucAlk46mAGSDw3wuA==
medusa-test-utils@1.1.25-dev-1634316075104:
version "1.1.25-dev-1634316075104"
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.25-dev-1634316075104.tgz#5958d02013a73f8ddcaf4140d9c81e900172f826"
integrity sha512-2P+Mq+kICR4oa2AhT36HhP8VR15tEli6XBT/yWweVBkRzrjoGptqHg5BtDtUt/s+PYy7STlKG/tin/VH6J3zLg==
dependencies:
"@babel/plugin-transform-classes" "^7.9.5"
medusa-core-utils "1.1.22-dev-1634202426468"
medusa-core-utils "1.1.22-dev-1634316075104"
randomatic "^3.1.1"
merge-descriptors@1.0.1:
+1 -1
View File
@@ -88,4 +88,4 @@
"winston": "^3.2.1"
},
"gitHead": "41a5425405aea5045a26def95c0dc00cf4a5a44d"
}
}
@@ -45,6 +45,17 @@ import { defaultFields, defaultRelations } from "./"
* quantity:
* description: The quantity of the Product Variant to ship.
* type: integer
* custom_shipping_options:
* description: The custom shipping options to potentially create a Shipping Method from.
* type: array
* items:
* properties:
* option_id:
* description: The id of the Shipping Option to override with a custom price.
* type: string
* price:
* description: The custom price of the Shipping Option.
* type: integer
* no_notification:
* description: If set to true no notification will be send related to this Swap.
* type: boolean
@@ -85,6 +96,12 @@ export default async (req, res) => {
variant_id: Validator.string().required(),
quantity: Validator.number().required(),
}),
custom_shipping_options: Validator.array()
.items({
option_id: Validator.string().required(),
price: Validator.number().required(),
})
.default([]),
no_notification: Validator.boolean().optional(),
allow_backorder: Validator.boolean().default(true),
})
@@ -149,7 +166,9 @@ export default async (req, res) => {
}
)
await swapService.withTransaction(manager).createCart(swap.id)
await swapService
.withTransaction(manager)
.createCart(swap.id, value.custom_shipping_options)
const returnOrder = await returnService
.withTransaction(manager)
.retrieveBySwap(swap.id)
@@ -23,7 +23,7 @@ describe("POST /store/carts/:id/shipping-methods", () => {
jest.clearAllMocks()
})
it("calls CartService addShipping", () => {
it("calls CartService addShippingMethod", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),
@@ -45,6 +45,50 @@ describe("POST /store/carts/:id/shipping-methods", () => {
})
})
describe("successfully adds a shipping method", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("swap-cart")
subject = await request(
"POST",
`/store/carts/${cartId}/shipping-methods`,
{
payload: {
option_id: IdMap.getId("freeShipping"),
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService addShippingMethod", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("swap-cart"),
IdMap.getId("freeShipping"),
{}
)
})
it("calls CartService retrieve", () => {
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(2)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body.cart).toEqual(
expect.objectContaining({ type: "swap", id: IdMap.getId("test-swap") })
)
})
})
describe("successfully adds a shipping method with additional data", () => {
let subject
@@ -68,7 +112,7 @@ describe("POST /store/carts/:id/shipping-methods", () => {
jest.clearAllMocks()
})
it("calls CartService addShipping", () => {
it("calls CartService addShippingMethod", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),
@@ -44,7 +44,9 @@ export default async (req, res) => {
await manager.transaction(async m => {
const txCartService = cartService.withTransaction(m)
await txCartService.addShippingMethod(id, value.option_id, value.data)
const updated = await txCartService.retrieve(id, {
relations: ["payment_sessions"],
})
@@ -54,12 +56,12 @@ export default async (req, res) => {
}
})
const cart = await cartService.retrieve(id, {
const updatedCart = await cartService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ cart })
res.status(200).json({ cart: updatedCart })
} catch (err) {
throw err
}
+1
View File
@@ -46,3 +46,4 @@ export { User } from "./models/user"
export { DraftOrder } from "./models/draft-order"
export { ReturnReason } from "./models/return-reason"
export { Note } from "./models/note"
export { CustomShippingOption } from "./models/custom-shipping-option"
@@ -0,0 +1,34 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class addCustomShippingOptions1633614437919 implements MigrationInterface {
name = 'addCustomShippingOptions1633614437919'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "custom_shipping_option" ("id" character varying NOT NULL, "price" integer NOT NULL, "shipping_option_id" character varying NOT NULL, "cart_id" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "UQ_0f838b122a9a01d921aa1cdb669" UNIQUE ("shipping_option_id", "cart_id"), CONSTRAINT "PK_8dfcb5c1172c29eec4a728420cc" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_44090cb11b06174cbcc667e91c" ON "custom_shipping_option" ("shipping_option_id") `);
await queryRunner.query(`CREATE INDEX "IDX_93caeb1bb70d37c1d36d6701a7" ON "custom_shipping_option" ("cart_id") `);
await queryRunner.query(`ALTER TYPE "cart_type_enum" RENAME TO "cart_type_enum_old"`);
await queryRunner.query(`CREATE TYPE "cart_type_enum" AS ENUM('default', 'swap', 'draft_order', 'payment_link', 'claim')`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" TYPE "cart_type_enum" USING "type"::"text"::"cart_type_enum"`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" SET DEFAULT 'default'`);
await queryRunner.query(`DROP TYPE "cart_type_enum_old"`);
await queryRunner.query(`ALTER TABLE "custom_shipping_option" ADD CONSTRAINT "FK_44090cb11b06174cbcc667e91ca" FOREIGN KEY ("shipping_option_id") REFERENCES "shipping_option"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "custom_shipping_option" ADD CONSTRAINT "FK_93caeb1bb70d37c1d36d6701a7a" FOREIGN KEY ("cart_id") REFERENCES "cart"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "custom_shipping_option" DROP CONSTRAINT "FK_93caeb1bb70d37c1d36d6701a7a"`);
await queryRunner.query(`ALTER TABLE "custom_shipping_option" DROP CONSTRAINT "FK_44090cb11b06174cbcc667e91ca"`);
await queryRunner.query(`CREATE TYPE "cart_type_enum_old" AS ENUM('default', 'swap', 'draft_order', 'payment_link')`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" TYPE "cart_type_enum_old" USING "type"::"text"::"cart_type_enum_old"`);
await queryRunner.query(`ALTER TABLE "cart" ALTER COLUMN "type" SET DEFAULT 'default'`);
await queryRunner.query(`DROP TYPE "cart_type_enum"`);
await queryRunner.query(`ALTER TYPE "cart_type_enum_old" RENAME TO "cart_type_enum"`);
await queryRunner.query(`DROP INDEX "IDX_93caeb1bb70d37c1d36d6701a7"`);
await queryRunner.query(`DROP INDEX "IDX_44090cb11b06174cbcc667e91c"`);
await queryRunner.query(`DROP TABLE "custom_shipping_option"`);
}
}
+2
View File
@@ -113,12 +113,14 @@ import { PaymentSession } from "./payment-session"
import { Payment } from "./payment"
import { GiftCard } from "./gift-card"
import { ShippingMethod } from "./shipping-method"
import { CustomShippingOption } from "./custom-shipping-option"
export enum CartType {
DEFAULT = "default",
SWAP = "swap",
DRAFT_ORDER = "draft_order",
PAYMENT_LINK = "payment_link",
CLAIM = "claim",
}
@Entity()
@@ -0,0 +1,99 @@
import {
BeforeInsert, Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryColumn,
Unique,
UpdateDateColumn
} from "typeorm";
import { ulid } from "ulid";
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column";
import { Cart } from './cart';
import { ShippingOption } from "./shipping-option";
@Entity()
@Unique(['shipping_option_id', 'cart_id'])
export class CustomShippingOption {
@PrimaryColumn()
id: string
@Column({ type: "int" })
price: number
@Index()
@Column()
shipping_option_id: string;
@ManyToOne(() => ShippingOption)
@JoinColumn({ name: "shipping_option_id" })
shipping_option: ShippingOption
@Index()
@Column({ nullable: true })
cart_id: string
@ManyToOne(() => Cart)
@JoinColumn({ name: "cart_id" })
cart: Cart
@CreateDateColumn({ type: resolveDbType("timestamptz") })
created_at: Date
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
updated_at: Date
@DeleteDateColumn({ type: resolveDbType("timestamptz") })
deleted_at: Date
@DbAwareColumn({ type: "jsonb", nullable: true })
metadata: any
@BeforeInsert()
private beforeInsert() {
if (this.id) return
const id = ulid()
this.id = `cso_${id}`
}
}
/**
* @schema Custom shipping_option
* title: "Custom Shipping Option"
* description: "Custom Shipping Options are 'overriden' Shipping Options. Store managers can attach a Custom Shipping Option to a cart in order to set a custom price for a particular Shipping Option"
* x-resourceId: custom_shipping_option
* properties:
* id:
* description: "The id of the Custom Shipping Option. This value will be prefixed with `cso_`."
* type: string
* price:
* description: "The custom price set that will override the shipping option's original price"
* type: integer
* shipping_option_id:
* description: "The id of the Shipping Option that the custom shipping option overrides"
* anyOf:
* - $ref: "#/components/schemas/shipping_option"
* cart_id:
* description: "The id of the Cart that the custom shipping option is attached to"
* anyOf:
* - $ref: "#/components/schemas/cart"
* created_at:
* description: "The date with timezone at which the resource was created."
* type: string
* format: date-time
* updated_at:
* description: "The date with timezone at which the resource was last updated."
* type: string
* format: date-time
* deleted_at:
* description: "The date with timezone at which the resource was deleted."
* type: string
* format: date-time
* metadata:
* description: "An optional key-value map with additional information."
* type: object
*/
@@ -0,0 +1,5 @@
import { EntityRepository, Repository } from "typeorm"
import { CustomShippingOption } from './../models/custom-shipping-option';
@EntityRepository(CustomShippingOption)
export class CustomShippingOptionRepository extends Repository<CustomShippingOption> {}
+140 -46
View File
@@ -6,29 +6,29 @@ import { MedusaError } from "medusa-core-utils"
const eventBusService = {
emit: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
describe("CartService", () => {
const totalsService = {
getTotal: o => {
getTotal: (o) => {
return o.total || 0
},
getSubtotal: o => {
getSubtotal: (o) => {
return o.subtotal || 0
},
getTaxTotal: o => {
getTaxTotal: (o) => {
return o.tax_total || 0
},
getDiscountTotal: o => {
getDiscountTotal: (o) => {
return o.discount_total || 0
},
getShippingTotal: o => {
getShippingTotal: (o) => {
return o.shipping_total || 0
},
getGiftCardTotal: o => {
getGiftCardTotal: (o) => {
return o.gift_card_total || 0
},
}
@@ -117,7 +117,7 @@ describe("CartService", () => {
describe("deleteMetadata", () => {
const cartRepository = MockRepository({
findOne: id => {
findOne: (id) => {
if (id === "empty") {
return Promise.resolve({
metadata: {},
@@ -200,7 +200,7 @@ describe("CartService", () => {
},
}
const addressRepository = MockRepository({ create: c => c })
const addressRepository = MockRepository({ create: (c) => c })
const cartRepository = MockRepository()
const customerService = {
retrieveByEmail: jest.fn().mockReturnValue(
@@ -209,7 +209,7 @@ describe("CartService", () => {
email: "email@test.com",
})
),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -317,14 +317,14 @@ describe("CartService", () => {
const lineItemService = {
update: jest.fn(),
create: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const shippingOptionService = {
deleteShippingMethod: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -505,7 +505,7 @@ describe("CartService", () => {
const lineItemService = {
delete: jest.fn(),
update: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -547,7 +547,7 @@ describe("CartService", () => {
const shippingOptionService = {
deleteShippingMethod: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -666,7 +666,7 @@ describe("CartService", () => {
describe("updateLineItem", () => {
const lineItemService = {
update: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -674,7 +674,7 @@ describe("CartService", () => {
...InventoryServiceMock,
confirmInventory: jest
.fn()
.mockImplementation(id => id !== IdMap.getId("cannot-cover")),
.mockImplementation((id) => id !== IdMap.getId("cannot-cover")),
}
const cartRepository = MockRepository({
@@ -747,7 +747,7 @@ describe("CartService", () => {
describe("updateEmail", () => {
const customerService = {
retrieveByEmail: jest.fn().mockImplementation(email => {
retrieveByEmail: jest.fn().mockImplementation((email) => {
if (email === "no@mail.com") {
return Promise.reject()
}
@@ -756,13 +756,13 @@ describe("CartService", () => {
email,
})
}),
create: jest.fn().mockImplementation(data =>
create: jest.fn().mockImplementation((data) =>
Promise.resolve({
id: IdMap.getId("newCus"),
email: data.email,
})
),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -847,7 +847,7 @@ describe("CartService", () => {
}),
})
const addressRepository = MockRepository({ create: c => c })
const addressRepository = MockRepository({ create: (c) => c })
const cartService = new CartService({
manager: MockManager,
@@ -908,7 +908,7 @@ describe("CartService", () => {
region: { countries: [{ iso_2: "us" }] },
}),
})
const addressRepository = MockRepository({ create: c => c })
const addressRepository = MockRepository({ create: (c) => c })
const cartService = new CartService({
manager: MockManager,
@@ -982,15 +982,15 @@ describe("CartService", () => {
describe("setRegion", () => {
const lineItemService = {
update: jest.fn(r => r),
update: jest.fn((r) => r),
delete: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const addressRepository = MockRepository({ create: c => c })
const addressRepository = MockRepository({ create: (c) => c })
const productVariantService = {
getRegionPrice: jest.fn().mockImplementation(id => {
getRegionPrice: jest.fn().mockImplementation((id) => {
if (id === IdMap.getId("fail")) {
return Promise.reject()
}
@@ -1034,7 +1034,7 @@ describe("CartService", () => {
deleteSession: jest.fn(),
updateSession: jest.fn(),
createSession: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -1238,7 +1238,7 @@ describe("CartService", () => {
deleteSession: jest.fn(),
updateSession: jest.fn(),
createSession: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
@@ -1309,11 +1309,50 @@ describe("CartService", () => {
})
})
describe("findCustomShippingOption", () => {
beforeEach(() => {
jest.clearAllMocks()
})
let cartService = new CartService({})
it("given a cart with custom shipping options and a shipping option id corresponding to a custom shipping option, then it should return a custom shipping option", async () => {
const cartCSO = [
{ id: "cso-test", shipping_option_id: "test-so", price: 20 },
]
const result = cartService.findCustomShippingOption(cartCSO, "test-so")
expect(result).toEqual({
id: "cso-test",
shipping_option_id: "test-so",
price: 20,
})
})
it("given a cart with empty custom shipping options and shipping option id, then it should return undefined", async () => {
const cartCSO = []
const result = cartService.findCustomShippingOption(cartCSO, "test-so")
expect(result).toBeUndefined()
})
it("given a cart with custom shipping options and a shipping option id that does not belong to the cart, then it should throw an invalid error", async () => {
const cartCSO = [
{ id: "cso-test", shipping_option_id: "test-so", price: 500 },
]
expect(() => {
cartService.findCustomShippingOption(cartCSO, "some-other-so")
}).toThrow(MedusaError)
})
})
describe("addShippingMethod", () => {
const buildCart = (id, config = {}) => {
return {
id: IdMap.getId(id),
items: (config.items || []).map(i => ({
items: (config.items || []).map((i) => ({
id: IdMap.getId(i.id),
variant: {
product: {
@@ -1321,7 +1360,7 @@ describe("CartService", () => {
},
},
})),
shipping_methods: (config.shipping_methods || []).map(m => ({
shipping_methods: (config.shipping_methods || []).map((m) => ({
id: IdMap.getId(m.id),
shipping_option: {
profile_id: IdMap.getId(m.profile),
@@ -1338,6 +1377,7 @@ describe("CartService", () => {
const cart3 = buildCart("lines", {
items: [{ id: "line", profile: "profile1" }],
})
const cartWithCustomSO = buildCart("cart-with-custom-so")
const cartRepository = MockRepository({
findOneWithRelations: (rels, q) => {
@@ -1346,6 +1386,8 @@ describe("CartService", () => {
return Promise.resolve(cart3)
case IdMap.getId("existing"):
return Promise.resolve(cart2)
case IdMap.getId("cart-with-custom-so"):
return Promise.resolve(cartWithCustomSO)
default:
return Promise.resolve(cart1)
}
@@ -1354,12 +1396,12 @@ describe("CartService", () => {
const lineItemService = {
update: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const shippingOptionService = {
createShippingMethod: jest.fn().mockImplementation(id => {
createShippingMethod: jest.fn().mockImplementation((id) => {
return Promise.resolve({
shipping_option: {
profile_id: id,
@@ -1367,11 +1409,25 @@ describe("CartService", () => {
})
}),
deleteShippingMethod: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const customShippingOptionService = {
list: jest.fn().mockImplementation(({ cart_id }) => {
if (cart_id === IdMap.getId("cart-with-custom-so")) {
return [
{
id: "cso-test",
shipping_profile_id: "test-so",
cart_id: IdMap.getId("cart-with-custom-so"),
},
]
}
}),
}
const cartService = new CartService({
manager: MockManager,
totalsService,
@@ -1379,6 +1435,7 @@ describe("CartService", () => {
shippingOptionService,
lineItemService,
eventBusService,
customShippingOptionService,
})
beforeEach(() => {
@@ -1396,9 +1453,11 @@ describe("CartService", () => {
IdMap.getId("option"),
data
)
expect(
shippingOptionService.createShippingMethod
).toHaveBeenCalledWith(IdMap.getId("option"), data, { cart: cart1 })
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
IdMap.getId("option"),
data,
{ cart: cart1 }
)
})
it("successfully overrides existing profile shipping method", async () => {
@@ -1410,9 +1469,11 @@ describe("CartService", () => {
IdMap.getId("profile1"),
data
)
expect(
shippingOptionService.createShippingMethod
).toHaveBeenCalledWith(IdMap.getId("profile1"), data, { cart: cart2 })
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
IdMap.getId("profile1"),
data,
{ cart: cart2 }
)
expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledWith({
id: IdMap.getId("ship1"),
shipping_option: {
@@ -1438,9 +1499,11 @@ describe("CartService", () => {
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes(
1
)
expect(
shippingOptionService.createShippingMethod
).toHaveBeenCalledWith(IdMap.getId("additional"), data, { cart: cart2 })
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
IdMap.getId("additional"),
data,
{ cart: cart2 }
)
})
it("updates item shipping", async () => {
@@ -1460,19 +1523,50 @@ describe("CartService", () => {
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes(
1
)
expect(
shippingOptionService.createShippingMethod
).toHaveBeenCalledWith(IdMap.getId("profile1"), data, { cart: cart3 })
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
IdMap.getId("profile1"),
data,
{ cart: cart3 }
)
expect(lineItemService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledWith(IdMap.getId("line"), {
has_shipping: true,
})
})
it("successfully adds a shipping method from a custom shipping option and custom price", async () => {
const data = {
id: "test",
extra: "yes",
}
cartService.findCustomShippingOption = jest
.fn()
.mockImplementation((cartCustomShippingOptions) => {
return {
price: 0,
}
})
await cartService.addShippingMethod(
IdMap.getId("cart-with-custom-so"),
IdMap.getId("test-so"),
data
)
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
IdMap.getId("test-so"),
data,
{
cart_id: IdMap.getId("cart-with-custom-so"),
price: 0,
}
)
})
})
describe("applyDiscount", () => {
const getOffsetDate = offset => {
const getOffsetDate = (offset) => {
const date = new Date()
date.setDate(date.getDate() + offset)
return date
@@ -1509,7 +1603,7 @@ describe("CartService", () => {
})
const discountService = {
retrieveByCode: jest.fn().mockImplementation(code => {
retrieveByCode: jest.fn().mockImplementation((code) => {
if (code === "US10") {
return Promise.resolve({
regions: [{ id: IdMap.getId("bad") }],
@@ -0,0 +1,130 @@
import CustomShippingOptionService from "../custom-shipping-option"
import { MockManager, MockRepository, IdMap } from "medusa-test-utils"
describe("CustomShippingOptionService", () => {
describe("list", () => {
const customShippingOptionRepository = MockRepository({
find: q => {
return Promise.resolve([
{
id: "cso-test",
shipping_option_id: "test-so",
price: 0,
cart_id: "test-cso-cart",
},
])
},
})
const customShippingOptionService = new CustomShippingOptionService({
manager: MockManager,
customShippingOptionRepository,
})
beforeAll(async () => {
jest.clearAllMocks()
})
it("calls customShippingOptionRepository find method", async () => {
await customShippingOptionService.list(
{ cart_id: "test-cso-cart" },
{
relations: ["shipping_option"],
}
)
expect(customShippingOptionRepository.find).toHaveBeenCalledTimes(1)
expect(customShippingOptionRepository.find).toHaveBeenCalledWith({
where: {
cart_id: "test-cso-cart",
},
relations: ["shipping_option"],
})
})
})
describe("retrieve", () => {
const customShippingOptionRepository = MockRepository({
findOne: q => {
if (q.where.id === "cso-test") {
return Promise.resolve({
id: "cso-test",
shipping_option_id: "test-so",
price: 0,
cart_id: "test-cso-cart",
})
}
},
})
const customShippingOptionService = new CustomShippingOptionService({
manager: MockManager,
customShippingOptionRepository,
})
beforeAll(async () => {
jest.clearAllMocks()
})
it("calls customShippingOptionRepository findOne method", async () => {
await customShippingOptionService.retrieve("cso-test", {
relations: ["shipping_option", "cart"],
})
expect(customShippingOptionRepository.findOne).toHaveBeenCalledTimes(1)
expect(customShippingOptionRepository.findOne).toHaveBeenCalledWith({
where: { id: "cso-test" },
relations: ["shipping_option", "cart"],
})
})
it("fails when custom shipping option is not found", async () => {
expect(customShippingOptionService.retrieve("bad-cso")).rejects.toThrow(
`Custom shipping option with id: bad-cso was not found.`
)
})
})
describe("create", () => {
const customShippingOptionRepository = MockRepository({
create: jest
.fn()
.mockImplementation(f => Promise.resolve({ id: "test-cso", ...f })),
save: jest.fn().mockImplementation(f => Promise.resolve(f)),
})
const customShippingOptionService = new CustomShippingOptionService({
manager: MockManager,
customShippingOptionRepository,
})
beforeAll(async () => {
jest.clearAllMocks()
})
it("calls customShippingOptionRepository create method", async () => {
const customShippingOption = {
cart_id: "test-cso-cart",
shipping_option_id: "test-so",
price: 30,
}
await customShippingOptionService.create(customShippingOption)
expect(customShippingOptionRepository.create).toHaveBeenCalledTimes(1)
expect(customShippingOptionRepository.create).toHaveBeenCalledWith({
cart_id: "test-cso-cart",
shipping_option_id: "test-so",
price: 30,
metadata: {},
})
expect(customShippingOptionRepository.save).toHaveBeenCalledTimes(1)
expect(customShippingOptionRepository.save).toHaveBeenCalledWith({
id: "test-cso",
cart_id: "test-cso-cart",
shipping_option_id: "test-so",
price: 30,
metadata: {},
})
})
})
})
@@ -176,8 +176,17 @@ describe("ShippingProfileService", () => {
})
const shippingOptionService = {
list: jest.fn().mockImplementation(() =>
Promise.resolve([
list: jest.fn().mockImplementation(({ id }) => {
if (id && id.includes("test-option")) {
return Promise.resolve([
{
id: "test-option",
amount: 1000,
name: "Test option",
},
])
}
return Promise.resolve([
{
id: "ship_1",
},
@@ -185,24 +194,74 @@ describe("ShippingProfileService", () => {
id: "ship_2",
},
])
),
}),
validateCartOption: jest.fn().mockImplementation(s => s),
withTransaction: function() {
return this
},
}
const customShippingOptionService = {
list: jest.fn().mockImplementation(({ cart_id }, config) => {
if (cart_id === "cso-cart") {
return Promise.resolve([
{
id: "cso_1",
cart_id: "cso-cart",
shipping_option_id: "test-option",
price: 0,
},
])
}
return Promise.resolve([])
}),
}
const profileService = new ShippingProfileService({
manager: MockManager,
shippingProfileRepository: profRepo,
shippingOptionService,
customShippingOptionService,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("fetches correct options", async () => {
it("given a cart with custom shipping options, should return correct custom shipping options ", async () => {
const cart = {
id: "cso-cart",
items: [
{
variant: {
product: {
_id: IdMap.getId("product_1"),
profile_id: IdMap.getId("profile"),
},
},
},
{
variant: {
product: {
_id: IdMap.getId("product_2"),
profile_id: IdMap.getId("profile"),
},
},
},
],
type: "swap",
}
await expect(profileService.fetchCartOptions(cart)).resolves.toEqual([
expect.objectContaining({
id: "test-option",
amount: 0,
name: "Test option",
}),
])
})
it("given a cart with no custom shipping options, should return normal shipping options", async () => {
const cart = {
items: [
{
+12 -1
View File
@@ -170,6 +170,14 @@ describe("SwapService", () => {
findOneWithRelations: () => Promise.resolve(existing),
})
const customShippingOptionService = {
create: jest.fn().mockReturnValue(Promise.resolve({ id: "cso-test" })),
update: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
return this
},
}
const lineItemService = {
create: jest.fn().mockImplementation((d) => Promise.resolve(d)),
update: jest.fn().mockImplementation((d) => Promise.resolve(d)),
@@ -185,10 +193,13 @@ describe("SwapService", () => {
swapRepository: swapRepo,
cartService,
lineItemService,
customShippingOptionService,
})
it("finds swap and calls return create cart", async () => {
await swapService.createCart(IdMap.getId("swap-1"))
await swapService.createCart(IdMap.getId("swap-1"), [
{ option_id: "test-option", price: 10 },
])
expect(swapRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
expect(swapRepo.findOneWithRelations).toHaveBeenCalledWith(
+96 -54
View File
@@ -24,7 +24,6 @@ class CartService extends BaseService {
regionService,
lineItemService,
shippingOptionService,
shippingProfileService,
customerService,
discountService,
giftCardService,
@@ -32,6 +31,7 @@ class CartService extends BaseService {
addressRepository,
paymentSessionRepository,
inventoryService,
customShippingOptionService,
}) {
super()
@@ -62,9 +62,6 @@ class CartService extends BaseService {
/** @private @const {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
/** @private @const {ShippingProfileService} */
this.shippingProfileService_ = shippingProfileService
/** @private @const {CustomerService} */
this.customerService_ = customerService
@@ -88,6 +85,9 @@ class CartService extends BaseService {
/** @private @const {InventoryService} */
this.inventoryService_ = inventoryService
/** @private @const {CustomShippingOptionService} */
this.customShippingOptionService_ = customShippingOptionService
}
withTransaction(transactionManager) {
@@ -107,13 +107,13 @@ class CartService extends BaseService {
regionService: this.regionService_,
lineItemService: this.lineItemService_,
shippingOptionService: this.shippingOptionService_,
shippingProfileService: this.shippingProfileService_,
customerService: this.customerService_,
discountService: this.discountService_,
totalsService: this.totalsService_,
addressRepository: this.addressRepository_,
giftCardService: this.giftCardService_,
inventoryService: this.inventoryService_,
customShippingOptionService: this.customShippingOptionService_,
})
cloned.transactionManager_ = transactionManager
@@ -160,7 +160,7 @@ class CartService extends BaseService {
"total",
]
const totalsToSelect = select.filter(v => totalFields.includes(v))
const totalsToSelect = select.filter((v) => totalFields.includes(v))
if (totalsToSelect.length > 0) {
const relationSet = new Set(relations)
relationSet.add("items")
@@ -168,14 +168,14 @@ class CartService extends BaseService {
relationSet.add("discounts")
relationSet.add("discounts.rule")
relationSet.add("discounts.rule.valid_for")
//relationSet.add("discounts.parent_discount")
//relationSet.add("discounts.parent_discount.rule")
//relationSet.add("discounts.parent_discount.regions")
// relationSet.add("discounts.parent_discount")
// relationSet.add("discounts.parent_discount.rule")
// relationSet.add("discounts.parent_discount.regions")
relationSet.add("shipping_methods")
relationSet.add("region")
relations = [...relationSet]
select = select.filter(v => !totalFields.includes(v))
select = select.filter((v) => !totalFields.includes(v))
}
return {
@@ -238,9 +238,8 @@ class CartService extends BaseService {
const cartRepo = this.manager_.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId)
const { select, relations, totalsToSelect } = this.transformQueryForTotals_(
options
)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals_(options)
const query = {
where: { id: validatedId },
@@ -275,7 +274,7 @@ class CartService extends BaseService {
* @return {Promise} the result of the create operation
*/
async create(data) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
const addressRepo = manager.getCustomRepository(this.addressRepository_)
const { region_id } = data
@@ -344,7 +343,7 @@ class CartService extends BaseService {
* @retur {Promise} the result of the update operation
*/
async removeLineItem(cartId, lineItemId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
relations: [
"items",
@@ -354,7 +353,7 @@ class CartService extends BaseService {
],
})
const lineItem = cart.items.find(li => li.id === lineItemId)
const lineItem = cart.items.find((li) => li.id === lineItemId)
if (!lineItem) {
return cart
}
@@ -423,7 +422,7 @@ class CartService extends BaseService {
* @return {Promise} the result of the update operation
*/
async addLineItem(cartId, lineItem) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
relations: [
"shipping_methods",
@@ -436,7 +435,7 @@ class CartService extends BaseService {
let currentItem
if (lineItem.should_merge) {
currentItem = cart.items.find(line => {
currentItem = cart.items.find((line) => {
if (line.should_merge && line.variant_id === lineItem.variant_id) {
return _.isEqual(line.metadata, lineItem.metadata)
}
@@ -502,13 +501,13 @@ class CartService extends BaseService {
* @return {Promise} the result of the update operation
*/
async updateLineItem(cartId, lineItemId, lineItemUpdate) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
relations: ["items", "payment_sessions"],
})
// Ensure that the line item exists in the cart
const lineItemExists = cart.items.find(i => i.id === lineItemId)
const lineItemExists = cart.items.find((i) => i.id === lineItemId)
if (!lineItemExists) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -555,7 +554,7 @@ class CartService extends BaseService {
// if any free shipping discounts, we ensure to update shipping method amount
if (shouldAdd) {
await Promise.all(
cart.shipping_methods.map(async sm => {
cart.shipping_methods.map(async (sm) => {
const smRepo = this.manager_.getCustomRepository(
this.shippingMethodRepository_
)
@@ -570,7 +569,7 @@ class CartService extends BaseService {
)
} else {
await Promise.all(
cart.shipping_methods.map(async sm => {
cart.shipping_methods.map(async (sm) => {
const smRepo = this.manager_.getCustomRepository(
this.shippingMethodRepository_
)
@@ -586,7 +585,7 @@ class CartService extends BaseService {
}
async update(cartId, update) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
const cart = await this.retrieve(cartId, {
select: [
@@ -733,9 +732,7 @@ class CartService extends BaseService {
* @return {Promise} the resultign customer object
*/
async createOrFetchUserFromEmail_(email) {
const schema = Validator.string()
.email()
.required()
const schema = Validator.string().email().required()
const { value, error } = schema.validate(email.toLowerCase())
if (error) {
throw new MedusaError(
@@ -886,11 +883,12 @@ class CartService extends BaseService {
if (discount.usage_limit) {
discount.usage_count = discount.usage_count || 0
if (discount.usage_limit === discount.usage_count)
if (discount.usage_limit === discount.usage_count) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount has been used maximum allowed times"
)
}
}
const today = new Date()
@@ -942,7 +940,7 @@ class CartService extends BaseService {
const toParse = [...cart.discounts, discount]
let sawNotShipping = false
const newDiscounts = toParse.map(d => {
const newDiscounts = toParse.map((d) => {
const drule = d.rule
switch (drule.type) {
case "free_shipping":
@@ -972,7 +970,7 @@ class CartService extends BaseService {
* @return {Promise<Cart>} the resulting cart
*/
async removeDiscount(cartId, discountCode) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
relations: [
"discounts",
@@ -987,7 +985,7 @@ class CartService extends BaseService {
await this.adjustFreeShipping_(cart, false)
}
cart.discounts = cart.discounts.filter(d => d.code !== discountCode)
cart.discounts = cart.discounts.filter((d) => d.code !== discountCode)
const cartRepo = manager.getCustomRepository(this.cartRepository_)
@@ -1018,7 +1016,7 @@ class CartService extends BaseService {
* Updates the currently selected payment session.
*/
async updatePaymentSession(cartId, update) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"],
})
@@ -1052,7 +1050,7 @@ class CartService extends BaseService {
* @return {Promise<Cart>} the resulting cart
*/
async authorizePayment(cartId, context = {}) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cartRepository = manager.getCustomRepository(this.cartRepository_)
const cart = await this.retrieve(cartId, {
@@ -1096,10 +1094,10 @@ class CartService extends BaseService {
* Sets a payment method for a cart.
* @param {string} cartId - the id of the cart to add payment method to
* @param {PaymentMethod} paymentMethod - the method to be set to the cart
* @returns {Promise} result of update operation
* @return {Promise} result of update operation
*/
async setPaymentSession(cartId, providerId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const psRepo = manager.getCustomRepository(this.paymentSessionRepository_)
const cart = await this.retrieve(cartId, {
@@ -1128,13 +1126,13 @@ class CartService extends BaseService {
}
await Promise.all(
cart.payment_sessions.map(ps => {
cart.payment_sessions.map((ps) => {
return psRepo.save({ ...ps, is_selected: null })
})
)
const sess = cart.payment_sessions.find(
ps => ps.provider_id === providerId
(ps) => ps.provider_id === providerId
)
sess.is_selected = true
@@ -1157,13 +1155,13 @@ class CartService extends BaseService {
* amounts, currencies, etc. as well as make sure to filter payment sessions
* that are not available for the cart's region.
* @param {string} cartId - the id of the cart to set payment session for
* @returns {Promise} the result of the update operation.
* @return {Promise} the result of the update operation.
*/
async setPaymentSessions(cartOrCartId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const psRepo = manager.getCustomRepository(this.paymentSessionRepository_)
let cartId =
const cartId =
typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id
const cart = await this.retrieve(cartId, {
select: [
@@ -1192,7 +1190,7 @@ class CartService extends BaseService {
const region = cart.region
// If there are existing payment sessions ensure that these are up to date
let seen = []
const seen = []
if (cart.payment_sessions && cart.payment_sessions.length) {
for (const session of cart.payment_sessions) {
if (
@@ -1242,10 +1240,10 @@ class CartService extends BaseService {
* @param {string} cartId - the id of the cart to remove from
* @param {string} providerId - the id of the provider whoose payment session
* should be removed.
* @returns {Promise<Cart>} the resulting cart.
* @return {Promise<Cart>} the resulting cart.
*/
async deletePaymentSession(cartId, providerId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"],
})
@@ -1283,10 +1281,10 @@ class CartService extends BaseService {
* @param {string} cartId - the id of the cart to remove from
* @param {string} providerId - the id of the provider whoose payment session
* should be removed.
* @returns {Promise<Cart>} the resulting cart.
* @return {Promise<Cart>} the resulting cart.
*/
async refreshPaymentSession(cartId, providerId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
relations: ["payment_sessions"],
})
@@ -1325,7 +1323,7 @@ class CartService extends BaseService {
* @return {Promise} the result of the update operation
*/
async addShippingMethod(cartId, optionId, data) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
select: ["subtotal"],
relations: [
@@ -1340,11 +1338,32 @@ class CartService extends BaseService {
"items.variant.product",
],
})
const cartCustomShippingOptions =
await this.customShippingOptionService_.list({ cart_id: cart.id })
const customShippingOption = this.findCustomShippingOption(
cartCustomShippingOptions,
optionId
)
const { shipping_methods } = cart
/**
* If we have a custom shipping option configured we want the price
* override to take effect and do not want `validateCartOption` to check
* if requirements are met, hence we are not passing the entire cart, but
* just the id.
*/
const shippingMethodConfig = customShippingOption
? { cart_id: cart.id, price: customShippingOption.price }
: {
cart,
}
const newMethod = await this.shippingOptionService_
.withTransaction(manager)
.createShippingMethod(optionId, data, { cart })
.createShippingMethod(optionId, data, shippingMethodConfig)
const methods = [newMethod]
if (shipping_methods.length) {
@@ -1389,6 +1408,29 @@ class CartService extends BaseService {
}, "SERIALIZABLE")
}
/**
* Finds the cart's custom shipping options based on the passed option id.
* throws if custom options is not empty and no shipping option corresponds to optionId
* @param {Object} cartCustomShippingOptions - the cart's custom shipping options
* @param {string} option - id of the normal or custom shipping option to find in the cartCustomShippingOptions
* @return {CustomShippingOption | undefined}
*/
findCustomShippingOption(cartCustomShippingOptions, optionId) {
const customOption = cartCustomShippingOptions?.find(
(cso) => cso.shipping_option_id === optionId
)
const hasCustomOptions = cartCustomShippingOptions?.length
if (hasCustomOptions && !customOption) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Wrong shipping option"
)
}
return customOption
}
/**
* Set's the region of a cart.
* @param {string} cartId - the id of the cart to set region on
@@ -1416,7 +1458,7 @@ class CartService extends BaseService {
if (cart.items.length) {
cart.items = await Promise.all(
cart.items
.map(async item => {
.map(async (item) => {
const availablePrice = await this.productVariantService_
.getRegionPrice(item.variant_id, regionId)
.catch(() => undefined)
@@ -1495,20 +1537,20 @@ class CartService extends BaseService {
}
if (cart.discounts && cart.discounts.length) {
const newDiscounts = cart.discounts.map(d => {
const newDiscounts = cart.discounts.map((d) => {
if (d.regions.find(({ id }) => id === regionId)) {
return d
}
})
cart.discounts = newDiscounts.filter(d => !!d)
cart.discounts = newDiscounts.filter((d) => !!d)
}
cart.gift_cards = []
if (cart.payment_sessions && cart.payment_sessions.length) {
await Promise.all(
cart.payment_sessions.map(ps =>
cart.payment_sessions.map((ps) =>
this.paymentProviderService_
.withTransaction(this.manager_)
.deleteSession(ps)
@@ -1522,11 +1564,11 @@ class CartService extends BaseService {
/**
* Deletes a cart from the database. Completed carts cannot be deleted.
* @param {string} cartId - the id of the cart to delete
* @returns {Promise<Cart?>} the deleted cart or undefined if the cart was
* @return {Promise<Cart?>} the deleted cart or undefined if the cart was
* not found.
*/
async delete(cartId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cart = await this.retrieve(cartId, {
relations: [
"items",
@@ -1566,7 +1608,7 @@ class CartService extends BaseService {
* @return {Promise} resolves to the updated result.
*/
async setMetadata(cartId, key, value) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId)
@@ -1600,7 +1642,7 @@ class CartService extends BaseService {
* @return {Promise} resolves to the updated result.
*/
async deleteMetadata(cartId, key) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
const validatedId = this.validateId_(cartId)
@@ -0,0 +1,111 @@
import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
class CustomShippingOptionService extends BaseService {
constructor({ manager, customShippingOptionRepository }) {
super()
/** @private @const {EntityManager} */
this.manager_ = manager
/** @private @const {CustomShippingOptionRepository} */
this.customShippingOptionRepository_ = customShippingOptionRepository
}
/**
* Sets the service's manager to a given transaction manager
* @param {EntityManager} manager - the manager to use
* @return {CustomShippingOptionService} a cloned CustomShippingOption service
*/
withTransaction(manager) {
if (!manager) {
return this
}
const cloned = new CustomShippingOptionService({
manager,
customShippingOptionRepository: this.customShippingOptionRepository_,
})
cloned.transactionManager_ = manager
return cloned
}
/**
* Retrieves a specific shipping option.
* @param {string} id - the id of the custom shipping option to retrieve.
* @param {*} config - any options needed to query for the result.
* @return {Promise<CustomShippingOption>} which resolves to the requested custom shipping option.
*/
async retrieve(id, config = {}) {
const customShippingOptionRepo = this.manager_.getCustomRepository(
this.customShippingOptionRepository_
)
const validatedId = this.validateId_(id)
const query = this.buildQuery_({ id: validatedId }, config)
const customShippingOption = await customShippingOptionRepo.findOne(query)
if (!customShippingOption) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Custom shipping option with id: ${id} was not found.`
)
}
return customShippingOption
}
/** Fetches all custom shipping options related to the given selector
* @param {Object} selector - the query object for find
* @param {Object} config - the configuration used to find the objects. contains relations, skip, and take.
* @return {Promise<CustomShippingOption[]>} custom shipping options matching the query
*/
async list(
selector,
config = {
skip: 0,
take: 50,
relations: [],
}
) {
const customShippingOptionRepo = this.manager_.getCustomRepository(
this.customShippingOptionRepository_
)
const query = this.buildQuery_(selector, config)
return customShippingOptionRepo.find(query)
}
/**
* Creates a custom shipping option associated with a given author
* @param {object} data - the custom shipping option to create
* @param {*} config - any configurations if needed, including meta data
* @return {Promise<CustomShippingOption>} resolves to the creation result
*/
async create(data, config = { metadata: {} }) {
const { metadata } = config
const { cart_id, shipping_option_id, price } = data
return this.atomicPhase_(async (manager) => {
const customShippingOptionRepo = manager.getCustomRepository(
this.customShippingOptionRepository_
)
const customShippingOption = await customShippingOptionRepo.create({
cart_id,
shipping_option_id,
price,
metadata,
})
const result = await customShippingOptionRepo.save(customShippingOption)
return result
})
}
}
export default CustomShippingOptionService
+7 -5
View File
@@ -90,7 +90,7 @@ class LineItemService extends BaseService {
}
async generate(variantId, regionId, quantity, config = {}) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const variant = await this.productVariantService_
.withTransaction(manager)
.retrieve(variantId, {
@@ -142,7 +142,7 @@ class LineItemService extends BaseService {
* @return {LineItem} the created line item
*/
async create(lineItem) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const lineItemRepository = manager.getCustomRepository(
this.lineItemRepository_
)
@@ -160,7 +160,7 @@ class LineItemService extends BaseService {
* @return {LineItem} the update line item
*/
async update(id, update) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const lineItemRepository = manager.getCustomRepository(
this.lineItemRepository_
)
@@ -188,14 +188,16 @@ class LineItemService extends BaseService {
* @return {Promise} the result of the delete operation
*/
async delete(id) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const lineItemRepository = manager.getCustomRepository(
this.lineItemRepository_
)
const lineItem = await lineItemRepository.findOne({ where: { id } })
if (!lineItem) { return Promise.resolve() }
if (!lineItem) {
return Promise.resolve()
}
await lineItemRepository.remove(lineItem)
+20 -16
View File
@@ -175,7 +175,7 @@ class ShippingOptionService extends BaseService {
* @return {Promise<ShippingMethod>} the resulting shipping method
*/
async updateShippingMethod(id, update) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const methodRepo = manager.getCustomRepository(this.methodRepository_)
const method = await methodRepo.findOne({ where: { id } })
@@ -204,7 +204,7 @@ class ShippingOptionService extends BaseService {
* @param {string} sm - the shipping method to remove
*/
async deleteShippingMethod(sm) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const methodRepo = manager.getCustomRepository(this.methodRepository_)
return methodRepo.remove(sm)
})
@@ -218,7 +218,7 @@ class ShippingOptionService extends BaseService {
* @return {ShippingMethod} the resulting shipping method.
*/
async createShippingMethod(optionId, data, config) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const option = await this.retrieve(optionId, {
relations: ["requirements"],
})
@@ -256,6 +256,10 @@ class ShippingOptionService extends BaseService {
toCreate.cart_id = config.cart.id
}
if (config.cart_id) {
toCreate.cart_id = config.cart_id
}
if (config.return_id) {
toCreate.return_id = config.return_id
}
@@ -304,7 +308,7 @@ class ShippingOptionService extends BaseService {
}
const subtotal = cart.subtotal
const requirementResults = option.requirements.map(requirement => {
const requirementResults = option.requirements.map((requirement) => {
switch (requirement.type) {
case "max_subtotal":
return requirement.amount > subtotal
@@ -333,7 +337,7 @@ class ShippingOptionService extends BaseService {
* @return {Promise<ShippingOption>} the result of the create operation
*/
async create(data) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const optionRepo = manager.getCustomRepository(this.optionRepository_)
const option = await optionRepo.create(data)
@@ -371,7 +375,7 @@ class ShippingOptionService extends BaseService {
for (const r of data.requirements) {
const validated = await this.validateRequirement_(r)
if (acc.find(raw => raw.type === validated.type)) {
if (acc.find((raw) => raw.type === validated.type)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Only one requirement of each type is allowed"
@@ -380,7 +384,7 @@ class ShippingOptionService extends BaseService {
if (
acc.find(
raw =>
(raw) =>
(raw.type === "max_subtotal" &&
validated.amount > raw.amount) ||
(raw.type === "min_subtotal" && validated.amount < raw.amount)
@@ -450,7 +454,7 @@ class ShippingOptionService extends BaseService {
* @return {Promise} resolves to the update result.
*/
async update(optionId, update) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const option = await this.retrieve(optionId, {
relations: ["requirements"],
})
@@ -478,7 +482,7 @@ class ShippingOptionService extends BaseService {
for (const r of update.requirements) {
const validated = await this.validateRequirement_(r, optionId)
if (acc.find(raw => raw.type === validated.type)) {
if (acc.find((raw) => raw.type === validated.type)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Only one requirement of each type is allowed"
@@ -487,7 +491,7 @@ class ShippingOptionService extends BaseService {
if (
acc.find(
raw =>
(raw) =>
(raw.type === "max_subtotal" &&
validated.amount > raw.amount) ||
(raw.type === "min_subtotal" && validated.amount < raw.amount)
@@ -503,12 +507,12 @@ class ShippingOptionService extends BaseService {
}
if (option.requirements) {
const accReqs = acc.map(a => a.id)
const accReqs = acc.map((a) => a.id)
const toRemove = option.requirements.filter(
r => !accReqs.includes(r.id)
(r) => !accReqs.includes(r.id)
)
await Promise.all(
toRemove.map(async req => {
toRemove.map(async (req) => {
await this.removeRequirement(req.id)
})
)
@@ -580,13 +584,13 @@ class ShippingOptionService extends BaseService {
* @return {Promise} the result of update
*/
async addRequirement(optionId, requirement) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const option = await this.retrieve(optionId, {
relations: ["requirements"],
})
const validatedReq = await this.validateRequirement_(requirement)
if (option.requirements.find(r => r.type === validatedReq.type)) {
if (option.requirements.find((r) => r.type === validatedReq.type)) {
throw new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
`A requirement with type: ${validatedReq.type} already exists`
@@ -606,7 +610,7 @@ class ShippingOptionService extends BaseService {
* @return {Promise} the result of update
*/
async removeRequirement(requirementId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
try {
const reqRepo = manager.getCustomRepository(this.requirementRepository_)
const requirement = await reqRepo.findOne({
@@ -14,6 +14,7 @@ class ShippingProfileService extends BaseService {
productService,
productRepository,
shippingOptionService,
customShippingOptionService,
}) {
super()
@@ -31,6 +32,9 @@ class ShippingProfileService extends BaseService {
/** @private @const {ShippingOptionService} */
this.shippingOptionService_ = shippingOptionService
/** @private @const {CustomShippingOptionService} */
this.customShippingOptionService_ = customShippingOptionService
}
withTransaction(transactionManager) {
@@ -43,6 +47,7 @@ class ShippingProfileService extends BaseService {
shippingProfileRepository: this.shippingProfileRepository_,
productService: this.productService_,
shippingOptionService: this.shippingOptionService_,
customShippingOptionService: this.customShippingOptionService_,
})
cloned.transactionManager_ = transactionManager
@@ -410,19 +415,47 @@ class ShippingProfileService extends BaseService {
* Finds all the shipping profiles that cover the products in a cart, and
* validates all options that are available for the cart.
* @param {Cart} cart - the cart object to find shipping options for
* @return {[ShippingOptions]} a list of the available shipping options
* @return {[ShippingOption]} a list of the available shipping options
*/
async fetchCartOptions(cart) {
const profileIds = this.getProfilesInCart_(cart)
const rawOpts = await this.shippingOptionService_.list(
const selector = {
profile_id: profileIds,
admin_only: false,
}
const customShippingOptions = await this.customShippingOptionService_.list(
{
profile_id: profileIds,
admin_only: false,
cart_id: cart.id,
},
{ relations: ["requirements", "profile"] }
{ select: ["id", "shipping_option_id", "price"] }
)
const hasCustomShippingOptions = customShippingOptions?.length
// if there are custom shipping options associated with the cart, use those
if (hasCustomShippingOptions) {
selector.id = customShippingOptions.map(cso => cso.shipping_option_id)
}
const rawOpts = await this.shippingOptionService_.list(selector, {
relations: ["requirements", "profile"],
})
// if there are custom shipping options associated with the cart, return cart shipping options with custom price
if (hasCustomShippingOptions) {
return rawOpts.map(so => {
const customOption = customShippingOptions.find(
cso => cso.shipping_option_id === so.id
)
return {
...so,
amount: customOption?.price,
}
})
}
const options = []
for (const o of rawOpts) {
+16 -1
View File
@@ -32,6 +32,7 @@ class SwapService extends BaseService {
fulfillmentService,
orderService,
inventoryService,
customShippingOptionService,
}) {
super()
@@ -70,6 +71,9 @@ class SwapService extends BaseService {
/** @private @const {EventBusService} */
this.eventBus_ = eventBusService
/** @private @const {CustomShippingOptionService} */
this.customShippingOptionService_ = customShippingOptionService
}
withTransaction(transactionManager) {
@@ -90,6 +94,7 @@ class SwapService extends BaseService {
orderService: this.orderService_,
inventoryService: this.inventoryService_,
fulfillmentService: this.fulfillmentService_,
customShippingOptionService: this.customShippingOptionService_,
})
cloned.transactionManager_ = transactionManager
@@ -522,7 +527,7 @@ class SwapService extends BaseService {
* @returns {Promise<Swap>} the swap with its cart_id prop set to the id of
* the new cart.
*/
async createCart(swapId) {
async createCart(swapId, customShippingOptions = []) {
return this.atomicPhase_(async (manager) => {
const swap = await this.retrieve(swapId, {
relations: [
@@ -576,6 +581,16 @@ class SwapService extends BaseService {
},
})
for (const customShippingOption of customShippingOptions) {
await this.customShippingOptionService_
.withTransaction(manager)
.create({
cart_id: cart.id,
shipping_option_id: customShippingOption.option_id,
price: customShippingOption.price,
})
}
for (const item of swap.additional_items) {
await this.lineItemService_.withTransaction(manager).update(item.id, {
cart_id: cart.id,
+695 -968
View File
File diff suppressed because it is too large Load Diff