fix: more adjustments

add: composite unique constraint for shipping_option_id and cart_id
fix: fetchCartOptions to format custom shipping options like normal shipping options
fix: addShippingMethod should throw when custom shipping options is not empty and no optionId corresponds to custom shipping options
This commit is contained in:
zakariaelas
2021-10-07 14:54:05 +01:00
parent 52be911e50
commit 3d088c351b
8 changed files with 87 additions and 97 deletions
+13 -15
View File
@@ -5,6 +5,7 @@ const {
GiftCard, GiftCard,
Cart, Cart,
CustomShippingOption, CustomShippingOption,
ShippingOption,
} = require("@medusajs/medusa") } = require("@medusajs/medusa")
const setupServer = require("../../../helpers/setup-server") const setupServer = require("../../../helpers/setup-server")
@@ -466,11 +467,6 @@ describe("/store/carts", () => {
}) })
cartWithCustomSo = await manager.save(_cart) cartWithCustomSo = await manager.save(_cart)
await manager.insert(CustomShippingOption, {
id: "orphan-cso",
price: 0,
})
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }
@@ -497,8 +493,9 @@ describe("/store/carts", () => {
expect(cartWithShippingMethod.status).toEqual(200) expect(cartWithShippingMethod.status).toEqual(200)
}) })
it("given a cart with custom options and a custom option id already belonging to said cart, then it should add a shipping method based on the given custom shipping option", async () => { 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 customOptionId = cartWithCustomSo.custom_shipping_options[0].id const shippingOptionId =
cartWithCustomSo.custom_shipping_options[0].shipping_option_id
const api = useApi() const api = useApi()
@@ -506,7 +503,7 @@ describe("/store/carts", () => {
.post( .post(
"/store/carts/test-cart-with-cso/shipping-methods", "/store/carts/test-cart-with-cso/shipping-methods",
{ {
option_id: customOptionId, option_id: shippingOptionId,
}, },
{ withCredentials: true } { withCredentials: true }
) )
@@ -515,27 +512,28 @@ describe("/store/carts", () => {
expect( expect(
cartWithCustomShippingMethod.data.cart.shipping_methods cartWithCustomShippingMethod.data.cart.shipping_methods
).toContainEqual( ).toContainEqual(
expect.objectContaining({ shipping_option_id: "test-option", price: 5 }) expect.objectContaining({
shipping_option_id: shippingOptionId,
price: 5,
})
) )
expect(cartWithCustomShippingMethod.status).toEqual(200) expect(cartWithCustomShippingMethod.status).toEqual(200)
}) })
it("given a cart with custom options and a custom option id not belonging to said cart, then it should throw a shipping option not found error", async () => { 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() const api = useApi()
try { try {
await api.post( await api.post(
"/store/carts/test-cart-with-cso/shipping-methods", "/store/carts/test-cart-with-cso/shipping-methods",
{ {
option_id: "orphan-cso", option_id: "orphan-so",
}, },
{ withCredentials: true } { withCredentials: true }
) )
} catch (err) { } catch (err) {
expect(err.response.status).toEqual(404) expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual( expect(err.response.data.message).toEqual("Wrong shipping option")
"Shipping Option with orphan-cso was not found"
)
} }
}) })
@@ -160,7 +160,7 @@ describe("/store/shipping-options", () => {
) )
}) })
it("given a swap cart, when user retrieves its shipping options, then should return a list of custom shipping options", async () => { 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 api = useApi()
const response = await api const response = await api
@@ -173,8 +173,9 @@ describe("/store/shipping-options", () => {
expect(response.data.shipping_options).toEqual( expect(response.data.shipping_options).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
shipping_option_id: "test-option", id: "test-option",
price: 0, amount: 0,
name: "test-option",
}), }),
]) ])
) )
@@ -1,10 +1,10 @@
import {MigrationInterface, QueryRunner} from "typeorm"; import {MigrationInterface, QueryRunner} from "typeorm";
export class addCustomShippingOptions1633522106578 implements MigrationInterface { export class addCustomShippingOptions1633614437919 implements MigrationInterface {
name = 'addCustomShippingOptions1633522106578' name = 'addCustomShippingOptions1633614437919'
public async up(queryRunner: QueryRunner): Promise<void> { 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 "PK_8dfcb5c1172c29eec4a728420cc" PRIMARY KEY ("id"))`); 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_44090cb11b06174cbcc667e91c" ON "custom_shipping_option" ("shipping_option_id") `);
await queryRunner.query(`CREATE INDEX "IDX_93caeb1bb70d37c1d36d6701a7" ON "custom_shipping_option" ("cart_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(`ALTER TYPE "cart_type_enum" RENAME TO "cart_type_enum_old"`);
@@ -7,6 +7,7 @@ import {
JoinColumn, JoinColumn,
ManyToOne, ManyToOne,
PrimaryColumn, PrimaryColumn,
Unique,
UpdateDateColumn UpdateDateColumn
} from "typeorm"; } from "typeorm";
import { ulid } from "ulid"; import { ulid } from "ulid";
@@ -16,6 +17,7 @@ import { ShippingOption } from "./shipping-option";
@Entity() @Entity()
@Unique(['shipping_option_id', 'cart_id'])
export class CustomShippingOption { export class CustomShippingOption {
@PrimaryColumn() @PrimaryColumn()
id: string id: string
+20 -37
View File
@@ -1308,65 +1308,50 @@ describe("CartService", () => {
}) })
}) })
describe("extractShippingOptionIdAndPrice", () => { describe("findCustomShippingOption", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
let cartService = new CartService({}) let cartService = new CartService({})
it("given a cart with custom shipping options and a custom shipping option id, then it should return a normal shipping option id corresponding to the custom shipping option id and a customPrice", async () => { 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 cart = { const cart = {
id: "cart-with-so", id: "cart-with-so",
custom_shipping_options: [ custom_shipping_options: [
{ id: "cso-test", shipping_option_id: "test-so", price: 20 }, { id: "cso-test", shipping_option_id: "test-so", price: 20 },
], ],
} }
const result = cartService.extractShippingOptionIdAndPrice( const result = cartService.findCustomShippingOption(cart, "test-so")
cart,
"cso-test"
)
expect(result).toEqual({ expect(result).toEqual({
optionId: "test-so", id: "cso-test",
customPrice: { price: 20 }, shipping_option_id: "test-so",
price: 20,
}) })
}) })
it("given a cart with custom shipping options and a normal shipping option id, then it should return a normal shipping option id and empty customPrice", async () => { it("given a cart with empty custom shipping options and shipping option id, then it should return undefined", async () => {
const cart = { const cart = {
id: "cart-with-so", id: "cart-with-so",
custom_shipping_options: [ custom_shipping_options: [],
{ id: "cso-test", shipping_option_id: "test-so", price: 20 },
],
} }
const result = cartService.extractShippingOptionIdAndPrice( const result = cartService.findCustomShippingOption(cart, "test-so")
cart,
"test-so"
)
expect(result).toEqual({ expect(result).toBeUndefined()
optionId: "test-so",
customPrice: {},
})
}) })
it("given a cart with custom shipping options and a custom shipping option id that does not belong to the cart, then it should return the custom shipping option id and empty customPrice", async () => { 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 cart = { const cart = {
id: "cart-with-so", id: "cart-with-so",
custom_shipping_options: [ custom_shipping_options: [
{ id: "cso-test", shipping_option_id: "test-so", price: 20 }, { id: "cso-test", shipping_option_id: "test-so", price: 500 },
], ],
} }
const result = cartService.extractShippingOptionIdAndPrice(
cart,
"cso-test-2"
)
expect(result).toEqual({ expect(() => {
optionId: "cso-test-2", cartService.findCustomShippingOption(cart, "some-other-so")
customPrice: {}, }).toThrow(MedusaError)
})
}) })
}) })
@@ -1545,27 +1530,25 @@ describe("CartService", () => {
}) })
}) })
it("adds a shipping method from a custom shipping option and custom price", async () => { it("successfully adds a shipping method from a custom shipping option and custom price", async () => {
const data = { const data = {
id: "test", id: "test",
extra: "yes", extra: "yes",
} }
cartService.extractShippingOptionIdAndPrice = jest cartService.findCustomShippingOption = jest
.fn() .fn()
.mockImplementation((cart, optionId) => { .mockImplementation(cart => {
if (cart.id === IdMap.getId("cart-with-custom-so")) { if (cart.id === IdMap.getId("cart-with-custom-so")) {
return { return {
optionId: IdMap.getId("test-so"), price: 0,
customPrice: { price: 0 },
} }
} }
return { optionId, customPrice: {} }
}) })
await cartService.addShippingMethod( await cartService.addShippingMethod(
IdMap.getId("cart-with-custom-so"), IdMap.getId("cart-with-custom-so"),
IdMap.getId("cso-test"), IdMap.getId("test-so"),
data data
) )
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith( expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
@@ -207,14 +207,24 @@ describe("ShippingProfileService", () => {
id: "swap-cart", id: "swap-cart",
type: "swap", type: "swap",
custom_shipping_options: [ custom_shipping_options: [
{ option_id: "test-option1", id: "cso-option1", price: 10 }, {
{ option_id: "test-option2", id: "cso-option2", price: 0 }, shipping_option_id: "test-option1",
id: "cso-option1",
shipping_option: { id: "test-option1" },
price: 10,
},
{
shipping_option_id: "test-option2",
id: "cso-option2",
shipping_option: { id: "test-option2" },
price: 0,
},
], ],
} }
await expect(profileService.fetchCartOptions(cart)).resolves.toEqual([ await expect(profileService.fetchCartOptions(cart)).resolves.toEqual([
expect.objectContaining({ id: "cso-option1" }), expect.objectContaining({ id: "test-option1", amount: 10 }),
expect.objectContaining({ id: "cso-option2" }), expect.objectContaining({ id: "test-option2", amount: 0 }),
]) ])
}) })
+24 -30
View File
@@ -1305,7 +1305,7 @@ class CartService extends BaseService {
* @param {Object} data - the fulmillment data for the method * @param {Object} data - the fulmillment data for the method
* @return {Promise} the result of the update operation * @return {Promise} the result of the update operation
*/ */
async addShippingMethod(cartId, optionIdOrCustomOptionId, data) { async addShippingMethod(cartId, optionId, data) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async manager => {
const cart = await this.retrieve(cartId, { const cart = await this.retrieve(cartId, {
select: ["subtotal"], select: ["subtotal"],
@@ -1321,19 +1321,19 @@ class CartService extends BaseService {
], ],
}) })
let { optionId, customPrice } = this.extractShippingOptionIdAndPrice( let customShippingOption = this.findCustomShippingOption(cart, optionId)
cart,
optionIdOrCustomOptionId
)
const { shipping_methods } = cart const { shipping_methods } = cart
const shippingMethodConfig = customShippingOption
? { cart, price: customShippingOption.price }
: {
cart,
}
const newMethod = await this.shippingOptionService_ const newMethod = await this.shippingOptionService_
.withTransaction(manager) .withTransaction(manager)
.createShippingMethod(optionId, data, { .createShippingMethod(optionId, data, shippingMethodConfig)
cart,
...customPrice,
})
const methods = [newMethod] const methods = [newMethod]
if (shipping_methods.length) { if (shipping_methods.length) {
@@ -1374,32 +1374,26 @@ class CartService extends BaseService {
} }
/** /**
* Adds the corresponding shipping method either from a normal or custom option to the list of shipping methods associated with * Finds the cart's custom shipping option based on the passed option id.
* the cart. * throws if custom options is not empty and no shipping option corresponds to optionId
* @param {Object} cart - the cart object * @param {Object} cart - the cart object
* @param {string} optionIdOrCustomOptionId - id of the normal or custom shipping option to add as valid method * @param {string} option - id of the normal or custom shipping option to add as valid method
* @returns {{ optionId: string; customPrice: { price: number; } | {};}} * @returns {CustomShippingOption | undefined}
*/ */
extractShippingOptionIdAndPrice(cart, optionIdOrCustomOptionId) { findCustomShippingOption(cart, optionId) {
if ( let customOption = cart.custom_shipping_options?.find(
cart.custom_shipping_options && cso => cso.shipping_option_id === optionId
cart.custom_shipping_options.length > 0 )
) { const hasCustomOptions = cart.custom_shipping_options?.length
const customOption = cart.custom_shipping_options.find(
cso => cso.id === optionIdOrCustomOptionId if (hasCustomOptions && !customOption) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Wrong shipping option"
) )
if (customOption) {
return {
optionId: customOption.shipping_option_id,
customPrice: { price: customOption.price },
}
}
} }
return { return customOption
optionId: optionIdOrCustomOptionId,
customPrice: {},
}
} }
/** /**
@@ -410,13 +410,15 @@ class ShippingProfileService extends BaseService {
* Finds all the shipping profiles that cover the products in a cart, and * Finds all the shipping profiles that cover the products in a cart, and
* validates all options that are available for the cart. * validates all options that are available for the cart.
* @param {Cart} cart - the cart object to find shipping options for * @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) { async fetchCartOptions(cart) {
const customShippingOptions = cart.custom_shipping_options if (cart.custom_shipping_options?.length) {
return cart.custom_shipping_options.map(cso => ({
if (customShippingOptions && customShippingOptions.length > 0) ...cso.shipping_option,
return customShippingOptions amount: cso.price,
}))
}
const profileIds = this.getProfilesInCart_(cart) const profileIds = this.getProfilesInCart_(cart)