fix(medusa): Shipping profile CRUD (#3154)
**What** - Fixes wrong payload class for `POST /admin/shipping-profiles` - Fixes wrong payload class for `POST /admin/shipping-profiles/:id` - Fixes an issue where updating a shipping profile with products and/or shipping options would fail. - Fixes an issue where passing `profile_id` to `ShippingOptionService.update()` would not update the shipping profile of the option. **Testing** - Adds new `simpleshippingProfileFactory` - Adds new integration test suite for shipping profiles operations. Resolves CORE-1065
This commit is contained in:
committed by
GitHub
parent
4d6e63d68f
commit
d0adaf57ed
5
.changeset/mean-ghosts-live.md
Normal file
5
.changeset/mean-ghosts-live.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
fix(medusa): Fixes payloads associated with shipping profile requests, as well as fixes to the shippingProfileService. Also adds test suite for shipping profiles.
|
||||
346
integration-tests/api/__tests__/admin/shipping-profile.js
Normal file
346
integration-tests/api/__tests__/admin/shipping-profile.js
Normal file
@@ -0,0 +1,346 @@
|
||||
const path = require("path")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
const {
|
||||
simpleProductFactory,
|
||||
simpleShippingOptionFactory,
|
||||
simpleShippingProfileFactory,
|
||||
} = require("../../factories")
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
|
||||
const adminReqConfig = {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("/admin/shipping-profiles", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
dbConnection = await initDb({ cwd })
|
||||
medusaProcess = await setupServer({ cwd, verbose: false })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("GET /admin/shipping-profiles", () => {
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("lists shipping profiles", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const {
|
||||
data: { shipping_profiles },
|
||||
status,
|
||||
} = await api.get("/admin/shipping-profiles", adminReqConfig)
|
||||
|
||||
expect(status).toEqual(200)
|
||||
|
||||
// Should contain default and gift_card profiles
|
||||
expect(shipping_profiles.length).toEqual(2)
|
||||
})
|
||||
|
||||
it("gets a shipping profile by id", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const profile = await simpleShippingProfileFactory(dbConnection)
|
||||
|
||||
const {
|
||||
data: { shipping_profile },
|
||||
status,
|
||||
} = await api.get(
|
||||
`/admin/shipping-profiles/${profile.id}`,
|
||||
adminReqConfig
|
||||
)
|
||||
|
||||
expect(status).toEqual(200)
|
||||
expect(shipping_profile).toEqual(
|
||||
expect.objectContaining({
|
||||
...profile,
|
||||
updated_at: expect.any(String),
|
||||
created_at: expect.any(String),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /admin/shipping-profiles", () => {
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("creates a custom shipping profile", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const payload = {
|
||||
name: "test-profile-2023",
|
||||
type: "custom",
|
||||
}
|
||||
|
||||
const {
|
||||
data: { shipping_profile },
|
||||
status,
|
||||
} = await api.post("/admin/shipping-profiles", payload, adminReqConfig)
|
||||
|
||||
expect(status).toEqual(200)
|
||||
expect(shipping_profile).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
deleted_at: null,
|
||||
...payload,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("creates a default shipping profile", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const payload = {
|
||||
name: "test-profile-2023",
|
||||
type: "default",
|
||||
}
|
||||
|
||||
const {
|
||||
data: { shipping_profile },
|
||||
status,
|
||||
} = await api.post("/admin/shipping-profiles", payload, adminReqConfig)
|
||||
|
||||
expect(status).toEqual(200)
|
||||
expect(shipping_profile).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
deleted_at: null,
|
||||
...payload,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("creates a gift_card shipping profile", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const payload = {
|
||||
name: "test-profile-2023",
|
||||
type: "gift_card",
|
||||
}
|
||||
|
||||
const {
|
||||
data: { shipping_profile },
|
||||
status,
|
||||
} = await api.post("/admin/shipping-profiles", payload, adminReqConfig)
|
||||
|
||||
expect(status).toEqual(200)
|
||||
expect(shipping_profile).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
deleted_at: null,
|
||||
...payload,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("creates a shipping profile with metadata", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const payload = {
|
||||
name: "test-profile-2023",
|
||||
type: "default",
|
||||
metadata: {
|
||||
custom_key: "custom_value",
|
||||
},
|
||||
}
|
||||
|
||||
const {
|
||||
data: { shipping_profile },
|
||||
status,
|
||||
} = await api.post("/admin/shipping-profiles", payload, adminReqConfig)
|
||||
|
||||
expect(status).toEqual(200)
|
||||
expect(shipping_profile).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
deleted_at: null,
|
||||
...payload,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("fails to create a shipping profile with invalid type", async () => {
|
||||
const api = useApi()
|
||||
expect.assertions(2)
|
||||
|
||||
const payload = {
|
||||
name: "test-profile-2023",
|
||||
type: "invalid",
|
||||
}
|
||||
|
||||
await api
|
||||
.post("/admin/shipping-profiles", payload, adminReqConfig)
|
||||
.catch((err) => {
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data.message).toEqual(
|
||||
"type must be one of 'default', 'custom', 'gift_card'"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("updates a shipping profile", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const testProducts = await Promise.all(
|
||||
[...Array(5).keys()].map(async () => {
|
||||
return await simpleProductFactory(dbConnection)
|
||||
})
|
||||
)
|
||||
|
||||
const testShippingOptions = await Promise.all(
|
||||
[...Array(5).keys()].map(async () => {
|
||||
return await simpleShippingOptionFactory(dbConnection)
|
||||
})
|
||||
)
|
||||
|
||||
const payload = {
|
||||
name: "test-profile-2023",
|
||||
type: "custom",
|
||||
metadata: {
|
||||
my_key: "my_value",
|
||||
},
|
||||
}
|
||||
|
||||
const {
|
||||
data: { shipping_profile: created },
|
||||
} = await api.post("/admin/shipping-profiles", payload, adminReqConfig)
|
||||
|
||||
const updatePayload = {
|
||||
name: "test-profile-2023-updated",
|
||||
products: testProducts.map((p) => p.id),
|
||||
shipping_options: testShippingOptions.map((o) => o.id),
|
||||
metadata: {
|
||||
my_key: "",
|
||||
my_new_key: "my_new_value",
|
||||
},
|
||||
}
|
||||
|
||||
const {
|
||||
data: { shipping_profile },
|
||||
status,
|
||||
} = await api.post(
|
||||
`/admin/shipping-profiles/${created.id}`,
|
||||
updatePayload,
|
||||
adminReqConfig
|
||||
)
|
||||
|
||||
expect(status).toEqual(200)
|
||||
expect(shipping_profile).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "test-profile-2023-updated",
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
metadata: {
|
||||
my_new_key: "my_new_value",
|
||||
},
|
||||
deleted_at: null,
|
||||
type: "custom",
|
||||
})
|
||||
)
|
||||
|
||||
const {
|
||||
data: { products },
|
||||
} = await api.get(`/admin/products`, adminReqConfig)
|
||||
|
||||
expect(products.length).toEqual(5)
|
||||
expect(products).toEqual(
|
||||
expect.arrayContaining(
|
||||
testProducts.map((p) => {
|
||||
return expect.objectContaining({
|
||||
id: p.id,
|
||||
profile_id: shipping_profile.id,
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const {
|
||||
data: { shipping_options },
|
||||
} = await api.get(`/admin/shipping-options`, adminReqConfig)
|
||||
|
||||
const numberOfShippingOptionsWithProfile = shipping_options.filter(
|
||||
(so) => so.profile_id === shipping_profile.id
|
||||
).length
|
||||
|
||||
expect(numberOfShippingOptionsWithProfile).toEqual(5)
|
||||
expect(shipping_options).toEqual(
|
||||
expect.arrayContaining(
|
||||
testShippingOptions.map((o) => {
|
||||
return expect.objectContaining({
|
||||
id: o.id,
|
||||
profile_id: shipping_profile.id,
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("DELETE /admin/shipping-profiles", () => {
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it("deletes a shipping profile", async () => {
|
||||
expect.assertions(2)
|
||||
|
||||
const api = useApi()
|
||||
|
||||
const profile = await simpleShippingProfileFactory(dbConnection)
|
||||
|
||||
const { status } = await api.delete(
|
||||
`/admin/shipping-profiles/${profile.id}`,
|
||||
adminReqConfig
|
||||
)
|
||||
|
||||
expect(status).toEqual(200)
|
||||
await api
|
||||
.get(`/admin/shipping-profiles/${profile.id}`, adminReqConfig)
|
||||
.catch((err) => {
|
||||
expect(err.response.status).toEqual(404)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,25 +1,25 @@
|
||||
export * from "./simple-gift-card-factory"
|
||||
export * from "./simple-payment-factory"
|
||||
export * from "./simple-batch-job-factory"
|
||||
export * from "./simple-discount-factory"
|
||||
export * from "./simple-order-factory"
|
||||
export * from "./simple-cart-factory"
|
||||
export * from "./simple-region-factory"
|
||||
export * from "./simple-custom-shipping-option-factory"
|
||||
export * from "./simple-customer-factory"
|
||||
export * from "./simple-discount-factory"
|
||||
export * from "./simple-gift-card-factory"
|
||||
export * from "./simple-line-item-factory"
|
||||
export * from "./simple-order-edit-factory"
|
||||
export * from "./simple-order-factory"
|
||||
export * from "./simple-order-item-change-factory"
|
||||
export * from "./simple-payment-collection-factory"
|
||||
export * from "./simple-payment-factory"
|
||||
export * from "./simple-price-list-factory"
|
||||
export * from "./simple-product-category-factory"
|
||||
export * from "./simple-product-factory"
|
||||
export * from "./simple-product-variant-factory"
|
||||
export * from "./simple-product-tax-rate-factory"
|
||||
export * from "./simple-product-type-tax-rate-factory"
|
||||
export * from "./simple-product-variant-factory"
|
||||
export * from "./simple-region-factory"
|
||||
export * from "./simple-sales-channel-factory"
|
||||
export * from "./simple-shipping-method-factory"
|
||||
export * from "./simple-shipping-option-factory"
|
||||
export * from "./simple-shipping-profile-factory"
|
||||
export * from "./simple-shipping-tax-rate-factory"
|
||||
export * from "./simple-tax-rate-factory"
|
||||
export * from "./simple-shipping-option-factory"
|
||||
export * from "./simple-shipping-method-factory"
|
||||
export * from "./simple-product-type-tax-rate-factory"
|
||||
export * from "./simple-price-list-factory"
|
||||
export * from "./simple-batch-job-factory"
|
||||
export * from "./simple-sales-channel-factory"
|
||||
export * from "./simple-custom-shipping-option-factory"
|
||||
export * from "./simple-payment-collection-factory"
|
||||
export * from "./simple-order-edit-factory"
|
||||
export * from "./simple-order-item-change-factory"
|
||||
export * from "./simple-customer-factory"
|
||||
export * from "./simple-product-category-factory"
|
||||
|
||||
@@ -7,28 +7,29 @@ import {
|
||||
} from "@medusajs/medusa"
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
import { simpleRegionFactory } from "./simple-region-factory"
|
||||
|
||||
export type ShippingOptionFactoryData = {
|
||||
id?: string
|
||||
name?: string
|
||||
region_id: string
|
||||
region_id?: string
|
||||
is_return?: boolean
|
||||
is_giftcard?: boolean
|
||||
price?: number
|
||||
price_type?: ShippingOptionPriceType
|
||||
includes_tax?: boolean
|
||||
data?: object
|
||||
requirements: ShippingOptionRequirementData[]
|
||||
requirements?: ShippingOptionRequirementData[]
|
||||
}
|
||||
|
||||
type ShippingOptionRequirementData = {
|
||||
type: 'min_subtotal' | 'max_subtotal'
|
||||
type: "min_subtotal" | "max_subtotal"
|
||||
amount: number
|
||||
}
|
||||
|
||||
export const simpleShippingOptionFactory = async (
|
||||
connection: Connection,
|
||||
data: ShippingOptionFactoryData,
|
||||
data: ShippingOptionFactoryData = {},
|
||||
seed?: number
|
||||
): Promise<ShippingOption> => {
|
||||
if (typeof seed !== "undefined") {
|
||||
@@ -44,11 +45,18 @@ export const simpleShippingOptionFactory = async (
|
||||
type: ShippingProfileType.GIFT_CARD,
|
||||
})
|
||||
|
||||
let region_id = data.region_id
|
||||
|
||||
if (!region_id) {
|
||||
const { id } = await simpleRegionFactory(connection)
|
||||
region_id = id
|
||||
}
|
||||
|
||||
const shippingOptionData = {
|
||||
id: data.id ?? `simple-so-${Math.random() * 1000}`,
|
||||
name: data.name || "Test Method",
|
||||
is_return: data.is_return ?? false,
|
||||
region_id: data.region_id,
|
||||
region_id: region_id,
|
||||
provider_id: "test-ful",
|
||||
profile_id: data.is_giftcard ? gcProfile.id : defaultProfile.id,
|
||||
price_type: data.price_type ?? ShippingOptionPriceType.FLAT_RATE,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ShippingProfile, ShippingProfileType } from "@medusajs/medusa"
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
|
||||
export type ShippingProfileFactoryData = {
|
||||
id?: string
|
||||
name?: string
|
||||
type?: ShippingProfileType
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const simpleShippingProfileFactory = async (
|
||||
connection: Connection,
|
||||
data: ShippingOptionFactoryData = {},
|
||||
seed?: number
|
||||
): Promise<ShippingProfile> => {
|
||||
if (typeof seed !== "undefined") {
|
||||
faker.seed(seed)
|
||||
}
|
||||
|
||||
const manager = connection.manager
|
||||
|
||||
const shippingProfileData = {
|
||||
id: data.id ?? `simple-sp-${Math.random() * 1000}`,
|
||||
name: data.name || `sp-${Math.random() * 1000}`,
|
||||
type: data.type || ShippingProfileType.DEFAULT,
|
||||
metadata: data.metadata,
|
||||
products: [],
|
||||
shipping_options: [],
|
||||
}
|
||||
|
||||
const created = manager.create(ShippingProfile, shippingProfileData)
|
||||
|
||||
return await manager.save(created)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ describe("POST /admin/shipping-profiles", () => {
|
||||
subject = await request("POST", "/admin/shipping-profiles", {
|
||||
payload: {
|
||||
name: "Test Profile",
|
||||
type: "default",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
@@ -27,6 +28,7 @@ describe("POST /admin/shipping-profiles", () => {
|
||||
expect(ShippingProfileServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(ShippingProfileServiceMock.create).toHaveBeenCalledWith({
|
||||
name: "Test Profile",
|
||||
type: "default",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IsString } from "class-validator"
|
||||
import { IsEnum, IsObject, IsOptional, IsString } from "class-validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { ShippingProfileType } from "../../../../models"
|
||||
import { ShippingProfileService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
/**
|
||||
* @oas [post] /shipping-profiles
|
||||
@@ -84,12 +85,26 @@ export default async (req, res) => {
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - type
|
||||
* properties:
|
||||
* name:
|
||||
* description: "The name of the Shipping Profile"
|
||||
* description: The name of the Shipping Profile
|
||||
* type: string
|
||||
* type:
|
||||
* description: The type of the Shipping Profile
|
||||
* type: string
|
||||
* enum: [default, gift_card, custom]
|
||||
*/
|
||||
export class AdminPostShippingProfilesReq {
|
||||
@IsString()
|
||||
name: string
|
||||
|
||||
@IsEnum(ShippingProfileType, {
|
||||
message: "type must be one of 'default', 'custom', 'gift_card'",
|
||||
})
|
||||
type: ShippingProfileType
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from "class-validator"
|
||||
|
||||
import { EntityManager } from "typeorm"
|
||||
import { ShippingProfileType } from "../../../../models"
|
||||
import { ShippingProfileService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
/**
|
||||
* @oas [post] /shipping-profiles/{id}
|
||||
@@ -93,11 +100,44 @@ export default async (req, res) => {
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* description: "The name of the Shipping Profile"
|
||||
* description: The name of the Shipping Profile
|
||||
* type: string
|
||||
* metadata:
|
||||
* description: An optional set of key-value pairs with additional information.
|
||||
* type: object
|
||||
* type:
|
||||
* description: The type of the Shipping Profile
|
||||
* type: string
|
||||
* enum: [default, gift_card, custom]
|
||||
* products:
|
||||
* description: An optional array of product ids to associate with the Shipping Profile
|
||||
* type: array
|
||||
* shipping_options:
|
||||
* description: An optional array of shipping option ids to associate with the Shipping Profile
|
||||
* type: array
|
||||
*/
|
||||
export class AdminPostShippingProfilesProfileReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ShippingProfileType, {
|
||||
message: "type must be one of 'default', 'custom', 'gift_card'",
|
||||
})
|
||||
type?: ShippingProfileType
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
products?: string[]
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
shipping_options?: string[]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
In,
|
||||
Repository,
|
||||
} from "typeorm"
|
||||
import { PriceList, Product, SalesChannel, ProductCategory } from "../models"
|
||||
import { PriceList, Product, ProductCategory, SalesChannel } from "../models"
|
||||
import {
|
||||
ExtendedFindConfig,
|
||||
Selector,
|
||||
@@ -540,6 +540,25 @@ export class ProductRepository extends Repository<Product> {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upserts shipping profile for products
|
||||
* @param productIds IDs of products to update
|
||||
* @param shippingProfileId ID of shipping profile to assign to products
|
||||
* @returns updated products
|
||||
*/
|
||||
public async upsertShippingProfile(
|
||||
productIds: string[],
|
||||
shippingProfileId: string
|
||||
): Promise<Product[]> {
|
||||
await this.createQueryBuilder()
|
||||
.update(Product)
|
||||
.set({ profile_id: shippingProfileId })
|
||||
.where({ id: In(productIds) })
|
||||
.execute()
|
||||
|
||||
return await this.findByIds(productIds)
|
||||
}
|
||||
|
||||
private _cleanOptions(
|
||||
options: FindWithoutRelationsOptions
|
||||
): WithRequiredProperty<FindWithoutRelationsOptions, "where"> {
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { EntityRepository, In, Repository } from "typeorm"
|
||||
import { ShippingOption } from "../models/shipping-option"
|
||||
|
||||
@EntityRepository(ShippingOption)
|
||||
export class ShippingOptionRepository extends Repository<ShippingOption> {}
|
||||
export class ShippingOptionRepository extends Repository<ShippingOption> {
|
||||
public async upsertShippingProfile(
|
||||
shippingOptionIds: string[],
|
||||
shippingProfileId: string
|
||||
): Promise<ShippingOption[]> {
|
||||
await this.createQueryBuilder()
|
||||
.update(ShippingOption)
|
||||
.set({ profile_id: shippingProfileId })
|
||||
.where({ id: In(shippingOptionIds) })
|
||||
.execute()
|
||||
|
||||
return this.findByIds(shippingOptionIds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ export const ProductServiceMock = {
|
||||
deleteOption: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve(products.productWithOptions)),
|
||||
updateshippingProfiles: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
retrieveVariants: jest.fn().mockImplementation((productId) => {
|
||||
if (productId === IdMap.getId("product1")) {
|
||||
return Promise.resolve([
|
||||
|
||||
@@ -89,6 +89,7 @@ export const ShippingOptionServiceMock = {
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
update: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
updateShippingprofile: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
listAndCount: jest.fn().mockImplementation((data) => {
|
||||
if (data.region_id === IdMap.getId("region-france")) {
|
||||
return Promise.resolve([[shippingOptions.franceShipping], 1])
|
||||
|
||||
@@ -19,10 +19,10 @@ export const ShippingProfileServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
update: jest.fn().mockImplementation(data => {
|
||||
update: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
create: jest.fn().mockImplementation(data => {
|
||||
create: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
createDefault: jest.fn().mockImplementation(() => {
|
||||
@@ -31,7 +31,7 @@ export const ShippingProfileServiceMock = {
|
||||
createGiftCardDefault: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(data => {
|
||||
retrieve: jest.fn().mockImplementation((data) => {
|
||||
if (data === IdMap.getId("default")) {
|
||||
return Promise.resolve(profiles.default)
|
||||
}
|
||||
@@ -40,13 +40,13 @@ export const ShippingProfileServiceMock = {
|
||||
}
|
||||
return Promise.resolve(profiles.default)
|
||||
}),
|
||||
retrieveGiftCardDefault: jest.fn().mockImplementation(data => {
|
||||
retrieveGiftCardDefault: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({ id: IdMap.getId("giftCardProfile") })
|
||||
}),
|
||||
retrieveDefault: jest.fn().mockImplementation(data => {
|
||||
retrieveDefault: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({ id: IdMap.getId("default_shipping_profile") })
|
||||
}),
|
||||
list: jest.fn().mockImplementation(selector => {
|
||||
list: jest.fn().mockImplementation((selector) => {
|
||||
if (!selector) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export const ShippingProfileServiceMock = {
|
||||
])
|
||||
}
|
||||
}),
|
||||
decorate: jest.fn().mockImplementation(d => Promise.resolve(d)),
|
||||
decorate: jest.fn().mockImplementation((d) => Promise.resolve(d)),
|
||||
addShippingOption: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
removeShippingOption: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
addProduct: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
|
||||
@@ -35,14 +35,14 @@ describe("ShippingProfileService", () => {
|
||||
})
|
||||
|
||||
const productService = {
|
||||
update: jest.fn(),
|
||||
updateShippingProfile: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const shippingOptionService = {
|
||||
update: jest.fn(),
|
||||
updateShippingProfile: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
@@ -75,10 +75,11 @@ describe("ShippingProfileService", () => {
|
||||
products: [IdMap.getId("product1")],
|
||||
})
|
||||
|
||||
expect(productService.update).toBeCalledTimes(1)
|
||||
expect(productService.update).toBeCalledWith(IdMap.getId("product1"), {
|
||||
profile_id: id,
|
||||
})
|
||||
expect(productService.updateShippingProfile).toBeCalledTimes(1)
|
||||
expect(productService.updateShippingProfile).toBeCalledWith(
|
||||
[IdMap.getId("product1")],
|
||||
id
|
||||
)
|
||||
})
|
||||
|
||||
it("calls updateOne with shipping options", async () => {
|
||||
@@ -88,10 +89,10 @@ describe("ShippingProfileService", () => {
|
||||
shipping_options: [IdMap.getId("validId")],
|
||||
})
|
||||
|
||||
expect(shippingOptionService.update).toBeCalledTimes(1)
|
||||
expect(shippingOptionService.update).toBeCalledWith(
|
||||
IdMap.getId("validId"),
|
||||
{ profile_id: id }
|
||||
expect(shippingOptionService.updateShippingProfile).toBeCalledTimes(1)
|
||||
expect(shippingOptionService.updateShippingProfile).toBeCalledWith(
|
||||
[IdMap.getId("validId")],
|
||||
id
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -125,7 +126,7 @@ describe("ShippingProfileService", () => {
|
||||
const profRepo = MockRepository({ findOne: () => Promise.resolve({}) })
|
||||
|
||||
const productService = {
|
||||
update: jest.fn(),
|
||||
updateShippingProfile: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
@@ -142,15 +143,15 @@ describe("ShippingProfileService", () => {
|
||||
})
|
||||
|
||||
it("add product to profile successfully", async () => {
|
||||
await profileService.addProduct(
|
||||
IdMap.getId("validId"),
|
||||
IdMap.getId("product2")
|
||||
)
|
||||
await profileService.addProduct(IdMap.getId("validId"), [
|
||||
IdMap.getId("product2"),
|
||||
])
|
||||
|
||||
expect(productService.update).toBeCalledTimes(1)
|
||||
expect(productService.update).toBeCalledWith(IdMap.getId("product2"), {
|
||||
profile_id: IdMap.getId("validId"),
|
||||
})
|
||||
expect(productService.updateShippingProfile).toBeCalledTimes(1)
|
||||
expect(productService.updateShippingProfile).toBeCalledWith(
|
||||
[IdMap.getId("product2")],
|
||||
IdMap.getId("validId")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -300,7 +301,7 @@ describe("ShippingProfileService", () => {
|
||||
const profRepo = MockRepository({ findOne: () => Promise.resolve({}) })
|
||||
|
||||
const shippingOptionService = {
|
||||
update: jest.fn(),
|
||||
updateShippingProfile: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
@@ -317,15 +318,14 @@ describe("ShippingProfileService", () => {
|
||||
})
|
||||
|
||||
it("add shipping option to profile successfully", async () => {
|
||||
await profileService.addShippingOption(
|
||||
IdMap.getId("validId"),
|
||||
IdMap.getId("freeShipping")
|
||||
)
|
||||
|
||||
expect(shippingOptionService.update).toBeCalledTimes(1)
|
||||
expect(shippingOptionService.update).toBeCalledWith(
|
||||
await profileService.addShippingOption(IdMap.getId("validId"), [
|
||||
IdMap.getId("freeShipping"),
|
||||
{ profile_id: IdMap.getId("validId") }
|
||||
])
|
||||
|
||||
expect(shippingOptionService.updateShippingProfile).toBeCalledTimes(1)
|
||||
expect(shippingOptionService.updateShippingProfile).toBeCalledWith(
|
||||
[IdMap.getId("freeShipping")],
|
||||
IdMap.getId("validId")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager, In } from "typeorm"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { ProductVariantService, SearchService } from "."
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
||||
import {
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductOption,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
SalesChannel,
|
||||
ProductCategory,
|
||||
} from "../models"
|
||||
import { ImageRepository } from "../repositories/image"
|
||||
import {
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
ProductSelector,
|
||||
UpdateProductInput,
|
||||
} from "../types/product"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { buildQuery, isString, setMetadata } from "../utils"
|
||||
import EventBusService from "./event-bus"
|
||||
|
||||
type InjectedDependencies = {
|
||||
@@ -446,7 +446,9 @@ class ProductService extends TransactionBaseService {
|
||||
|
||||
if (categories?.length) {
|
||||
const categoryIds = categories.map((c) => c.id)
|
||||
const categoryRecords = categoryIds.map((id) => ({ id } as ProductCategory))
|
||||
const categoryRecords = categoryIds.map(
|
||||
(id) => ({ id } as ProductCategory)
|
||||
)
|
||||
|
||||
product.categories = categoryRecords
|
||||
}
|
||||
@@ -560,7 +562,9 @@ class ProductService extends TransactionBaseService {
|
||||
|
||||
if (categories?.length) {
|
||||
const categoryIds = categories.map((c) => c.id)
|
||||
const categoryRecords = categoryIds.map((id) => ({ id } as ProductCategory))
|
||||
const categoryRecords = categoryIds.map(
|
||||
(id) => ({ id } as ProductCategory)
|
||||
)
|
||||
|
||||
product.categories = categoryRecords
|
||||
}
|
||||
@@ -869,6 +873,31 @@ class ProductService extends TransactionBaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param productIds ID or IDs of the products to update
|
||||
* @param profileId Shipping profile ID to update the shipping options with
|
||||
* @returns updated shipping options
|
||||
*/
|
||||
async updateShippingProfile(
|
||||
productIds: string | string[],
|
||||
profileId: string
|
||||
): Promise<Product[]> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||
|
||||
const ids = isString(productIds) ? [productIds] : productIds
|
||||
|
||||
const products = await productRepo.upsertShippingProfile(ids, profileId)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ProductService.Events.UPDATED, products)
|
||||
|
||||
return products
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a query object to be used for list queries.
|
||||
* @param selector - the selector to create the query from
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
UpdateShippingOptionInput,
|
||||
ValidatePriceTypeAndAmountInput,
|
||||
} from "../types/shipping-options"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { buildQuery, isString, setMetadata } from "../utils"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import RegionService from "./region"
|
||||
@@ -663,6 +663,10 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
optionWithValidatedPrice.admin_only = update.admin_only
|
||||
}
|
||||
|
||||
if (isDefined(update.profile_id)) {
|
||||
optionWithValidatedPrice.profile_id = update.profile_id
|
||||
}
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
TaxInclusivePricingFeatureFlag.key
|
||||
@@ -754,6 +758,25 @@ class ShippingOptionService extends TransactionBaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param optionIds ID or IDs of the shipping options to update
|
||||
* @param profileId Shipping profile ID to update the shipping options with
|
||||
* @returns updated shipping options
|
||||
*/
|
||||
async updateShippingProfile(
|
||||
optionIds: string | string[],
|
||||
profileId: string
|
||||
): Promise<ShippingOption[]> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const optionRepo = manager.getCustomRepository(this.optionRepository_)
|
||||
|
||||
const ids = isString(optionIds) ? [optionIds] : optionIds
|
||||
|
||||
return await optionRepo.upsertShippingProfile(ids, profileId)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount to be paid for a shipping method. Will ask the
|
||||
* fulfillment provider to calculate the price if the shipping option has the
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
CreateShippingProfile,
|
||||
UpdateShippingProfile,
|
||||
} from "../types/shipping-profile"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { buildQuery, isString, setMetadata } from "../utils"
|
||||
import CustomShippingOptionService from "./custom-shipping-option"
|
||||
import ProductService from "./product"
|
||||
import ShippingOptionService from "./shipping-option"
|
||||
@@ -260,7 +260,14 @@ class ShippingProfileService extends TransactionBaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const created = profileRepository.create(profile)
|
||||
const { metadata, ...rest } = profile
|
||||
|
||||
const created = profileRepository.create(rest)
|
||||
|
||||
if (metadata) {
|
||||
created.metadata = setMetadata(created, metadata)
|
||||
}
|
||||
|
||||
const result = await profileRepository.save(created)
|
||||
return result
|
||||
})
|
||||
@@ -284,7 +291,7 @@ class ShippingProfileService extends TransactionBaseService {
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const profile = await this.retrieve(profileId, {
|
||||
let profile = await this.retrieve(profileId, {
|
||||
relations: [
|
||||
"products",
|
||||
"products.profile",
|
||||
@@ -295,27 +302,16 @@ class ShippingProfileService extends TransactionBaseService {
|
||||
|
||||
const { metadata, products, shipping_options, ...rest } = update
|
||||
|
||||
if (metadata) {
|
||||
profile.metadata = setMetadata(profile, metadata)
|
||||
}
|
||||
|
||||
if (products) {
|
||||
const productServiceTx = this.productService_.withTransaction(manager)
|
||||
for (const pId of products) {
|
||||
await productServiceTx.update(pId, {
|
||||
profile_id: profile.id,
|
||||
})
|
||||
}
|
||||
profile = await this.addProduct(profile.id, products)
|
||||
}
|
||||
|
||||
if (shipping_options) {
|
||||
const shippingOptionServiceTx =
|
||||
this.shippingOptionService_.withTransaction(manager)
|
||||
for (const oId of shipping_options) {
|
||||
await shippingOptionServiceTx.update(oId, {
|
||||
profile_id: profile.id,
|
||||
})
|
||||
}
|
||||
profile = await this.addShippingOption(profile.id, shipping_options)
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
profile.metadata = setMetadata(profile, metadata)
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
@@ -352,22 +348,31 @@ class ShippingProfileService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a product to a profile. The method is idempotent, so multiple calls
|
||||
* with the same product variant will have the same result.
|
||||
* @param profileId - the profile to add the product to.
|
||||
* @param productId - the product to add.
|
||||
* Adds a product of an array of products to the profile.
|
||||
* @param profileId - the profile to add the products to.
|
||||
* @param productId - the ID of the product or multiple products to add.
|
||||
* @return the result of update
|
||||
*/
|
||||
async addProduct(
|
||||
profileId: string,
|
||||
productId: string
|
||||
productId: string | string[]
|
||||
): Promise<ShippingProfile> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
await this.productService_
|
||||
.withTransaction(manager)
|
||||
.update(productId, { profile_id: profileId })
|
||||
const productServiceTx = this.productService_.withTransaction(manager)
|
||||
|
||||
return await this.retrieve(profileId)
|
||||
await productServiceTx.updateShippingProfile(
|
||||
isString(productId) ? [productId] : productId,
|
||||
profileId
|
||||
)
|
||||
|
||||
return await this.retrieve(profileId, {
|
||||
relations: [
|
||||
"products",
|
||||
"products.profile",
|
||||
"shipping_options",
|
||||
"shipping_options.profile",
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -375,20 +380,30 @@ class ShippingProfileService extends TransactionBaseService {
|
||||
* Adds a shipping option to the profile. The shipping option can be used to
|
||||
* fulfill the products in the products field.
|
||||
* @param profileId - the profile to apply the shipping option to
|
||||
* @param optionId - the option to add to the profile
|
||||
* @param optionId - the ID of the option or multiple options to add to the profile
|
||||
* @return the result of the model update operation
|
||||
*/
|
||||
async addShippingOption(
|
||||
profileId: string,
|
||||
optionId: string
|
||||
optionId: string | string[]
|
||||
): Promise<ShippingProfile> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.update(optionId, { profile_id: profileId })
|
||||
const shippingOptionServiceTx =
|
||||
this.shippingOptionService_.withTransaction(manager)
|
||||
|
||||
const updated = await this.retrieve(profileId)
|
||||
return updated
|
||||
await shippingOptionServiceTx.updateShippingProfile(
|
||||
isString(optionId) ? [optionId] : optionId,
|
||||
profileId
|
||||
)
|
||||
|
||||
return await this.retrieve(profileId, {
|
||||
relations: [
|
||||
"products",
|
||||
"products.profile",
|
||||
"shipping_options",
|
||||
"shipping_options.profile",
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Product, ShippingOption, ShippingProfileType } from "../models"
|
||||
import { ShippingProfileType } from "../models"
|
||||
|
||||
export type CreateShippingProfile = {
|
||||
name: string
|
||||
type: ShippingProfileType
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type UpdateShippingProfile = {
|
||||
|
||||
Reference in New Issue
Block a user