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:
1 parent
4d6e63d68f
commit
d0adaf57ed
18 files changed
+663
-109
No files matched your search
@@ -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.
|
||||||
@@ -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-batch-job-factory"
|
||||||
export * from "./simple-discount-factory"
|
|
||||||
export * from "./simple-order-factory"
|
|
||||||
export * from "./simple-cart-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-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-factory"
|
||||||
export * from "./simple-product-variant-factory"
|
|
||||||
export * from "./simple-product-tax-rate-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-shipping-tax-rate-factory"
|
||||||
export * from "./simple-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"
|
} from "@medusajs/medusa"
|
||||||
import faker from "faker"
|
import faker from "faker"
|
||||||
import { Connection } from "typeorm"
|
import { Connection } from "typeorm"
|
||||||
|
import { simpleRegionFactory } from "./simple-region-factory"
|
||||||
|
|
||||||
export type ShippingOptionFactoryData = {
|
export type ShippingOptionFactoryData = {
|
||||||
id?: string
|
id?: string
|
||||||
name?: string
|
name?: string
|
||||||
region_id: string
|
region_id?: string
|
||||||
is_return?: boolean
|
is_return?: boolean
|
||||||
is_giftcard?: boolean
|
is_giftcard?: boolean
|
||||||
price?: number
|
price?: number
|
||||||
price_type?: ShippingOptionPriceType
|
price_type?: ShippingOptionPriceType
|
||||||
includes_tax?: boolean
|
includes_tax?: boolean
|
||||||
data?: object
|
data?: object
|
||||||
requirements: ShippingOptionRequirementData[]
|
requirements?: ShippingOptionRequirementData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShippingOptionRequirementData = {
|
type ShippingOptionRequirementData = {
|
||||||
type: 'min_subtotal' | 'max_subtotal'
|
type: "min_subtotal" | "max_subtotal"
|
||||||
amount: number
|
amount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const simpleShippingOptionFactory = async (
|
export const simpleShippingOptionFactory = async (
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
data: ShippingOptionFactoryData,
|
data: ShippingOptionFactoryData = {},
|
||||||
seed?: number
|
seed?: number
|
||||||
): Promise<ShippingOption> => {
|
): Promise<ShippingOption> => {
|
||||||
if (typeof seed !== "undefined") {
|
if (typeof seed !== "undefined") {
|
||||||
@@ -44,11 +45,18 @@ export const simpleShippingOptionFactory = async (
|
|||||||
type: ShippingProfileType.GIFT_CARD,
|
type: ShippingProfileType.GIFT_CARD,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let region_id = data.region_id
|
||||||
|
|
||||||
|
if (!region_id) {
|
||||||
|
const { id } = await simpleRegionFactory(connection)
|
||||||
|
region_id = id
|
||||||
|
}
|
||||||
|
|
||||||
const shippingOptionData = {
|
const shippingOptionData = {
|
||||||
id: data.id ?? `simple-so-${Math.random() * 1000}`,
|
id: data.id ?? `simple-so-${Math.random() * 1000}`,
|
||||||
name: data.name || "Test Method",
|
name: data.name || "Test Method",
|
||||||
is_return: data.is_return ?? false,
|
is_return: data.is_return ?? false,
|
||||||
region_id: data.region_id,
|
region_id: region_id,
|
||||||
provider_id: "test-ful",
|
provider_id: "test-ful",
|
||||||
profile_id: data.is_giftcard ? gcProfile.id : defaultProfile.id,
|
profile_id: data.is_giftcard ? gcProfile.id : defaultProfile.id,
|
||||||
price_type: data.price_type ?? ShippingOptionPriceType.FLAT_RATE,
|
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)
|
||||||
|
}
|
||||||
+2
@@ -10,6 +10,7 @@ describe("POST /admin/shipping-profiles", () => {
|
|||||||
subject = await request("POST", "/admin/shipping-profiles", {
|
subject = await request("POST", "/admin/shipping-profiles", {
|
||||||
payload: {
|
payload: {
|
||||||
name: "Test Profile",
|
name: "Test Profile",
|
||||||
|
type: "default",
|
||||||
},
|
},
|
||||||
adminSession: {
|
adminSession: {
|
||||||
jwt: {
|
jwt: {
|
||||||
@@ -27,6 +28,7 @@ describe("POST /admin/shipping-profiles", () => {
|
|||||||
expect(ShippingProfileServiceMock.create).toHaveBeenCalledTimes(1)
|
expect(ShippingProfileServiceMock.create).toHaveBeenCalledTimes(1)
|
||||||
expect(ShippingProfileServiceMock.create).toHaveBeenCalledWith({
|
expect(ShippingProfileServiceMock.create).toHaveBeenCalledWith({
|
||||||
name: "Test Profile",
|
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 { ShippingProfileService } from "../../../../services"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
import { EntityManager } from "typeorm"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /shipping-profiles
|
* @oas [post] /shipping-profiles
|
||||||
@@ -84,12 +85,26 @@ export default async (req, res) => {
|
|||||||
* type: object
|
* type: object
|
||||||
* required:
|
* required:
|
||||||
* - name
|
* - name
|
||||||
|
* - type
|
||||||
* properties:
|
* properties:
|
||||||
* name:
|
* name:
|
||||||
* description: "The name of the Shipping Profile"
|
* description: The name of the Shipping Profile
|
||||||
* type: string
|
* type: string
|
||||||
|
* type:
|
||||||
|
* description: The type of the Shipping Profile
|
||||||
|
* type: string
|
||||||
|
* enum: [default, gift_card, custom]
|
||||||
*/
|
*/
|
||||||
export class AdminPostShippingProfilesReq {
|
export class AdminPostShippingProfilesReq {
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string
|
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 { ShippingProfileService } from "../../../../services"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
import { EntityManager } from "typeorm"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /shipping-profiles/{id}
|
* @oas [post] /shipping-profiles/{id}
|
||||||
@@ -93,11 +100,44 @@ export default async (req, res) => {
|
|||||||
* type: object
|
* type: object
|
||||||
* properties:
|
* properties:
|
||||||
* name:
|
* name:
|
||||||
* description: "The name of the Shipping Profile"
|
* description: The name of the Shipping Profile
|
||||||
* type: string
|
* 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 {
|
export class AdminPostShippingProfilesProfileReq {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
name?: string
|
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,
|
In,
|
||||||
Repository,
|
Repository,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { PriceList, Product, SalesChannel, ProductCategory } from "../models"
|
import { PriceList, Product, ProductCategory, SalesChannel } from "../models"
|
||||||
import {
|
import {
|
||||||
ExtendedFindConfig,
|
ExtendedFindConfig,
|
||||||
Selector,
|
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(
|
private _cleanOptions(
|
||||||
options: FindWithoutRelationsOptions
|
options: FindWithoutRelationsOptions
|
||||||
): WithRequiredProperty<FindWithoutRelationsOptions, "where"> {
|
): WithRequiredProperty<FindWithoutRelationsOptions, "where"> {
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import { EntityRepository, Repository } from "typeorm"
|
import { EntityRepository, In, Repository } from "typeorm"
|
||||||
import { ShippingOption } from "../models/shipping-option"
|
import { ShippingOption } from "../models/shipping-option"
|
||||||
|
|
||||||
@EntityRepository(ShippingOption)
|
@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
|
deleteOption: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(Promise.resolve(products.productWithOptions)),
|
.mockReturnValue(Promise.resolve(products.productWithOptions)),
|
||||||
|
updateshippingProfiles: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
retrieveVariants: jest.fn().mockImplementation((productId) => {
|
retrieveVariants: jest.fn().mockImplementation((productId) => {
|
||||||
if (productId === IdMap.getId("product1")) {
|
if (productId === IdMap.getId("product1")) {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const ShippingOptionServiceMock = {
|
|||||||
return Promise.resolve(undefined)
|
return Promise.resolve(undefined)
|
||||||
}),
|
}),
|
||||||
update: jest.fn().mockReturnValue(Promise.resolve()),
|
update: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
updateShippingprofile: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
listAndCount: jest.fn().mockImplementation((data) => {
|
listAndCount: jest.fn().mockImplementation((data) => {
|
||||||
if (data.region_id === IdMap.getId("region-france")) {
|
if (data.region_id === IdMap.getId("region-france")) {
|
||||||
return Promise.resolve([[shippingOptions.franceShipping], 1])
|
return Promise.resolve([[shippingOptions.franceShipping], 1])
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ export const ShippingProfileServiceMock = {
|
|||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
update: jest.fn().mockImplementation(data => {
|
update: jest.fn().mockImplementation((data) => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
create: jest.fn().mockImplementation(data => {
|
create: jest.fn().mockImplementation((data) => {
|
||||||
return Promise.resolve(data)
|
return Promise.resolve(data)
|
||||||
}),
|
}),
|
||||||
createDefault: jest.fn().mockImplementation(() => {
|
createDefault: jest.fn().mockImplementation(() => {
|
||||||
@@ -31,7 +31,7 @@ export const ShippingProfileServiceMock = {
|
|||||||
createGiftCardDefault: jest.fn().mockImplementation(() => {
|
createGiftCardDefault: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
retrieve: jest.fn().mockImplementation(data => {
|
retrieve: jest.fn().mockImplementation((data) => {
|
||||||
if (data === IdMap.getId("default")) {
|
if (data === IdMap.getId("default")) {
|
||||||
return Promise.resolve(profiles.default)
|
return Promise.resolve(profiles.default)
|
||||||
}
|
}
|
||||||
@@ -40,13 +40,13 @@ export const ShippingProfileServiceMock = {
|
|||||||
}
|
}
|
||||||
return Promise.resolve(profiles.default)
|
return Promise.resolve(profiles.default)
|
||||||
}),
|
}),
|
||||||
retrieveGiftCardDefault: jest.fn().mockImplementation(data => {
|
retrieveGiftCardDefault: jest.fn().mockImplementation((data) => {
|
||||||
return Promise.resolve({ id: IdMap.getId("giftCardProfile") })
|
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") })
|
return Promise.resolve({ id: IdMap.getId("default_shipping_profile") })
|
||||||
}),
|
}),
|
||||||
list: jest.fn().mockImplementation(selector => {
|
list: jest.fn().mockImplementation((selector) => {
|
||||||
if (!selector) {
|
if (!selector) {
|
||||||
return Promise.resolve([])
|
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()),
|
addShippingOption: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
removeShippingOption: jest.fn().mockImplementation(() => Promise.resolve()),
|
removeShippingOption: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
addProduct: jest.fn().mockImplementation(() => Promise.resolve()),
|
addProduct: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ describe("ShippingProfileService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const productService = {
|
const productService = {
|
||||||
update: jest.fn(),
|
updateShippingProfile: jest.fn(),
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const shippingOptionService = {
|
const shippingOptionService = {
|
||||||
update: jest.fn(),
|
updateShippingProfile: jest.fn(),
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
@@ -75,10 +75,11 @@ describe("ShippingProfileService", () => {
|
|||||||
products: [IdMap.getId("product1")],
|
products: [IdMap.getId("product1")],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(productService.update).toBeCalledTimes(1)
|
expect(productService.updateShippingProfile).toBeCalledTimes(1)
|
||||||
expect(productService.update).toBeCalledWith(IdMap.getId("product1"), {
|
expect(productService.updateShippingProfile).toBeCalledWith(
|
||||||
profile_id: id,
|
[IdMap.getId("product1")],
|
||||||
})
|
id
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calls updateOne with shipping options", async () => {
|
it("calls updateOne with shipping options", async () => {
|
||||||
@@ -88,10 +89,10 @@ describe("ShippingProfileService", () => {
|
|||||||
shipping_options: [IdMap.getId("validId")],
|
shipping_options: [IdMap.getId("validId")],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(shippingOptionService.update).toBeCalledTimes(1)
|
expect(shippingOptionService.updateShippingProfile).toBeCalledTimes(1)
|
||||||
expect(shippingOptionService.update).toBeCalledWith(
|
expect(shippingOptionService.updateShippingProfile).toBeCalledWith(
|
||||||
IdMap.getId("validId"),
|
[IdMap.getId("validId")],
|
||||||
{ profile_id: id }
|
id
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -125,7 +126,7 @@ describe("ShippingProfileService", () => {
|
|||||||
const profRepo = MockRepository({ findOne: () => Promise.resolve({}) })
|
const profRepo = MockRepository({ findOne: () => Promise.resolve({}) })
|
||||||
|
|
||||||
const productService = {
|
const productService = {
|
||||||
update: jest.fn(),
|
updateShippingProfile: jest.fn(),
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
@@ -142,15 +143,15 @@ describe("ShippingProfileService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("add product to profile successfully", async () => {
|
it("add product to profile successfully", async () => {
|
||||||
await profileService.addProduct(
|
await profileService.addProduct(IdMap.getId("validId"), [
|
||||||
IdMap.getId("validId"),
|
IdMap.getId("product2"),
|
||||||
IdMap.getId("product2")
|
])
|
||||||
)
|
|
||||||
|
|
||||||
expect(productService.update).toBeCalledTimes(1)
|
expect(productService.updateShippingProfile).toBeCalledTimes(1)
|
||||||
expect(productService.update).toBeCalledWith(IdMap.getId("product2"), {
|
expect(productService.updateShippingProfile).toBeCalledWith(
|
||||||
profile_id: IdMap.getId("validId"),
|
[IdMap.getId("product2")],
|
||||||
})
|
IdMap.getId("validId")
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -300,7 +301,7 @@ describe("ShippingProfileService", () => {
|
|||||||
const profRepo = MockRepository({ findOne: () => Promise.resolve({}) })
|
const profRepo = MockRepository({ findOne: () => Promise.resolve({}) })
|
||||||
|
|
||||||
const shippingOptionService = {
|
const shippingOptionService = {
|
||||||
update: jest.fn(),
|
updateShippingProfile: jest.fn(),
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
@@ -317,15 +318,14 @@ describe("ShippingProfileService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("add shipping option to profile successfully", async () => {
|
it("add shipping option to profile successfully", async () => {
|
||||||
await profileService.addShippingOption(
|
await profileService.addShippingOption(IdMap.getId("validId"), [
|
||||||
IdMap.getId("validId"),
|
|
||||||
IdMap.getId("freeShipping")
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(shippingOptionService.update).toBeCalledTimes(1)
|
|
||||||
expect(shippingOptionService.update).toBeCalledWith(
|
|
||||||
IdMap.getId("freeShipping"),
|
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 { FlagRouter } from "../utils/flag-router"
|
||||||
|
|
||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||||
import { EntityManager, In } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { ProductVariantService, SearchService } from "."
|
import { ProductVariantService, SearchService } from "."
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
||||||
import {
|
import {
|
||||||
Product,
|
Product,
|
||||||
|
ProductCategory,
|
||||||
ProductOption,
|
ProductOption,
|
||||||
ProductTag,
|
ProductTag,
|
||||||
ProductType,
|
ProductType,
|
||||||
ProductVariant,
|
ProductVariant,
|
||||||
SalesChannel,
|
SalesChannel,
|
||||||
ProductCategory,
|
|
||||||
} from "../models"
|
} from "../models"
|
||||||
import { ImageRepository } from "../repositories/image"
|
import { ImageRepository } from "../repositories/image"
|
||||||
import {
|
import {
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
ProductSelector,
|
ProductSelector,
|
||||||
UpdateProductInput,
|
UpdateProductInput,
|
||||||
} from "../types/product"
|
} from "../types/product"
|
||||||
import { buildQuery, setMetadata } from "../utils"
|
import { buildQuery, isString, setMetadata } from "../utils"
|
||||||
import EventBusService from "./event-bus"
|
import EventBusService from "./event-bus"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
@@ -446,7 +446,9 @@ class ProductService extends TransactionBaseService {
|
|||||||
|
|
||||||
if (categories?.length) {
|
if (categories?.length) {
|
||||||
const categoryIds = categories.map((c) => c.id)
|
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
|
product.categories = categoryRecords
|
||||||
}
|
}
|
||||||
@@ -560,7 +562,9 @@ class ProductService extends TransactionBaseService {
|
|||||||
|
|
||||||
if (categories?.length) {
|
if (categories?.length) {
|
||||||
const categoryIds = categories.map((c) => c.id)
|
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
|
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.
|
* Creates a query object to be used for list queries.
|
||||||
* @param selector - the selector to create the query from
|
* @param selector - the selector to create the query from
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
UpdateShippingOptionInput,
|
UpdateShippingOptionInput,
|
||||||
ValidatePriceTypeAndAmountInput,
|
ValidatePriceTypeAndAmountInput,
|
||||||
} from "../types/shipping-options"
|
} from "../types/shipping-options"
|
||||||
import { buildQuery, setMetadata } from "../utils"
|
import { buildQuery, isString, setMetadata } from "../utils"
|
||||||
import { FlagRouter } from "../utils/flag-router"
|
import { FlagRouter } from "../utils/flag-router"
|
||||||
import FulfillmentProviderService from "./fulfillment-provider"
|
import FulfillmentProviderService from "./fulfillment-provider"
|
||||||
import RegionService from "./region"
|
import RegionService from "./region"
|
||||||
@@ -663,6 +663,10 @@ class ShippingOptionService extends TransactionBaseService {
|
|||||||
optionWithValidatedPrice.admin_only = update.admin_only
|
optionWithValidatedPrice.admin_only = update.admin_only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDefined(update.profile_id)) {
|
||||||
|
optionWithValidatedPrice.profile_id = update.profile_id
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.featureFlagRouter_.isFeatureEnabled(
|
this.featureFlagRouter_.isFeatureEnabled(
|
||||||
TaxInclusivePricingFeatureFlag.key
|
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
|
* 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
|
* fulfillment provider to calculate the price if the shipping option has the
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
CreateShippingProfile,
|
CreateShippingProfile,
|
||||||
UpdateShippingProfile,
|
UpdateShippingProfile,
|
||||||
} from "../types/shipping-profile"
|
} from "../types/shipping-profile"
|
||||||
import { buildQuery, setMetadata } from "../utils"
|
import { buildQuery, isString, setMetadata } from "../utils"
|
||||||
import CustomShippingOptionService from "./custom-shipping-option"
|
import CustomShippingOptionService from "./custom-shipping-option"
|
||||||
import ProductService from "./product"
|
import ProductService from "./product"
|
||||||
import ShippingOptionService from "./shipping-option"
|
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)
|
const result = await profileRepository.save(created)
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@@ -284,7 +291,7 @@ class ShippingProfileService extends TransactionBaseService {
|
|||||||
this.shippingProfileRepository_
|
this.shippingProfileRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const profile = await this.retrieve(profileId, {
|
let profile = await this.retrieve(profileId, {
|
||||||
relations: [
|
relations: [
|
||||||
"products",
|
"products",
|
||||||
"products.profile",
|
"products.profile",
|
||||||
@@ -295,27 +302,16 @@ class ShippingProfileService extends TransactionBaseService {
|
|||||||
|
|
||||||
const { metadata, products, shipping_options, ...rest } = update
|
const { metadata, products, shipping_options, ...rest } = update
|
||||||
|
|
||||||
if (metadata) {
|
|
||||||
profile.metadata = setMetadata(profile, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (products) {
|
if (products) {
|
||||||
const productServiceTx = this.productService_.withTransaction(manager)
|
profile = await this.addProduct(profile.id, products)
|
||||||
for (const pId of products) {
|
|
||||||
await productServiceTx.update(pId, {
|
|
||||||
profile_id: profile.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shipping_options) {
|
if (shipping_options) {
|
||||||
const shippingOptionServiceTx =
|
profile = await this.addShippingOption(profile.id, shipping_options)
|
||||||
this.shippingOptionService_.withTransaction(manager)
|
|
||||||
for (const oId of shipping_options) {
|
|
||||||
await shippingOptionServiceTx.update(oId, {
|
|
||||||
profile_id: profile.id,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
profile.metadata = setMetadata(profile, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(rest)) {
|
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
|
* Adds a product of an array of products to the profile.
|
||||||
* with the same product variant will have the same result.
|
* @param profileId - the profile to add the products to.
|
||||||
* @param profileId - the profile to add the product to.
|
* @param productId - the ID of the product or multiple products to add.
|
||||||
* @param productId - the product to add.
|
|
||||||
* @return the result of update
|
* @return the result of update
|
||||||
*/
|
*/
|
||||||
async addProduct(
|
async addProduct(
|
||||||
profileId: string,
|
profileId: string,
|
||||||
productId: string
|
productId: string | string[]
|
||||||
): Promise<ShippingProfile> {
|
): Promise<ShippingProfile> {
|
||||||
return await this.atomicPhase_(async (manager) => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
await this.productService_
|
const productServiceTx = this.productService_.withTransaction(manager)
|
||||||
.withTransaction(manager)
|
|
||||||
.update(productId, { profile_id: profileId })
|
|
||||||
|
|
||||||
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
|
* Adds a shipping option to the profile. The shipping option can be used to
|
||||||
* fulfill the products in the products field.
|
* fulfill the products in the products field.
|
||||||
* @param profileId - the profile to apply the shipping option to
|
* @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
|
* @return the result of the model update operation
|
||||||
*/
|
*/
|
||||||
async addShippingOption(
|
async addShippingOption(
|
||||||
profileId: string,
|
profileId: string,
|
||||||
optionId: string
|
optionId: string | string[]
|
||||||
): Promise<ShippingProfile> {
|
): Promise<ShippingProfile> {
|
||||||
return await this.atomicPhase_(async (manager) => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
await this.shippingOptionService_
|
const shippingOptionServiceTx =
|
||||||
.withTransaction(manager)
|
this.shippingOptionService_.withTransaction(manager)
|
||||||
.update(optionId, { profile_id: profileId })
|
|
||||||
|
|
||||||
const updated = await this.retrieve(profileId)
|
await shippingOptionServiceTx.updateShippingProfile(
|
||||||
return updated
|
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 = {
|
export type CreateShippingProfile = {
|
||||||
name: string
|
name: string
|
||||||
|
type: ShippingProfileType
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateShippingProfile = {
|
export type UpdateShippingProfile = {
|
||||||
|
|||||||
Reference in new issue
Block a user