feat(core-flows, dashboard, link-modules,medusa, types, utils): fulfillment shipping changes (#10902)

**What**
- product <> shipping profile link
- create and update product workflows/endpoints accepts shipping profile
- pass shipping option id when creating fulfillment to allow overriding customer selected SO
- validate shipping profile delete
- dashboard
  - set shipping profile on product create
  - manage shipping profile for a product
  - **update the create fulfillment form**
- other
  - fix create product form infinite rerenders
 
---

CLOSES CMRC-831 CMRC-834 CMRC-836 CMRC-837 CMRC-838 CMRC-857 TRI-761
This commit is contained in:
Frane Polić authored and GitHub committed 2025-01-27 12:00:20 +00:00
1 parent 3e81962503
commit 864d772e34
78 files changed
+3381 -646

No files matched your search

@@ -43,7 +43,8 @@ medusaIntegrationTestRunner({
salesChannel, salesChannel,
cart, cart,
customer, customer,
promotion promotion,
shippingProfile
beforeAll(async () => { beforeAll(async () => {
appContainer = getContainer() appContainer = getContainer()
@@ -68,6 +69,14 @@ medusaIntegrationTestRunner({
}, },
} }
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
await setupTaxStructure(appContainer.resolve(Modules.TAX)) await setupTaxStructure(appContainer.resolve(Modules.TAX))
region = ( region = (
@@ -87,7 +96,7 @@ medusaIntegrationTestRunner({
).data.region ).data.region
product = ( product = (
await api.post("/admin/products", medusaTshirtProduct, adminHeaders) await api.post("/admin/products", { ...medusaTshirtProduct, shipping_profile_id: shippingProfile.id }, adminHeaders)
).data.product ).data.product
salesChannel = ( salesChannel = (
@@ -34,6 +34,17 @@ medusaIntegrationTestRunner({
const container = getContainer() const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container) await createAdminUser(dbConnection, adminHeaders, container)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
const region = ( const region = (
await api.post( await api.post(
"/admin/regions", "/admin/regions",
@@ -72,6 +83,7 @@ medusaIntegrationTestRunner({
{ {
title: "Test product", title: "Test product",
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -96,6 +108,7 @@ medusaIntegrationTestRunner({
{ {
title: "Extra product", title: "Extra product",
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "my variant", title: "my variant",
@@ -213,17 +226,6 @@ medusaIntegrationTestRunner({
customer_id: customer.id, customer_id: customer.id,
}) })
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
location = ( location = (
await api.post( await api.post(
`/admin/stock-locations`, `/admin/stock-locations`,
@@ -16,6 +16,8 @@ medusaIntegrationTestRunner({
let baseProduct let baseProduct
let baseProduct1 let baseProduct1
let shippingProfile
beforeEach(async () => { beforeEach(async () => {
const container = getContainer() const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container) await createAdminUser(dbConnection, adminHeaders, container)
@@ -44,12 +46,21 @@ medusaIntegrationTestRunner({
) )
).data.collection ).data.collection
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
baseProduct = ( baseProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
{ {
title: "test-product", title: "test-product",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
}, },
adminHeaders adminHeaders
) )
@@ -61,6 +72,7 @@ medusaIntegrationTestRunner({
{ {
title: "test-product1", title: "test-product1",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
}, },
adminHeaders adminHeaders
) )
@@ -41,6 +41,17 @@ medusaIntegrationTestRunner({
) )
).data.region ).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
const customer = ( const customer = (
await api.post( await api.post(
"/admin/customers", "/admin/customers",
@@ -67,6 +78,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: "Test product", title: "Test product",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
variants: [ variants: [
{ {
@@ -91,6 +103,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: "Extra product", title: "Extra product",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
variants: [ variants: [
{ {
@@ -203,17 +216,6 @@ medusaIntegrationTestRunner({
customer_id: customer.id, customer_id: customer.id,
}) })
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
location = ( location = (
await api.post( await api.post(
`/admin/stock-locations`, `/admin/stock-locations`,
@@ -1,6 +1,7 @@
import { import {
AdminInventoryItem, AdminInventoryItem,
AdminProduct, AdminProduct,
AdminShippingProfile,
AdminStockLocation, AdminStockLocation,
MedusaContainer, MedusaContainer,
} from "@medusajs/types" } from "@medusajs/types"
@@ -18,6 +19,8 @@ export async function createOrderSeeder({
additionalProducts, additionalProducts,
stockChannelOverride, stockChannelOverride,
inventoryItemOverride, inventoryItemOverride,
shippingProfileOverride,
withoutShipping,
}: { }: {
api: any api: any
container: MedusaContainer container: MedusaContainer
@@ -26,6 +29,8 @@ export async function createOrderSeeder({
stockChannelOverride?: AdminStockLocation stockChannelOverride?: AdminStockLocation
additionalProducts?: { variant_id: string; quantity: number }[] additionalProducts?: { variant_id: string; quantity: number }[]
inventoryItemOverride?: AdminInventoryItem inventoryItemOverride?: AdminInventoryItem
shippingProfileOverride?: AdminShippingProfile
withoutShipping?: boolean
}) { }) {
const publishableKey = await generatePublishableKey(container) const publishableKey = await generatePublishableKey(container)
@@ -86,7 +91,9 @@ export async function createOrderSeeder({
adminHeaders adminHeaders
) )
const shippingProfile = ( const shippingProfile =
shippingProfileOverride ??
(
await api.post( await api.post(
`/admin/shipping-profiles`, `/admin/shipping-profiles`,
{ name: `test-${stockLocation.id}`, type: "default" }, { name: `test-${stockLocation.id}`, type: "default" },
@@ -101,6 +108,7 @@ export async function createOrderSeeder({
"/admin/products", "/admin/products",
{ {
title: `Test fixture ${shippingProfile.id}`, title: `Test fixture ${shippingProfile.id}`,
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -217,6 +225,14 @@ export async function createOrderSeeder({
) )
).data.cart ).data.cart
if (!withoutShipping) {
await api.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: shippingOption.id },
storeHeaders
)
}
const paymentCollection = ( const paymentCollection = (
await api.post( await api.post(
`/store/payment-collections`, `/store/payment-collections`,
@@ -13,9 +13,19 @@ medusaIntegrationTestRunner({
let stockLocation1 let stockLocation1
let stockLocation2 let stockLocation2
let stockLocation3 let stockLocation3
let shippingProfile
beforeEach(async () => { beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer()) await createAdminUser(dbConnection, adminHeaders, getContainer())
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
stockLocation1 = ( stockLocation1 = (
await api.post(`/admin/stock-locations`, { name: "loc1" }, adminHeaders) await api.post(`/admin/stock-locations`, { name: "loc1" }, adminHeaders)
).data.stock_location ).data.stock_location
@@ -990,6 +1000,7 @@ medusaIntegrationTestRunner({
{ {
title: "product 1", title: "product 1",
options: [{ title: "size", values: ["large"] }], options: [{ title: "size", values: ["large"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "variant 1", title: "variant 1",
@@ -85,12 +85,24 @@ medusaIntegrationTestRunner({
) )
).data.sales_channel ).data.sales_channel
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
const product = ( const product = (
await api.post( await api.post(
"/admin/products", "/admin/products",
{ {
title: "Test product", title: "Test product",
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -115,6 +127,7 @@ medusaIntegrationTestRunner({
{ {
title: "Extra product", title: "Extra product",
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "my variant", title: "my variant",
@@ -174,17 +187,6 @@ medusaIntegrationTestRunner({
customer_id: customer.id, customer_id: customer.id,
}) })
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
location = ( location = (
await api.post( await api.post(
`/admin/stock-locations`, `/admin/stock-locations`,
@@ -11,13 +11,21 @@ jest.setTimeout(300000)
medusaIntegrationTestRunner({ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
let order, seeder, inventoryItemOverride3, productOverride3 let order, seeder, inventoryItemOverride3, productOverride3, shippingProfile
beforeEach(async () => { beforeEach(async () => {
const container = getContainer() const container = getContainer()
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX)) await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
await createAdminUser(dbConnection, adminHeaders, container) await createAdminUser(dbConnection, adminHeaders, container)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
}) })
describe("POST /orders/:id", () => { describe("POST /orders/:id", () => {
@@ -345,9 +353,19 @@ medusaIntegrationTestRunner({
describe("POST /orders/:id/cancel", () => { describe("POST /orders/:id/cancel", () => {
beforeEach(async () => { beforeEach(async () => {
const inventoryItemOverride = (
await api.post(
`/admin/inventory-items`,
{ sku: "test-variant", requires_shipping: false },
adminHeaders
)
).data.inventory_item
seeder = await createOrderSeeder({ seeder = await createOrderSeeder({
api, api,
container: getContainer(), container: getContainer(),
inventoryItemOverride,
withoutShipping: true,
}) })
order = seeder.order order = seeder.order
@@ -536,8 +554,12 @@ medusaIntegrationTestRunner({
}) })
describe("POST /orders/:id/fulfillments", () => { describe("POST /orders/:id/fulfillments", () => {
let productOverride4WithOverrideShippingProfile,
shippingProfileOverride,
stockChannelOverride
beforeEach(async () => { beforeEach(async () => {
const stockChannelOverride = ( stockChannelOverride = (
await api.post( await api.post(
`/admin/stock-locations`, `/admin/stock-locations`,
{ name: "test location" }, { name: "test location" },
@@ -558,6 +580,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: `Test fixture`, title: `Test fixture`,
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -605,6 +628,14 @@ medusaIntegrationTestRunner({
) )
).data.inventory_item ).data.inventory_item
const inventoryItemOverride4RequiresShipping = (
await api.post(
`/admin/inventory-items`,
{ sku: "test-variant-4", requires_shipping: true },
adminHeaders
)
).data.inventory_item
await api.post( await api.post(
`/admin/inventory-items/${inventoryItemOverride2.id}/location-levels`, `/admin/inventory-items/${inventoryItemOverride2.id}/location-levels`,
{ {
@@ -623,11 +654,21 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
await api.post(
`/admin/inventory-items/${inventoryItemOverride4RequiresShipping.id}/location-levels`,
{
location_id: stockChannelOverride.id,
stocked_quantity: 10,
},
adminHeaders
)
const productOverride2 = ( const productOverride2 = (
await api.post( await api.post(
"/admin/products", "/admin/products",
{ {
title: `Test fixture 2`, title: `Test fixture 2`,
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -664,6 +705,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: `Test fixture 3`, title: `Test fixture 3`,
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -695,6 +737,52 @@ medusaIntegrationTestRunner({
) )
).data.product ).data.product
shippingProfileOverride = (
await api.post(
`/admin/shipping-profiles`,
{ name: `test-${stockChannelOverride.id}`, type: "default" },
adminHeaders
)
).data.shipping_profile
productOverride4WithOverrideShippingProfile = (
await api.post(
"/admin/products",
{
title: `Test fixture 4`,
shipping_profile_id: shippingProfileOverride.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
],
variants: [
{
title: "Test variant 4",
sku: "test-variant-4",
inventory_items: [
{
inventory_item_id:
inventoryItemOverride4RequiresShipping.id,
required_quantity: 1,
},
],
prices: [
{
currency_code: "usd",
amount: 100,
},
],
options: {
size: "small",
color: "green",
},
},
],
},
adminHeaders
)
).data.product
seeder = await createOrderSeeder({ seeder = await createOrderSeeder({
api, api,
container: getContainer(), container: getContainer(),
@@ -702,9 +790,15 @@ medusaIntegrationTestRunner({
additionalProducts: [ additionalProducts: [
{ variant_id: productOverride2.variants[0].id, quantity: 1 }, { variant_id: productOverride2.variants[0].id, quantity: 1 },
{ variant_id: productOverride3.variants[0].id, quantity: 3 }, { variant_id: productOverride3.variants[0].id, quantity: 3 },
{
variant_id:
productOverride4WithOverrideShippingProfile.variants[0].id,
quantity: 1,
},
], ],
stockChannelOverride, stockChannelOverride,
inventoryItemOverride, inventoryItemOverride,
shippingProfileOverride: shippingProfile,
}) })
order = seeder.order order = seeder.order
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
@@ -827,6 +921,30 @@ medusaIntegrationTestRunner({
) )
}) })
it("should throw if shipping profile of the product doesn't match the shipping profile of the shipping option", async () => {
const orderItemId = order.items.find(
(i) =>
i.variant_id ===
productOverride4WithOverrideShippingProfile.variants[0].id
).id
const res = await api
.post(
`/admin/orders/${order.id}/fulfillments`,
{
location_id: stockChannelOverride.id,
items: [{ id: orderItemId, quantity: 1 }],
},
adminHeaders
)
.catch((e) => e)
expect(res.response.status).toBe(400)
expect(res.response.data.message).toBe(
`Shipping profile ${seeder.shippingProfile.id} does not match the shipping profile of the order item ${orderItemId}`
)
})
it("should only create fulfillments grouped by shipping requirement", async () => { it("should only create fulfillments grouped by shipping requirement", async () => {
const { const {
response: { data }, response: { data },
@@ -24,7 +24,21 @@ medusaIntegrationTestRunner({
await setupTaxStructure(container.resolve(Modules.TAX)) await setupTaxStructure(container.resolve(Modules.TAX))
await createAdminUser(dbConnection, adminHeaders, container) await createAdminUser(dbConnection, adminHeaders, container)
const seeders = await createOrderSeeder({ api, container })
const inventoryItemOverride = (
await api.post(
`/admin/inventory-items`,
{ sku: "test-variant", requires_shipping: false },
adminHeaders
)
).data.inventory_item
const seeders = await createOrderSeeder({
api,
container,
inventoryItemOverride,
withoutShipping: true,
})
order = seeders.order order = seeders.order
shippingProfile = ( shippingProfile = (
@@ -24,6 +24,7 @@ medusaIntegrationTestRunner({
let region let region
let product let product
let cart let cart
let shippingProfile
beforeEach(async () => { beforeEach(async () => {
region = ( region = (
@@ -34,12 +35,21 @@ medusaIntegrationTestRunner({
) )
).data.region ).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product = ( product = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ getProductFixture({
title: "test", title: "test",
status: "published", status: "published",
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -38,7 +38,22 @@ medusaIntegrationTestRunner({
beforeEach(async () => { beforeEach(async () => {
container = getContainer() container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container) await createAdminUser(dbConnection, adminHeaders, container)
const seeders = await createOrderSeeder({ api, container })
const inventoryItemOverride = (
await api.post(
`/admin/inventory-items`,
{ sku: "test-variant", requires_shipping: false },
adminHeaders
)
).data.inventory_item
const seeders = await createOrderSeeder({
api,
container,
inventoryItemOverride,
withoutShipping: true,
})
order = seeders.order order = seeders.order
await api.post( await api.post(
@@ -18,6 +18,7 @@ medusaIntegrationTestRunner({
let region1 let region1
let product1 let product1
let customerGroup1 let customerGroup1
let shippingProfile
beforeEach(async () => { beforeEach(async () => {
const container = getContainer() const container = getContainer()
@@ -34,10 +35,21 @@ medusaIntegrationTestRunner({
) )
).data.region ).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product1 = ( product1 = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ title: "Test product" }), getProductFixture({
title: "Test product",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders adminHeaders
) )
).data.product ).data.product
@@ -18,9 +18,19 @@ medusaIntegrationTestRunner({
let productCategoryChild2 let productCategoryChild2
let productCategoryChild3 let productCategoryChild3
let shippingProfile
beforeEach(async () => { beforeEach(async () => {
const appContainer = getContainer() const appContainer = getContainer()
await createAdminUser(dbConnection, adminHeaders, appContainer) await createAdminUser(dbConnection, adminHeaders, appContainer)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
}) })
describe("GET /admin/product-categories/:id", () => { describe("GET /admin/product-categories/:id", () => {
@@ -1349,6 +1359,7 @@ medusaIntegrationTestRunner({
title: "product 1", title: "product 1",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
categories: [{ id: productCategory.id }], categories: [{ id: productCategory.id }],
shipping_profile_id: shippingProfile.id,
}, },
adminHeaders adminHeaders
) )
@@ -1358,6 +1369,7 @@ medusaIntegrationTestRunner({
{ {
title: "product 2", title: "product 2",
options: [{ title: "color", values: ["r", "g"] }], options: [{ title: "color", values: ["r", "g"] }],
shipping_profile_id: shippingProfile.id,
}, },
adminHeaders adminHeaders
) )
@@ -1,6 +1,6 @@
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
prod_01J44RRJZ3M5F63NY82434RNM5,base-product,Base product,draft,"test-product-description prod_01J44RRJZ3M5F63NY82434RNM5,base-product,Base product,draft,"test-product-description
test line 2",,,test-image.png,pcol_01J44RRJXM6AM3YS5PMJDMH3YF,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.102Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.102Z,,,variant_01J44RRJZW1T9KQB6XG7Q6K61F,Test variant,,,,,,true,false,,2024-07-31T16:07:55.133Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRJZ3M5F63NY82434RNM5,2024-07-31T16:07:55.133Z,0,, test line 2",,,test-image.png,pcol_01J44RRJXM6AM3YS5PMJDMH3YF,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.102Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.102Z,,,variant_01J44RRJZW1T9KQB6XG7Q6K61F,Test variant,,,,,,true,false,,2024-07-31T16:07:55.133Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRJZ3M5F63NY82434RNM5,2024-07-31T16:07:55.133Z,0,,,import-shipping-profile
prod_01J44RRJZ3M5F63NY82434RNM5,base-product,Base product,draft,"test-product-description prod_01J44RRJZ3M5F63NY82434RNM5,base-product,Base product,draft,"test-product-description
test line 2",,,test-image.png,pcol_01J44RRJXM6AM3YS5PMJDMH3YF,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.102Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.102Z,,,variant_01J44RRJZW5GNQKT1FEDACEESW,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.133Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRJZ3M5F63NY82434RNM5,2024-07-31T16:07:55.133Z,0,, test line 2",,,test-image.png,pcol_01J44RRJXM6AM3YS5PMJDMH3YF,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.102Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.102Z,,,variant_01J44RRJZW5GNQKT1FEDACEESW,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.133Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRJZ3M5F63NY82434RNM5,2024-07-31T16:07:55.133Z,0,,,import-shipping-profile
prod_01J44RRK2GJJVMQQXT67TJCV08,proposed-product,Proposed product,proposed,test-product-description,,,test-image.png,,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.213Z,,true,,,test-image.png,test-image-2.png,false,,,,,new-tag,,2024-07-31T16:07:55.213Z,,,variant_01J44RRK2WYHH0RDEK8BBGP7CY,Test variant,,,,,,true,false,,2024-07-31T16:07:55.228Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRK2GJJVMQQXT67TJCV08,2024-07-31T16:07:55.228Z,0,, prod_01J44RRK2GJJVMQQXT67TJCV08,proposed-product,Proposed product,proposed,test-product-description,,,test-image.png,,ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD,2024-07-31T16:07:55.213Z,,true,,,test-image.png,test-image-2.png,false,,,,,new-tag,,2024-07-31T16:07:55.213Z,,,variant_01J44RRK2WYHH0RDEK8BBGP7CY,Test variant,,,,,,true,false,,2024-07-31T16:07:55.228Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRK2GJJVMQQXT67TJCV08,2024-07-31T16:07:55.228Z,0,,,import-shipping-profile
1 Product Id Product Handle Product Title Product Status Product Description Product Subtitle Product External Id Product Thumbnail Product Collection Id Product Type Id Product Created At Product Deleted At Product Discountable Product Height Product Hs Code Product Image 1 Product Image 2 Product Is Giftcard Product Length Product Material Product Mid Code Product Origin Country Product Tag 1 Product Tag 2 Product Updated At Product Weight Product Width Variant Id Variant Title Variant Sku Variant Upc Variant Ean Variant Hs Code Variant Mid Code Variant Manage Inventory Variant Allow Backorder Variant Barcode Variant Created At Variant Deleted At Variant Height Variant Length Variant Material Variant Metadata Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value Variant Origin Country Variant Price DKK Variant Price EUR Variant Price USD Variant Product Id Variant Updated At Variant Variant Rank Variant Weight Variant Width Shipping Profile Id
2 prod_01J44RRJZ3M5F63NY82434RNM5 base-product Base product draft test-product-description test line 2 test-image.png pcol_01J44RRJXM6AM3YS5PMJDMH3YF ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD 2024-07-31T16:07:55.102Z true test-image.png test-image-2.png false 123 456 2024-07-31T16:07:55.102Z variant_01J44RRJZW1T9KQB6XG7Q6K61F Test variant true false 2024-07-31T16:07:55.133Z size large color green 30 45 100 prod_01J44RRJZ3M5F63NY82434RNM5 2024-07-31T16:07:55.133Z 0 import-shipping-profile
3 prod_01J44RRJZ3M5F63NY82434RNM5 base-product Base product draft test-product-description test line 2 test-image.png pcol_01J44RRJXM6AM3YS5PMJDMH3YF ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD 2024-07-31T16:07:55.102Z true test-image.png test-image-2.png false 123 456 2024-07-31T16:07:55.102Z variant_01J44RRJZW5GNQKT1FEDACEESW Test variant 2 true false 2024-07-31T16:07:55.133Z size small color green 50 65 200 prod_01J44RRJZ3M5F63NY82434RNM5 2024-07-31T16:07:55.133Z 0 import-shipping-profile
4 prod_01J44RRK2GJJVMQQXT67TJCV08 proposed-product Proposed product proposed test-product-description test-image.png ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD 2024-07-31T16:07:55.213Z true test-image.png test-image-2.png false new-tag 2024-07-31T16:07:55.213Z variant_01J44RRK2WYHH0RDEK8BBGP7CY Test variant true false 2024-07-31T16:07:55.228Z size large color green 30 45 100 prod_01J44RRK2GJJVMQQXT67TJCV08 2024-07-31T16:07:55.228Z 0 import-shipping-profile
5
6
@@ -1,6 +1,6 @@
Product Id;Product Handle;Product Title;Product Status;Product Description;Product Subtitle;Product External Id;Product Thumbnail;Product Collection Id;Product Type Id;Product Created At;Product Deleted At;Product Discountable;Product Height;Product Hs Code;Product Image 1;Product Image 2;Product Is Giftcard;Product Length;Product Material;Product Mid Code;Product Origin Country;Product Tag 1;Product Tag 2;Product Updated At;Product Weight;Product Width;Variant Id;Variant Title;Variant Sku;Variant Upc;Variant Ean;Variant Hs Code;Variant Mid Code;Variant Manage Inventory;Variant Allow Backorder;Variant Barcode;Variant Created At;Variant Deleted At;Variant Height;Variant Length;Variant Material;Variant Metadata;Variant Option 1 Name;Variant Option 1 Value;Variant Option 2 Name;Variant Option 2 Value;Variant Origin Country;Variant Price DKK;Variant Price EUR;Variant Price USD;Variant Product Id;Variant Updated At;Variant Variant Rank;Variant Weight;Variant Width Product Id;Product Handle;Product Title;Product Status;Product Description;Product Subtitle;Product External Id;Product Thumbnail;Product Collection Id;Product Type Id;Product Created At;Product Deleted At;Product Discountable;Product Height;Product Hs Code;Product Image 1;Product Image 2;Product Is Giftcard;Product Length;Product Material;Product Mid Code;Product Origin Country;Product Tag 1;Product Tag 2;Product Updated At;Product Weight;Product Width;Variant Id;Variant Title;Variant Sku;Variant Upc;Variant Ean;Variant Hs Code;Variant Mid Code;Variant Manage Inventory;Variant Allow Backorder;Variant Barcode;Variant Created At;Variant Deleted At;Variant Height;Variant Length;Variant Material;Variant Metadata;Variant Option 1 Name;Variant Option 1 Value;Variant Option 2 Name;Variant Option 2 Value;Variant Origin Country;Variant Price DKK;Variant Price EUR;Variant Price USD;Variant Product Id;Variant Updated At;Variant Variant Rank;Variant Weight;Variant Width;Shipping Profile Id
prod_01J44RRJZ3M5F63NY82434RNM5;base-product;Base product;draft;"test-product-description prod_01J44RRJZ3M5F63NY82434RNM5;base-product;Base product;draft;"test-product-description
test line 2";;;test-image.png;pcol_01J44RRJXM6AM3YS5PMJDMH3YF;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.102Z;;true;;;test-image.png;test-image-2.png;false;;;;;123;456;2024-07-31T16:07:55.102Z;;;variant_01J44RRJZW1T9KQB6XG7Q6K61F;Test variant;;;;;;true;false;;2024-07-31T16:07:55.133Z;;;;;;size;large;color;green;;30;45;100;prod_01J44RRJZ3M5F63NY82434RNM5;2024-07-31T16:07:55.133Z;0;; test line 2";;;test-image.png;pcol_01J44RRJXM6AM3YS5PMJDMH3YF;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.102Z;;true;;;test-image.png;test-image-2.png;false;;;;;123;456;2024-07-31T16:07:55.102Z;;;variant_01J44RRJZW1T9KQB6XG7Q6K61F;Test variant;;;;;;true;false;;2024-07-31T16:07:55.133Z;;;;;;size;large;color;green;;30;45;100;prod_01J44RRJZ3M5F63NY82434RNM5;2024-07-31T16:07:55.133Z;0;;;import-shipping-profile
prod_01J44RRJZ3M5F63NY82434RNM5;base-product;Base product;draft;"test-product-description prod_01J44RRJZ3M5F63NY82434RNM5;base-product;Base product;draft;"test-product-description
test line 2";;;test-image.png;pcol_01J44RRJXM6AM3YS5PMJDMH3YF;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.102Z;;true;;;test-image.png;test-image-2.png;false;;;;;123;456;2024-07-31T16:07:55.102Z;;;variant_01J44RRJZW5GNQKT1FEDACEESW;Test variant 2;;;;;;true;false;;2024-07-31T16:07:55.133Z;;;;;;size;small;color;green;;50;65;200;prod_01J44RRJZ3M5F63NY82434RNM5;2024-07-31T16:07:55.133Z;0;; test line 2";;;test-image.png;pcol_01J44RRJXM6AM3YS5PMJDMH3YF;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.102Z;;true;;;test-image.png;test-image-2.png;false;;;;;123;456;2024-07-31T16:07:55.102Z;;;variant_01J44RRJZW5GNQKT1FEDACEESW;Test variant 2;;;;;;true;false;;2024-07-31T16:07:55.133Z;;;;;;size;small;color;green;;50;65;200;prod_01J44RRJZ3M5F63NY82434RNM5;2024-07-31T16:07:55.133Z;0;;;import-shipping-profile
prod_01J44RRK2GJJVMQQXT67TJCV08;proposed-product;Proposed product;proposed;test-product-description;;;test-image.png;;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.213Z;;true;;;test-image.png;test-image-2.png;false;;;;;new-tag;;2024-07-31T16:07:55.213Z;;;variant_01J44RRK2WYHH0RDEK8BBGP7CY;Test variant;;;;;;true;false;;2024-07-31T16:07:55.228Z;;;;;;size;large;color;green;;30;45;100;prod_01J44RRK2GJJVMQQXT67TJCV08;2024-07-31T16:07:55.228Z;0;; prod_01J44RRK2GJJVMQQXT67TJCV08;proposed-product;Proposed product;proposed;test-product-description;;;test-image.png;;ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD;2024-07-31T16:07:55.213Z;;true;;;test-image.png;test-image-2.png;false;;;;;new-tag;;2024-07-31T16:07:55.213Z;;;variant_01J44RRK2WYHH0RDEK8BBGP7CY;Test variant;;;;;;true;false;;2024-07-31T16:07:55.228Z;;;;;;size;large;color;green;;30;45;100;prod_01J44RRK2GJJVMQQXT67TJCV08;2024-07-31T16:07:55.228Z;0;;;import-shipping-profile
1 Product Id Product Handle Product Title Product Status Product Description Product Subtitle Product External Id Product Thumbnail Product Collection Id Product Type Id Product Created At Product Deleted At Product Discountable Product Height Product Hs Code Product Image 1 Product Image 2 Product Is Giftcard Product Length Product Material Product Mid Code Product Origin Country Product Tag 1 Product Tag 2 Product Updated At Product Weight Product Width Variant Id Variant Title Variant Sku Variant Upc Variant Ean Variant Hs Code Variant Mid Code Variant Manage Inventory Variant Allow Backorder Variant Barcode Variant Created At Variant Deleted At Variant Height Variant Length Variant Material Variant Metadata Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value Variant Origin Country Variant Price DKK Variant Price EUR Variant Price USD Variant Product Id Variant Updated At Variant Variant Rank Variant Weight Variant Width Shipping Profile Id
2 prod_01J44RRJZ3M5F63NY82434RNM5 base-product Base product draft test-product-description test line 2 test-image.png pcol_01J44RRJXM6AM3YS5PMJDMH3YF ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD 2024-07-31T16:07:55.102Z true test-image.png test-image-2.png false 123 456 2024-07-31T16:07:55.102Z variant_01J44RRJZW1T9KQB6XG7Q6K61F Test variant true false 2024-07-31T16:07:55.133Z size large color green 30 45 100 prod_01J44RRJZ3M5F63NY82434RNM5 2024-07-31T16:07:55.133Z 0 import-shipping-profile
3 prod_01J44RRJZ3M5F63NY82434RNM5 base-product Base product draft test-product-description test line 2 test-image.png pcol_01J44RRJXM6AM3YS5PMJDMH3YF ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD 2024-07-31T16:07:55.102Z true test-image.png test-image-2.png false 123 456 2024-07-31T16:07:55.102Z variant_01J44RRJZW5GNQKT1FEDACEESW Test variant 2 true false 2024-07-31T16:07:55.133Z size small color green 50 65 200 prod_01J44RRJZ3M5F63NY82434RNM5 2024-07-31T16:07:55.133Z 0 import-shipping-profile
4 prod_01J44RRK2GJJVMQQXT67TJCV08 proposed-product Proposed product proposed test-product-description test-image.png ptyp_01J44RRJYAFEBZ2EY1KE1JM3XD 2024-07-31T16:07:55.213Z true test-image.png test-image-2.png false new-tag 2024-07-31T16:07:55.213Z variant_01J44RRK2WYHH0RDEK8BBGP7CY Test variant true false 2024-07-31T16:07:55.228Z size large color green 30 45 100 prod_01J44RRK2GJJVMQQXT67TJCV08 2024-07-31T16:07:55.228Z 0 import-shipping-profile
5
6
@@ -1,2 +1,2 @@
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
prod_01J44RRMJ7H2K9JAD9AQHA724B,proposed-product,Proposed product,proposed,test-product-description,,,test-image.png,,ptyp_01J44RRMF4J9SG6F7FG3B16A7F,2024-07-31T16:07:56.741Z,,true,,,test-image.png,test-image-2.png,false,,,,,new-tag,2024-07-31T16:07:56.741Z,,,variant_01J44RRMJJT9HSEGFJHTY2SX2P,Test variant,,,,,,true,false,,2024-07-31T16:07:56.754Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRMJ7H2K9JAD9AQHA724B,2024-07-31T16:07:56.754Z,0,, prod_01J44RRMJ7H2K9JAD9AQHA724B,proposed-product,Proposed product,proposed,test-product-description,,,test-image.png,,ptyp_01J44RRMF4J9SG6F7FG3B16A7F,2024-07-31T16:07:56.741Z,,true,,,test-image.png,test-image-2.png,false,,,,,new-tag,2024-07-31T16:07:56.741Z,,,variant_01J44RRMJJT9HSEGFJHTY2SX2P,Test variant,,,,,,true,false,,2024-07-31T16:07:56.754Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRMJ7H2K9JAD9AQHA724B,2024-07-31T16:07:56.754Z,0,,,import-shipping-profile
1 Product Id Product Handle Product Title Product Status Product Description Product Subtitle Product External Id Product Thumbnail Product Collection Id Product Type Id Product Created At Product Deleted At Product Discountable Product Height Product Hs Code Product Image 1 Product Image 2 Product Is Giftcard Product Length Product Material Product Mid Code Product Origin Country Product Tag 1 Product Updated At Product Weight Product Width Variant Id Variant Title Variant Sku Variant Upc Variant Ean Variant Hs Code Variant Mid Code Variant Manage Inventory Variant Allow Backorder Variant Barcode Variant Created At Variant Deleted At Variant Height Variant Length Variant Material Variant Metadata Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value Variant Origin Country Variant Price DKK Variant Price EUR Variant Price USD Variant Product Id Variant Updated At Variant Variant Rank Variant Weight Variant Width Shipping Profile Id
2 prod_01J44RRMJ7H2K9JAD9AQHA724B proposed-product Proposed product proposed test-product-description test-image.png ptyp_01J44RRMF4J9SG6F7FG3B16A7F 2024-07-31T16:07:56.741Z true test-image.png test-image-2.png false new-tag 2024-07-31T16:07:56.741Z variant_01J44RRMJJT9HSEGFJHTY2SX2P Test variant true false 2024-07-31T16:07:56.754Z size large color green 30 45 100 prod_01J44RRMJ7H2K9JAD9AQHA724B 2024-07-31T16:07:56.754Z 0 import-shipping-profile
@@ -1,2 +1,2 @@
Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price EUR,Variant Price DKK,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Product field Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price EUR,Variant Price DKK,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Product field,Shipping Profile Id
prod_01J3CSN791SN1RN7X155Z8S9CN,Proposed product,,proposed,,test-product-description,proposed-product,false,true,test-image.png,,ptyp_01J3CSN76GCRSCDV9V489B5FWQ,,,,,,,,,2024-07-22T08:41:47.040Z,2024-07-22T08:41:47.040Z,,test-image.png,test-image-2.png,new-tag,variant_01J3CSN79CQ2ND94SRJSXMEMNH,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CSN791SN1RN7X155Z8S9CN,2024-07-22T08:41:47.053Z,2024-07-22T08:41:47.053Z,,100,45,30,size,large,color,green,someval prod_01J3CSN791SN1RN7X155Z8S9CN,Proposed product,,proposed,,test-product-description,proposed-product,false,true,test-image.png,,ptyp_01J3CSN76GCRSCDV9V489B5FWQ,,,,,,,,,2024-07-22T08:41:47.040Z,2024-07-22T08:41:47.040Z,,test-image.png,test-image-2.png,new-tag,variant_01J3CSN79CQ2ND94SRJSXMEMNH,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CSN791SN1RN7X155Z8S9CN,2024-07-22T08:41:47.053Z,2024-07-22T08:41:47.053Z,,100,45,30,size,large,color,green,someval,import-shipping-profile
1 Product Id Product Title Product Subtitle Product Status Product External Id Product Description Product Handle Product Is Giftcard Product Discountable Product Thumbnail Product Collection Id Product Type Id Product Weight Product Length Product Height Product Width Product Hs Code Product Origin Country Product Mid Code Product Material Product Created At Product Updated At Product Deleted At Product Image 1 Product Image 2 Product Tag 1 Variant Id Variant Title Variant Sku Variant Barcode Variant Ean Variant Upc Variant Allow Backorder Variant Manage Inventory Variant Hs Code Variant Origin Country Variant Mid Code Variant Material Variant Weight Variant Length Variant Height Variant Width Variant Metadata Variant Variant Rank Variant Product Id Variant Created At Variant Updated At Variant Deleted At Variant Price USD Variant Price EUR Variant Price DKK Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value Product field Shipping Profile Id
2 prod_01J3CSN791SN1RN7X155Z8S9CN Proposed product proposed test-product-description proposed-product false true test-image.png ptyp_01J3CSN76GCRSCDV9V489B5FWQ 2024-07-22T08:41:47.040Z 2024-07-22T08:41:47.040Z test-image.png test-image-2.png new-tag variant_01J3CSN79CQ2ND94SRJSXMEMNH Test variant false true 0 prod_01J3CSN791SN1RN7X155Z8S9CN 2024-07-22T08:41:47.053Z 2024-07-22T08:41:47.053Z 100 45 30 size large color green someval import-shipping-profile
@@ -1,2 +1,2 @@
Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price EUR,Variant Price nonexistent [EUR],Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price EUR,Variant Price nonexistent [EUR],Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Shipping Profile Id
prod_01J3CSN791SN1RN7X155Z8S9CN,Proposed product,,proposed,,test-product-description,proposed-product,false,true,test-image.png,,ptyp_01J3CSN76GCRSCDV9V489B5FWQ,,,,,,,,,2024-07-22T08:41:47.040Z,2024-07-22T08:41:47.040Z,,test-image.png,test-image-2.png,new-tag,variant_01J3CSN79CQ2ND94SRJSXMEMNH,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CSN791SN1RN7X155Z8S9CN,2024-07-22T08:41:47.053Z,2024-07-22T08:41:47.053Z,,100,45,30,size,large,color,green prod_01J3CSN791SN1RN7X155Z8S9CN,Proposed product,,proposed,,test-product-description,proposed-product,false,true,test-image.png,,ptyp_01J3CSN76GCRSCDV9V489B5FWQ,,,,,,,,,2024-07-22T08:41:47.040Z,2024-07-22T08:41:47.040Z,,test-image.png,test-image-2.png,new-tag,variant_01J3CSN79CQ2ND94SRJSXMEMNH,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CSN791SN1RN7X155Z8S9CN,2024-07-22T08:41:47.053Z,2024-07-22T08:41:47.053Z,,100,45,30,size,large,color,green,import-shipping-profile
1 Product Id Product Title Product Subtitle Product Status Product External Id Product Description Product Handle Product Is Giftcard Product Discountable Product Thumbnail Product Collection Id Product Type Id Product Weight Product Length Product Height Product Width Product Hs Code Product Origin Country Product Mid Code Product Material Product Created At Product Updated At Product Deleted At Product Image 1 Product Image 2 Product Tag 1 Variant Id Variant Title Variant Sku Variant Barcode Variant Ean Variant Upc Variant Allow Backorder Variant Manage Inventory Variant Hs Code Variant Origin Country Variant Mid Code Variant Material Variant Weight Variant Length Variant Height Variant Width Variant Metadata Variant Variant Rank Variant Product Id Variant Created At Variant Updated At Variant Deleted At Variant Price USD Variant Price EUR Variant Price nonexistent [EUR] Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value Shipping Profile Id
2 prod_01J3CSN791SN1RN7X155Z8S9CN Proposed product proposed test-product-description proposed-product false true test-image.png ptyp_01J3CSN76GCRSCDV9V489B5FWQ 2024-07-22T08:41:47.040Z 2024-07-22T08:41:47.040Z test-image.png test-image-2.png new-tag variant_01J3CSN79CQ2ND94SRJSXMEMNH Test variant false true 0 prod_01J3CSN791SN1RN7X155Z8S9CN 2024-07-22T08:41:47.053Z 2024-07-22T08:41:47.053Z 100 45 30 size large color green import-shipping-profile
@@ -1,2 +1,2 @@
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price Test Region [USD],Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price Test Region [USD],Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
prod_01J44RRM579P1EY2ZNQVGN1THT,product-with-prices,Product with prices,draft,test-product-description,,,test-image.png,,,2024-07-31T16:07:56.325Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:56.325Z,,,variant_01J44RRM5J1569XJD39DM3PNFN,Test variant,,,,,,true,false,,2024-07-31T16:07:56.338Z,,,,,,size,large,color,green,,45,100,prod_01J44RRM579P1EY2ZNQVGN1THT,2024-07-31T16:07:56.338Z,0,, prod_01J44RRM579P1EY2ZNQVGN1THT,product-with-prices,Product with prices,draft,test-product-description,,,test-image.png,,,2024-07-31T16:07:56.325Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:56.325Z,,,variant_01J44RRM5J1569XJD39DM3PNFN,Test variant,,,,,,true,false,,2024-07-31T16:07:56.338Z,,,,,,size,large,color,green,,45,100,prod_01J44RRM579P1EY2ZNQVGN1THT,2024-07-31T16:07:56.338Z,0,,,import-shipping-profile
1 Product Id Product Handle Product Title Product Status Product Description Product Subtitle Product External Id Product Thumbnail Product Collection Id Product Type Id Product Created At Product Deleted At Product Discountable Product Height Product Hs Code Product Image 1 Product Image 2 Product Is Giftcard Product Length Product Material Product Mid Code Product Origin Country Product Tag 1 Product Tag 2 Product Updated At Product Weight Product Width Variant Id Variant Title Variant Sku Variant Upc Variant Ean Variant Hs Code Variant Mid Code Variant Manage Inventory Variant Allow Backorder Variant Barcode Variant Created At Variant Deleted At Variant Height Variant Length Variant Material Variant Metadata Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value Variant Origin Country Variant Price Test Region [USD] Variant Price USD Variant Product Id Variant Updated At Variant Variant Rank Variant Weight Variant Width Shipping Profile Id
2 prod_01J44RRM579P1EY2ZNQVGN1THT product-with-prices Product with prices draft test-product-description test-image.png 2024-07-31T16:07:56.325Z true test-image.png test-image-2.png false 123 456 2024-07-31T16:07:56.325Z variant_01J44RRM5J1569XJD39DM3PNFN Test variant true false 2024-07-31T16:07:56.338Z size large color green 45 100 prod_01J44RRM579P1EY2ZNQVGN1THT 2024-07-31T16:07:56.338Z 0 import-shipping-profile
@@ -1,5 +1,5 @@
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Category 1,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Category 1,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRQ7WJ902X4936GEK7,Test variant,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,, test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRQ7WJ902X4936GEK7,Test variant,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRAYY7Q7NTXNNMDA1S,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,, test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRAYY7Q7NTXNNMDA1S,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
1 Product Id Product Handle Product Title Product Status Product Description Product Subtitle Product External Id Product Thumbnail Product Collection Id Product Type Id Product Category 1 Product Created At Product Deleted At Product Discountable Product Height Product Hs Code Product Image 1 Product Image 2 Product Is Giftcard Product Length Product Material Product Mid Code Product Origin Country Product Tag 1 Product Tag 2 Product Updated At Product Weight Product Width Variant Id Variant Title Variant Sku Variant Upc Variant Ean Variant Hs Code Variant Mid Code Variant Manage Inventory Variant Allow Backorder Variant Barcode Variant Created At Variant Deleted At Variant Height Variant Length Variant Material Variant Metadata Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value Variant Origin Country Variant Price DKK Variant Price EUR Variant Price USD Variant Product Id Variant Updated At Variant Variant Rank Variant Weight Variant Width Shipping Profile Id
2 prod_01J44RRKH4HH2SANJ0S05YM853 base-product Base product draft test-product-description test line 2 test-image.png pcol_01J44RRKG4P1RZ3CKAMBS760TD ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1 pcat_01J44RRKGS2N86A8V37ZJD877K 2024-07-31T16:07:55.681Z true test-image.png test-image-2.png false 123 456 2024-07-31T16:07:55.681Z variant_01J44RRKHRQ7WJ902X4936GEK7 Test variant true false 2024-07-31T16:07:55.704Z size large color green 30 45 100 prod_01J44RRKH4HH2SANJ0S05YM853 2024-07-31T16:07:55.704Z 0 import-shipping-profile
3 prod_01J44RRKH4HH2SANJ0S05YM853 base-product Base product draft test-product-description test line 2 test-image.png pcol_01J44RRKG4P1RZ3CKAMBS760TD ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1 pcat_01J44RRKGS2N86A8V37ZJD877K 2024-07-31T16:07:55.681Z true test-image.png test-image-2.png false 123 456 2024-07-31T16:07:55.681Z variant_01J44RRKHRAYY7Q7NTXNNMDA1S Test variant 2 true false 2024-07-31T16:07:55.704Z size small color green 50 65 200 prod_01J44RRKH4HH2SANJ0S05YM853 2024-07-31T16:07:55.704Z 0 import-shipping-profile
4
5
@@ -1,2 +1,2 @@
Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price EUR,Variant Price DKK,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Some field Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price EUR,Variant Price DKK,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Some field,Shipping Profile Id
prod_01J3CSN791SN1RN7X155Z8S9CN,Proposed product,,proposed,,test-product-description,proposed-product,false,true,test-image.png,,ptyp_01J3CSN76GCRSCDV9V489B5FWQ,,,,,,,,,2024-07-22T08:41:47.040Z,2024-07-22T08:41:47.040Z,,test-image.png,test-image-2.png,new-tag,variant_01J3CSN79CQ2ND94SRJSXMEMNH,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CSN791SN1RN7X155Z8S9CN,2024-07-22T08:41:47.053Z,2024-07-22T08:41:47.053Z,,100,45,30,size,large,color,green,someval prod_01J3CSN791SN1RN7X155Z8S9CN,Proposed product,,proposed,,test-product-description,proposed-product,false,true,test-image.png,,ptyp_01J3CSN76GCRSCDV9V489B5FWQ,,,,,,,,,2024-07-22T08:41:47.040Z,2024-07-22T08:41:47.040Z,,test-image.png,test-image-2.png,new-tag,variant_01J3CSN79CQ2ND94SRJSXMEMNH,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CSN791SN1RN7X155Z8S9CN,2024-07-22T08:41:47.053Z,2024-07-22T08:41:47.053Z,,100,45,30,size,large,color,green,someval,import-shipping-profile
1 Product Id Product Title Product Subtitle Product Status Product External Id Product Description Product Handle Product Is Giftcard Product Discountable Product Thumbnail Product Collection Id Product Type Id Product Weight Product Length Product Height Product Width Product Hs Code Product Origin Country Product Mid Code Product Material Product Created At Product Updated At Product Deleted At Product Image 1 Product Image 2 Product Tag 1 Variant Id Variant Title Variant Sku Variant Barcode Variant Ean Variant Upc Variant Allow Backorder Variant Manage Inventory Variant Hs Code Variant Origin Country Variant Mid Code Variant Material Variant Weight Variant Length Variant Height Variant Width Variant Metadata Variant Variant Rank Variant Product Id Variant Created At Variant Updated At Variant Deleted At Variant Price USD Variant Price EUR Variant Price DKK Variant Option 1 Name Variant Option 1 Value Variant Option 2 Name Variant Option 2 Value Some field Shipping Profile Id
2 prod_01J3CSN791SN1RN7X155Z8S9CN Proposed product proposed test-product-description proposed-product false true test-image.png ptyp_01J3CSN76GCRSCDV9V489B5FWQ 2024-07-22T08:41:47.040Z 2024-07-22T08:41:47.040Z test-image.png test-image-2.png new-tag variant_01J3CSN79CQ2ND94SRJSXMEMNH Test variant false true 0 prod_01J3CSN791SN1RN7X155Z8S9CN 2024-07-22T08:41:47.053Z 2024-07-22T08:41:47.053Z 100 45 30 size large color green someval import-shipping-profile
@@ -1,6 +1,6 @@
Product Id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product MID Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External Id,Variant Id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow Backorder,Variant Manage Inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant MID Code,Variant Material,Price Test region [USD],Price USD,Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url Product Id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product MID Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External Id,Variant Id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow Backorder,Variant Manage Inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant MID Code,Variant Material,Price Test region [USD],Price USD,Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url,Shipping Profile Id
,test-product-product-1,Test product,,"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",draft,,,,,,,,,,Test collection 1,test-collection1,,123_1,TRUE,,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,1.00,1.10,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png ,test-product-product-1,Test product,,"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",draft,,,,,,,,,,Test collection 1,test-collection1,,123_1,TRUE,,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,1.00,1.10,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png,import-shipping-profile
,test-product-product-1-1,Test product,,"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",draft,,,,,,,,,,Test collection 1,test-collection1,,,TRUE,,,Test variant,test-sku-1-1,test-barcode-1-1,10,FALSE,TRUE,,,,,,,,,1.00,1.10,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png ,test-product-product-1-1,Test product,,"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",draft,,,,,,,,,,Test collection 1,test-collection1,,,TRUE,,,Test variant,test-sku-1-1,test-barcode-1-1,10,FALSE,TRUE,,,,,,,,,1.00,1.10,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png,import-shipping-profile
existing-product-id,test-product-product-2,Test product,,test-product-description,draft,test-image.png,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,1.10,Size,Small,,,test-image.png existing-product-id,test-product-product-2,Test product,,test-product-description,draft,test-image.png,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,1.10,Size,Small,,,test-image.png,import-shipping-profile
existing-product-id,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,1.20,Size,Medium,,,test-image.png existing-product-id,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,1.20,Size,Medium,,,test-image.png,import-shipping-profile
existing-product-id,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,existing-variant-id,Test variant changed,test-sku-4,test-barcode-4,10,FALSE,TRUE,,,,,,,,,,,Size,Large,,,test-image.png existing-product-id,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,existing-variant-id,Test variant changed,test-sku-4,test-barcode-4,10,FALSE,TRUE,,,,,,,,,,,Size,Large,,,test-image.png,import-shipping-profile
1 Product Id Product Handle Product Title Product Subtitle Product Description Product Status Product Thumbnail Product Weight Product Length Product Width Product Height Product HS Code Product Origin Country Product MID Code Product Material Product Collection Title Product Collection Handle Product Type Product Tags Product Discountable Product External Id Variant Id Variant Title Variant SKU Variant Barcode Variant Inventory Quantity Variant Allow Backorder Variant Manage Inventory Variant Weight Variant Length Variant Width Variant Height Variant HS Code Variant Origin Country Variant MID Code Variant Material Price Test region [USD] Price USD Option 1 Name Option 1 Value Option 2 Name Option 2 Value Image 1 Url Shipping Profile Id
2 test-product-product-1 Test product Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal. draft Test collection 1 test-collection1 123_1 TRUE Test variant test-sku-1 test-barcode-1 10 FALSE TRUE 1.00 1.10 test-option-1 option 1 value red test-option-2 option 2 value 1 test-image.png import-shipping-profile
3 test-product-product-1-1 Test product Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal. draft Test collection 1 test-collection1 TRUE Test variant test-sku-1-1 test-barcode-1-1 10 FALSE TRUE 1.00 1.10 test-option-1 option 1 value red test-option-2 option 2 value 1 test-image.png import-shipping-profile
4 existing-product-id test-product-product-2 Test product test-product-description draft test-image.png Test collection test-collection2 test-type 123 TRUE Test variant test-sku-2 test-barcode-2 10 FALSE TRUE 1.10 Size Small test-image.png import-shipping-profile
5 existing-product-id test-product-product-2 Test product test-product-description draft Test collection test-collection2 test-type 123 TRUE Test variant test-sku-3 test-barcode-3 10 FALSE TRUE 1.20 Size Medium test-image.png import-shipping-profile
6 existing-product-id test-product-product-2 Test product test-product-description draft Test collection test-collection2 test-type 123 TRUE existing-variant-id Test variant changed test-sku-4 test-barcode-4 10 FALSE TRUE Size Large test-image.png import-shipping-profile
@@ -39,6 +39,9 @@ const compareCSVs = async (filePath, expectedFilePath) => {
fileContent = fileContent.replace(dateRegex, "<DATE>") fileContent = fileContent.replace(dateRegex, "<DATE>")
fixturesContent = fixturesContent.replace(dateRegex, "<DATE>") fixturesContent = fixturesContent.replace(dateRegex, "<DATE>")
fixturesContent = fixturesContent.replace(/,Shipping Profile Id*/g, "")
fixturesContent = fixturesContent.replace(/,import-shipping-profile*/g, "")
expect(fileContent).toEqual(fixturesContent) expect(fileContent).toEqual(fixturesContent)
} }
@@ -56,6 +59,7 @@ medusaIntegrationTestRunner({
let baseTag1 let baseTag1
let baseTag2 let baseTag2
let newTag let newTag
let shippingProfile
let eventBus: IEventBusModuleService let eventBus: IEventBusModuleService
beforeAll(async () => { beforeAll(async () => {
@@ -92,6 +96,14 @@ medusaIntegrationTestRunner({
) )
).data.collection ).data.collection
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
baseType = ( baseType = (
await api.post( await api.post(
"/admin/product-types", "/admin/product-types",
@@ -130,6 +142,7 @@ medusaIntegrationTestRunner({
getProductFixture({ getProductFixture({
title: "Base product", title: "Base product",
description: "test-product-description\ntest line 2", description: "test-product-description\ntest line 2",
shipping_profile_id: shippingProfile.id,
collection_id: baseCollection.id, collection_id: baseCollection.id,
type_id: baseType.id, type_id: baseType.id,
categories: [{ id: baseCategory.id }], categories: [{ id: baseCategory.id }],
@@ -191,6 +204,7 @@ medusaIntegrationTestRunner({
status: "proposed", status: "proposed",
tags: [{ id: newTag.id }], tags: [{ id: newTag.id }],
type_id: baseType.id, type_id: baseType.id,
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -281,6 +295,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
getProductFixture({ getProductFixture({
title: "Product with prices", title: "Product with prices",
shipping_profile_id: shippingProfile.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [ variants: [
{ {
@@ -40,6 +40,7 @@ medusaIntegrationTestRunner({
let baseTag2 let baseTag2
let baseTag3 let baseTag3
let newTag let newTag
let shippingProfile
let eventBus: IEventBusModuleService let eventBus: IEventBusModuleService
beforeAll(async () => { beforeAll(async () => {
@@ -84,12 +85,21 @@ medusaIntegrationTestRunner({
) )
).data.product_tag ).data.product_tag
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
baseProduct = ( baseProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ getProductFixture({
title: "Base product", title: "Base product",
tags: [{ id: baseTag1.id }, { id: baseTag2.id }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -150,6 +160,11 @@ medusaIntegrationTestRunner({
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id) fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id) fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({ const { form, meta } = getUploadReq({
name: "test.csv", name: "test.csv",
content: fileContent, content: fileContent,
@@ -400,6 +415,11 @@ medusaIntegrationTestRunner({
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id) fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
fileContent = fileContent.replace(/pcat_\w*\d*/g, baseCategory.id) fileContent = fileContent.replace(/pcat_\w*\d*/g, baseCategory.id)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({ const { form, meta } = getUploadReq({
name: "test.csv", name: "test.csv",
content: fileContent, content: fileContent,
@@ -440,6 +460,11 @@ medusaIntegrationTestRunner({
{ encoding: "utf-8" } { encoding: "utf-8" }
) )
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({ const { form, meta } = getUploadReq({
name: "test.csv", name: "test.csv",
content: fileContent, content: fileContent,
@@ -467,6 +492,11 @@ medusaIntegrationTestRunner({
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id) fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id) fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({ const { form, meta } = getUploadReq({
name: "test.csv", name: "test.csv",
content: fileContent, content: fileContent,
@@ -517,6 +547,11 @@ medusaIntegrationTestRunner({
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id) fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id) fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({ const { form, meta } = getUploadReq({
name: "test.csv", name: "test.csv",
content: fileContent, content: fileContent,
@@ -582,6 +617,11 @@ medusaIntegrationTestRunner({
baseCollection.handle baseCollection.handle
) )
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({ const { form, meta } = getUploadReq({
name: "test.csv", name: "test.csv",
content: fileContent, content: fileContent,
@@ -22,6 +22,8 @@ medusaIntegrationTestRunner({
let baseTag2 let baseTag2
let newTag let newTag
let shippingProfile
beforeEach(async () => { beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer()) await createAdminUser(dbConnection, adminHeaders, getContainer())
@@ -65,6 +67,14 @@ medusaIntegrationTestRunner({
) )
).data.product_tag ).data.product_tag
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
baseProduct = ( baseProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
@@ -74,6 +84,7 @@ medusaIntegrationTestRunner({
// BREAKING: Type input changed from {type: {value: string}} to {type_id: string} // BREAKING: Type input changed from {type: {value: string}} to {type_id: string}
type_id: baseType.id, type_id: baseType.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
shipping_profile_id: shippingProfile.id,
images: [ images: [
{ {
url: "image-one", url: "image-one",
@@ -95,6 +106,7 @@ medusaIntegrationTestRunner({
status: "proposed", status: "proposed",
tags: [{ id: newTag.id }], tags: [{ id: newTag.id }],
type_id: baseType.id, type_id: baseType.id,
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -108,6 +120,7 @@ medusaIntegrationTestRunner({
status: "published", status: "published",
collection_id: publishedCollection.id, collection_id: publishedCollection.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -115,7 +128,10 @@ medusaIntegrationTestRunner({
deletedProduct = ( deletedProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ title: "Deleted product" }), getProductFixture({
title: "Deleted product",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders adminHeaders
) )
).data.product ).data.product
@@ -605,6 +621,7 @@ medusaIntegrationTestRunner({
is_giftcard: true, is_giftcard: true,
description: "test-giftcard-description", description: "test-giftcard-description",
options: [{ title: "Denominations", values: ["100"] }], options: [{ title: "Denominations", values: ["100"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -693,6 +710,7 @@ medusaIntegrationTestRunner({
is_giftcard: true, is_giftcard: true,
description: "test-giftcard-description", description: "test-giftcard-description",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -912,6 +930,7 @@ medusaIntegrationTestRunner({
getProductFixture({ getProductFixture({
title: "Test saleschannel", title: "Test saleschannel",
sales_channels: [{ id: salesChannel.id }], sales_channels: [{ id: salesChannel.id }],
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -1098,6 +1117,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1", title: "Test product - 1",
handle: "test-1", handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Custom inventory 1", title: "Custom inventory 1",
@@ -1141,6 +1161,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1", title: "Test product - 1",
handle: "test-1", handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Custom inventory 1", title: "Custom inventory 1",
@@ -1202,6 +1223,7 @@ medusaIntegrationTestRunner({
collection_id: baseCollection.id, collection_id: baseCollection.id,
type_id: baseType.id, type_id: baseType.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
@@ -1341,6 +1363,7 @@ medusaIntegrationTestRunner({
{ {
title: "Test create", title: "Test create",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Price with rules", title: "Price with rules",
@@ -1394,6 +1417,7 @@ medusaIntegrationTestRunner({
collection_id: baseCollection.id, collection_id: baseCollection.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
options: [{ title: "size", values: ["large"] }], options: [{ title: "size", values: ["large"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -1425,7 +1449,8 @@ medusaIntegrationTestRunner({
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }], images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
collection_id: baseCollection.id, collection_id: baseCollection.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
options: [{ title: "size", values: ["l", x] }], options: [{ title: "size", values: ["l", "x"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant 1", title: "Test variant 1",
@@ -1477,6 +1502,7 @@ medusaIntegrationTestRunner({
is_giftcard: true, is_giftcard: true,
description: "test-giftcard-description", description: "test-giftcard-description",
options: [{ title: "size", values: ["large"] }], options: [{ title: "size", values: ["large"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -1637,6 +1663,7 @@ medusaIntegrationTestRunner({
it("updates product variants (update price on existing variant, create new variant)", async () => { it("updates product variants (update price on existing variant, create new variant)", async () => {
const payload = { const payload = {
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
id: baseProduct.variants[0].id, id: baseProduct.variants[0].id,
@@ -1815,6 +1842,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
getProductFixture({ getProductFixture({
title: "Test metadata", title: "Test metadata",
shipping_profile_id: shippingProfile.id,
metadata: { metadata: {
"test-key": "test-value", "test-key": "test-value",
"test-key-2": "test-value-2", "test-key-2": "test-value-2",
@@ -1877,6 +1905,7 @@ medusaIntegrationTestRunner({
getProductFixture({ getProductFixture({
title: "Test saleschannel", title: "Test saleschannel",
sales_channels: [{ id: salesChannel1.id }], sales_channels: [{ id: salesChannel1.id }],
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -2045,7 +2074,10 @@ medusaIntegrationTestRunner({
const plainProduct = ( const plainProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
{ title: "Test variant order" }, {
title: "Test variant order",
shipping_profile_id: shippingProfile.id,
},
adminHeaders adminHeaders
) )
).data.product ).data.product
@@ -2152,6 +2184,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1", title: "Test product - 1",
handle: "test-1", handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Custom inventory 1", title: "Custom inventory 1",
@@ -2265,6 +2298,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1", title: "Test product - 1",
handle: "test-1", handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Custom inventory 1", title: "Custom inventory 1",
@@ -2316,6 +2350,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1", title: "Test product - 1",
handle: "test-1", handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Custom inventory 1", title: "Custom inventory 1",
@@ -2490,6 +2525,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1", title: "Test product - 1",
handle: "test-1", handle: "test-1",
options: [{ title: "size", values: ["l"] }], options: [{ title: "size", values: ["l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Custom inventory 1", title: "Custom inventory 1",
@@ -2520,6 +2556,7 @@ medusaIntegrationTestRunner({
title: "Test product - 2", title: "Test product - 2",
handle: "test-2", handle: "test-2",
options: [{ title: "size", values: ["l"] }], options: [{ title: "size", values: ["l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "W/ shared inventory item", title: "W/ shared inventory item",
@@ -2604,6 +2641,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1", title: "Test product - 1",
handle: "test-1", handle: "test-1",
options: [{ title: "size", values: ["l"] }], options: [{ title: "size", values: ["l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Custom inventory 1", title: "Custom inventory 1",
@@ -2740,6 +2778,7 @@ medusaIntegrationTestRunner({
title: baseProduct.title, title: baseProduct.title,
handle: baseProduct.handle, handle: baseProduct.handle,
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -2772,6 +2811,7 @@ medusaIntegrationTestRunner({
handle: baseProduct.handle, handle: baseProduct.handle,
description: "test-product-description", description: "test-product-description",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -2948,6 +2988,7 @@ medusaIntegrationTestRunner({
const createPayload = getProductFixture({ const createPayload = getProductFixture({
title: "Test batch create", title: "Test batch create",
handle: "test-batch-create", handle: "test-batch-create",
shipping_profile_id: shippingProfile.id,
}) })
const updatePayload = { const updatePayload = {
@@ -3006,6 +3047,7 @@ medusaIntegrationTestRunner({
const productWithMultipleVariants = getProductFixture({ const productWithMultipleVariants = getProductFixture({
title: "Test batch variants", title: "Test batch variants",
handle: "test-batch-variants", handle: "test-batch-variants",
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Variant 1", title: "Variant 1",
@@ -11,7 +11,7 @@ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
let baseProduct let baseProduct
let baseRegion let baseRegion
let shippingProfile
beforeEach(async () => { beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer()) await createAdminUser(dbConnection, adminHeaders, getContainer())
// BREAKING: Creating a region no longer takes tax_rate, payment_providers, fulfillment_providers, countriesr // BREAKING: Creating a region no longer takes tax_rate, payment_providers, fulfillment_providers, countriesr
@@ -26,11 +26,20 @@ medusaIntegrationTestRunner({
) )
).data.region ).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
baseProduct = ( baseProduct = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ getProductFixture({
title: "Base product", title: "Base product",
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -67,6 +76,7 @@ medusaIntegrationTestRunner({
{ title: "First variant", prices: [] }, { title: "First variant", prices: [] },
{ title: "Second variant", prices: [] }, { title: "Second variant", prices: [] },
], ],
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -801,6 +811,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: "product 1", title: "product 1",
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "medium", "small"] }, { title: "size", values: ["large", "medium", "small"] },
], ],
@@ -41,6 +41,7 @@ medusaIntegrationTestRunner({
let publishableKey let publishableKey
let storeHeadersWithCustomer let storeHeadersWithCustomer
let customer let customer
let shippingProfile
const createProducts = async (data) => { const createProducts = async (data) => {
const response = await api.post( const response = await api.post(
@@ -135,6 +136,14 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
).data.region ).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
}) })
describe("Get products based on publishable key", () => { describe("Get products based on publishable key", () => {
@@ -145,7 +154,11 @@ medusaIntegrationTestRunner({
product1 = ( product1 = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ title: "test1", status: "published" }), getProductFixture({
title: "test1",
status: "published",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders adminHeaders
) )
).data.product ).data.product
@@ -153,7 +166,11 @@ medusaIntegrationTestRunner({
product2 = ( product2 = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ title: "test2", status: "published" }), getProductFixture({
title: "test2",
status: "published",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders adminHeaders
) )
).data.product ).data.product
@@ -161,7 +178,11 @@ medusaIntegrationTestRunner({
product3 = ( product3 = (
await api.post( await api.post(
"/admin/products", "/admin/products",
getProductFixture({ title: "test3", status: "published" }), getProductFixture({
title: "test3",
status: "published",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders adminHeaders
) )
).data.product ).data.product
@@ -500,6 +521,7 @@ medusaIntegrationTestRunner({
title: "test product 1", title: "test product 1",
collection_id: collection.id, collection_id: collection.id,
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -538,6 +560,7 @@ medusaIntegrationTestRunner({
;[product2, [variant2]] = await createProducts({ ;[product2, [variant2]] = await createProducts({
title: "test product 2 uniquely", title: "test product 2 uniquely",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "material", values: ["cotton", "polyester"] }, { title: "material", values: ["cotton", "polyester"] },
@@ -557,6 +580,7 @@ medusaIntegrationTestRunner({
;[product3, [variant3]] = await createProducts({ ;[product3, [variant3]] = await createProducts({
title: "product not in price list", title: "product not in price list",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
variants: [ variants: [
{ title: "test variant 3", prices: [], options: { size: "large" } }, { title: "test variant 3", prices: [], options: { size: "large" } },
@@ -565,6 +589,7 @@ medusaIntegrationTestRunner({
;[product4, [variant4]] = await createProducts({ ;[product4, [variant4]] = await createProducts({
title: "draft product", title: "draft product",
status: ProductStatus.DRAFT, status: ProductStatus.DRAFT,
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
variants: [ variants: [
{ title: "test variant 4", prices: [], options: { size: "large" } }, { title: "test variant 4", prices: [], options: { size: "large" } },
@@ -1727,6 +1752,7 @@ medusaIntegrationTestRunner({
;[product, [variant]] = await createProducts({ ;[product, [variant]] = await createProducts({
title: "test product 1", title: "test product 1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large"] }], options: [{ title: "size", values: ["large"] }],
variants: [ variants: [
{ {
@@ -2206,6 +2232,7 @@ medusaIntegrationTestRunner({
getProductFixture({ getProductFixture({
title: "test1", title: "test1",
status: "published", status: "published",
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test taxes", title: "Test taxes",
@@ -2239,6 +2266,7 @@ medusaIntegrationTestRunner({
getProductFixture({ getProductFixture({
title: "test2", title: "test2",
status: "published", status: "published",
shipping_profile_id: shippingProfile.id,
}), }),
adminHeaders adminHeaders
) )
@@ -56,6 +56,7 @@ medusaIntegrationTestRunner({
let appContainer let appContainer
let promotion let promotion
let standardPromotion let standardPromotion
let shippingProfile
const promotionRule = { const promotionRule = {
operator: "eq", operator: "eq",
@@ -90,6 +91,14 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
).data.promotion ).data.promotion
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
}) })
describe("GET /admin/promotions/:id", () => { describe("GET /admin/promotions/:id", () => {
@@ -512,7 +521,10 @@ medusaIntegrationTestRunner({
const product = ( const product = (
await api.post( await api.post(
"/admin/products", "/admin/products",
medusaTshirtProduct, {
...medusaTshirtProduct,
shipping_profile_id: shippingProfile.id,
},
adminHeaders adminHeaders
) )
).data.product ).data.product
@@ -1494,6 +1506,7 @@ medusaIntegrationTestRunner({
{ {
title: "Test product 1", title: "Test product 1",
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
}, },
adminHeaders adminHeaders
) )
@@ -1505,6 +1518,7 @@ medusaIntegrationTestRunner({
{ {
title: "Test product 2", title: "Test product 2",
options: [{ title: "size", values: ["large", "small"] }], options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
}, },
adminHeaders adminHeaders
) )
@@ -25,12 +25,24 @@ medusaIntegrationTestRunner({
const container = getContainer() const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container) await createAdminUser(dbConnection, adminHeaders, container)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
const product = ( const product = (
await api.post( await api.post(
"/admin/products", "/admin/products",
{ {
title: "Test product", title: "Test product",
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -149,17 +161,6 @@ medusaIntegrationTestRunner({
], ],
}) })
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
location = ( location = (
await api.post( await api.post(
`/admin/stock-locations`, `/admin/stock-locations`,
@@ -354,11 +354,20 @@ medusaIntegrationTestRunner({
// to: /admin/sales-channels/:id/products // to: /admin/sales-channels/:id/products
let product let product
beforeEach(async () => { beforeEach(async () => {
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product = ( product = (
await api.post( await api.post(
"/admin/products", "/admin/products",
{ {
title: "test name", title: "test name",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large"] }], options: [{ title: "size", values: ["large"] }],
}, },
adminHeaders adminHeaders
@@ -58,6 +58,14 @@ medusaIntegrationTestRunner({
) )
).data.region ).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
salesChannel = ( salesChannel = (
await api.post( await api.post(
"/admin/sales-channels", "/admin/sales-channels",
@@ -71,6 +79,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: "Test fixture", title: "Test fixture",
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -114,14 +123,6 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const fulfillmentSets = ( const fulfillmentSets = (
await api.post( await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
@@ -57,6 +57,14 @@ medusaIntegrationTestRunner({
) )
).data.region ).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
salesChannel = ( salesChannel = (
await api.post( await api.post(
"/admin/sales-channels", "/admin/sales-channels",
@@ -74,6 +82,7 @@ medusaIntegrationTestRunner({
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
], ],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "Test variant", title: "Test variant",
@@ -113,14 +122,6 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const fulfillmentSets = ( const fulfillmentSets = (
await api.post( await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
@@ -658,6 +658,7 @@ medusaIntegrationTestRunner({
describe("POST /store/carts/:id/line-items", () => { describe("POST /store/carts/:id/line-items", () => {
let region let region
const productData = { const productData = {
title: "Medusa T-Shirt", title: "Medusa T-Shirt",
handle: "t-shirt", handle: "t-shirt",
@@ -716,8 +717,21 @@ medusaIntegrationTestRunner({
}) })
it("adding an existing variant should update or create line item depending on metadata", async () => { it("adding an existing variant should update or create line item depending on metadata", async () => {
const shippingProfile =
await fulfillmentModule.createShippingProfiles({
name: "Test",
type: "default",
})
const product = ( const product = (
await api.post(`/admin/products`, productData, adminHeaders) await api.post(
`/admin/products`,
{
...productData,
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
).data.product ).data.product
const cart = ( const cart = (
@@ -1203,6 +1217,7 @@ medusaIntegrationTestRunner({
"/admin/products", "/admin/products",
{ {
title: "Test fixture", title: "Test fixture",
shipping_profile_id: shippingProfile.id,
options: [ options: [
{ title: "size", values: ["large", "small"] }, { title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] }, { title: "color", values: ["green"] },
@@ -30,11 +30,20 @@ medusaIntegrationTestRunner({
beforeEach(async () => { beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer()) await createAdminUser(dbConnection, adminHeaders, getContainer())
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product = ( product = (
await api.post( await api.post(
"/admin/products", "/admin/products",
{ {
title: "product 1", title: "product 1",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["x", "l"] }], options: [{ title: "size", values: ["x", "l"] }],
variants: [ variants: [
{ {
@@ -31,9 +31,18 @@ medusaIntegrationTestRunner({
describe("Index engine", () => { describe("Index engine", () => {
it("should search through the indexed data and return the correct results ordered and filtered [1]", async () => { it("should search through the indexed data and return the correct results ordered and filtered [1]", async () => {
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const payload = { const payload = {
title: "Test Giftcard", title: "Test Giftcard",
is_giftcard: true, is_giftcard: true,
shipping_profile_id: shippingProfile.id,
description: "test-giftcard-description", description: "test-giftcard-description",
options: [{ title: "Denominations", values: ["100"] }], options: [{ title: "Denominations", values: ["100"] }],
variants: new Array(10).fill(0).map((_, i) => ({ variants: new Array(10).fill(0).map((_, i) => ({
@@ -101,10 +110,19 @@ medusaIntegrationTestRunner({
}) })
it("should search through the indexed data and return the correct results ordered and filtered [2]", async () => { it("should search through the indexed data and return the correct results ordered and filtered [2]", async () => {
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const payload = { const payload = {
title: "Test Giftcard", title: "Test Giftcard",
is_giftcard: true, is_giftcard: true,
description: "test-giftcard-description", description: "test-giftcard-description",
shipping_profile_id: shippingProfile.id,
options: [{ title: "Denominations", values: ["100"] }], options: [{ title: "Denominations", values: ["100"] }],
variants: new Array(10).fill(0).map((_, i) => ({ variants: new Array(10).fill(0).map((_, i) => ({
title: `Test variant ${i}`, title: `Test variant ${i}`,
@@ -171,9 +189,18 @@ medusaIntegrationTestRunner({
}) })
it.skip("should search through the indexed data and return the correct results ordered and filtered [3]", async () => { it.skip("should search through the indexed data and return the correct results ordered and filtered [3]", async () => {
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const payloads = new Array(50).fill(0).map((_, a) => ({ const payloads = new Array(50).fill(0).map((_, a) => ({
title: "Test Giftcard-" + a, title: "Test Giftcard-" + a,
is_giftcard: true, is_giftcard: true,
shipping_profile_id: shippingProfile.id,
description: "test-giftcard-description" + a, description: "test-giftcard-description" + a,
options: [{ title: "Denominations", values: ["100"] }], options: [{ title: "Denominations", values: ["100"] }],
variants: new Array(10).fill(0).map((_, i) => ({ variants: new Array(10).fill(0).map((_, i) => ({
@@ -209,9 +209,18 @@ medusaIntegrationTestRunner({
beforeEach(async () => { beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, appContainer) await createAdminUser(dbConnection, adminHeaders, appContainer)
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const payload = { const payload = {
title: "Test Giftcard", title: "Test Giftcard",
is_giftcard: true, is_giftcard: true,
shipping_profile_id: shippingProfile.id,
description: "test-giftcard-description", description: "test-giftcard-description",
options: [{ title: "Denominations", values: ["100"] }], options: [{ title: "Denominations", values: ["100"] }],
variants: [ variants: [
@@ -130,6 +130,17 @@ async function prepareDataFixtures({ container }) {
}, },
]) ])
await remoteLink.create([
{
[Modules.PRODUCT]: {
product_id: product.id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id: shippingProfile.id,
},
},
])
await remoteLink.create([ await remoteLink.create([
{ {
[Modules.STOCK_LOCATION]: { [Modules.STOCK_LOCATION]: {
@@ -124,6 +124,17 @@ async function prepareDataFixtures({ container }) {
}, },
]) ])
await remoteLink.create([
{
[Modules.PRODUCT]: {
product_id: product.id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id: shippingProfile.id,
},
},
])
await remoteLink.create([ await remoteLink.create([
{ {
[Modules.STOCK_LOCATION]: { [Modules.STOCK_LOCATION]: {
@@ -4,7 +4,10 @@ import {
batchProductVariantsWorkflow, batchProductVariantsWorkflow,
batchProductVariantsWorkflowId, batchProductVariantsWorkflowId,
} from "@medusajs/core-flows" } from "@medusajs/core-flows"
import { IProductModuleService } from "@medusajs/types" import {
IFulfillmentModuleService,
IProductModuleService,
} from "@medusajs/types"
import { Modules } from "@medusajs/utils" import { Modules } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
@@ -17,9 +20,20 @@ medusaIntegrationTestRunner({
let appContainer let appContainer
let service: IProductModuleService let service: IProductModuleService
let fulfullmentService: IFulfillmentModuleService
let shippingProfile
beforeAll(async () => { beforeAll(async () => {
appContainer = getContainer() appContainer = getContainer()
service = appContainer.resolve(Modules.PRODUCT) service = appContainer.resolve(Modules.PRODUCT)
fulfullmentService = appContainer.resolve(Modules.FULFILLMENT)
})
beforeEach(async () => {
shippingProfile = await fulfullmentService.createShippingProfiles({
name: "Test",
type: "default",
})
}) })
describe("batchProductWorkflow", () => { describe("batchProductWorkflow", () => {
@@ -43,7 +57,8 @@ medusaIntegrationTestRunner({
create: [ create: [
{ {
title: "test3", title: "test3",
options: [{ title: "size", options: ["x"] }], shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["x"] }],
}, },
], ],
update: [{ id: product1.id, title: "test1-updated" }], update: [{ id: product1.id, title: "test1-updated" }],
@@ -94,6 +109,7 @@ medusaIntegrationTestRunner({
{ {
title: "test1", title: "test1",
options: [{ title: "size", values: ["x", "l", "m"] }], options: [{ title: "size", values: ["x", "l", "m"] }],
shipping_profile_id: shippingProfile.id,
variants: [ variants: [
{ {
title: "variant1", title: "variant1",
@@ -0,0 +1 @@
export * from "./sidebar-link"
@@ -0,0 +1,47 @@
import { ReactNode } from "react"
import { Link } from "react-router-dom"
import { IconAvatar } from "../icon-avatar"
import { Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
export interface SidebarLinkProps {
to: string
labelKey: string
descriptionKey: string
icon: ReactNode
}
export const SidebarLink = ({
to,
labelKey,
descriptionKey,
icon,
}: SidebarLinkProps) => {
return (
<Link to={to} className="group outline-none">
<div className="flex flex-col gap-2 px-2 pb-2">
<div className="shadow-elevation-card-rest bg-ui-bg-component transition-fg hover:bg-ui-bg-component-hover active:bg-ui-bg-component-pressed group-focus-visible:shadow-borders-interactive-with-active rounded-md px-4 py-2">
<div className="flex items-center gap-4">
<IconAvatar>{icon}</IconAvatar>
<div className="flex flex-1 flex-col">
<Text size="small" leading="compact" weight="plus">
{labelKey}
</Text>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
{descriptionKey}
</Text>
</div>
<div className="flex size-7 items-center justify-center">
<TriangleRightMini className="text-ui-fg-muted" />
</div>
</div>
</div>
</div>
</Link>
)
}
File diff suppressed because it is too large. Load diff
@@ -554,6 +554,10 @@
"label": "Discountable", "label": "Discountable",
"hint": "When unchecked, discounts will not be applied to this product" "hint": "When unchecked, discounts will not be applied to this product"
}, },
"shipping_profile": {
"label": "Shipping profile",
"hint": "Connect the product to a shipping profile"
},
"type": { "type": {
"label": "Type" "label": "Type"
}, },
@@ -681,6 +685,20 @@
"alreadyManagedWithSku": "This inventory item is already editable under {{title}} ({{sku}})." "alreadyManagedWithSku": "This inventory item is already editable under {{title}} ({{sku}})."
} }
}, },
"shippingProfile": {
"header": "Shipping configuration",
"edit": {
"header": "Shipping Configuration",
"toasts": {
"success": "Successfully updated the shipping profile for {{title}}."
}
},
"create": {
"errors": {
"required": "Shipping profile is required"
}
}
},
"toasts": { "toasts": {
"delete": { "delete": {
"success": { "success": {
@@ -1249,6 +1267,8 @@
"fulfillment": { "fulfillment": {
"cancelWarning": "You are about to cancel a fulfillment. This action cannot be undone.", "cancelWarning": "You are about to cancel a fulfillment. This action cannot be undone.",
"markAsDeliveredWarning": "You are about to mark fulfillment as delivered. This action cannot be undone.", "markAsDeliveredWarning": "You are about to mark fulfillment as delivered. This action cannot be undone.",
"differentOptionSelected": "The selected shipping option is different from the one selected by the customer.",
"disabledItemTooltip": "The shipping option you have selected don’t allow fulfillment of this item",
"unfulfilledItems": "Unfulfilled Items", "unfulfilledItems": "Unfulfilled Items",
"statusLabel": "Fulfillment status", "statusLabel": "Fulfillment status",
"statusTitle": "Fulfillment Status", "statusTitle": "Fulfillment Status",
@@ -1269,7 +1289,9 @@
"error": { "error": {
"wrongQuantity": "Only one item is available for fulfillment", "wrongQuantity": "Only one item is available for fulfillment",
"wrongQuantity_other": "Quantity should be a number between 1 and {{number}}", "wrongQuantity_other": "Quantity should be a number between 1 and {{number}}",
"noItems": "No items to fulfill." "noItems": "No items to fulfill.",
"noShippingOption": "Shipping option is required",
"noLocation": "Location is required"
}, },
"status": { "status": {
"notFulfilled": "Not fulfilled", "notFulfilled": "Not fulfilled",
@@ -2749,7 +2771,9 @@
"added": "Added", "added": "Added",
"removed": "Removed", "removed": "Removed",
"from": "From", "from": "From",
"to": "To" "to": "To",
"beaware": "Be aware",
"loading": "Loading"
}, },
"fields": { "fields": {
"amount": "Amount", "amount": "Amount",
@@ -102,6 +102,13 @@ export const RouteMap: RouteObject[] = [
lazy: () => lazy: () =>
import("../../routes/products/product-organization"), import("../../routes/products/product-organization"),
}, },
{
path: "shipping-profile",
lazy: () =>
import(
"../../routes/products/product-shipping-profile"
),
},
{ {
path: "media", path: "media",
lazy: () => lazy: () =>
@@ -13,6 +13,7 @@ import { IconAvatar } from "../../../components/common/icon-avatar"
import { TwoColumnPage } from "../../../components/layout/pages" import { TwoColumnPage } from "../../../components/layout/pages"
import { useDashboardExtension } from "../../../extensions" import { useDashboardExtension } from "../../../extensions"
import { LocationListHeader } from "./components/location-list-header" import { LocationListHeader } from "./components/location-list-header"
import { SidebarLink } from "../../../components/common/sidebar-link/sidebar-link"
export function LocationList() { export function LocationList() {
const initialData = useLoaderData() as Awaited< const initialData = useLoaderData() as Awaited<
@@ -61,47 +62,6 @@ export function LocationList() {
) )
} }
interface SidebarLinkProps {
to: string
labelKey: string
descriptionKey: string
icon: ReactNode
}
const SidebarLink = ({
to,
labelKey,
descriptionKey,
icon,
}: SidebarLinkProps) => {
return (
<Link to={to} className="group outline-none">
<div className="flex flex-col gap-2 px-2 pb-2">
<div className="shadow-elevation-card-rest bg-ui-bg-component transition-fg hover:bg-ui-bg-component-hover active:bg-ui-bg-component-pressed group-focus-visible:shadow-borders-interactive-with-active rounded-md px-4 py-2">
<div className="flex items-center gap-4">
<IconAvatar>{icon}</IconAvatar>
<div className="flex flex-1 flex-col">
<Text size="small" leading="compact" weight="plus">
{labelKey}
</Text>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
{descriptionKey}
</Text>
</div>
<div className="flex size-7 items-center justify-center">
<TriangleRightMini className="text-ui-fg-muted" />
</div>
</div>
</div>
</div>
</Link>
)
}
const LinksSection = () => { const LinksSection = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -2,7 +2,6 @@ import { z } from "zod"
export const CreateFulfillmentSchema = z.object({ export const CreateFulfillmentSchema = z.object({
quantity: z.record(z.string(), z.number()), quantity: z.record(z.string(), z.number()),
location_id: z.string(), location_id: z.string(),
shipping_option_id: z.string().optional(), shipping_option_id: z.string().optional(),
send_notification: z.boolean().optional(), send_notification: z.boolean().optional(),
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { AdminOrder } from "@medusajs/types" import { AdminOrder, HttpTypes } from "@medusajs/types"
import { Alert, Button, Select, Switch, toast } from "@medusajs/ui" import { Alert, Button, Select, Switch, toast } from "@medusajs/ui"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
@@ -19,7 +19,10 @@ import { useStockLocations } from "../../../../../hooks/api/stock-locations"
import { getFulfillableQuantity } from "../../../../../lib/order-item" import { getFulfillableQuantity } from "../../../../../lib/order-item"
import { CreateFulfillmentSchema } from "./constants" import { CreateFulfillmentSchema } from "./constants"
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item" import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
import { useReservationItems } from "../../../../../hooks/api" import {
useReservationItems,
useShippingOptions,
} from "../../../../../hooks/api"
type OrderCreateFulfillmentFormProps = { type OrderCreateFulfillmentFormProps = {
order: AdminOrder order: AdminOrder
@@ -56,30 +59,86 @@ export function OrderCreateFulfillmentForm({
const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({ const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({
defaultValues: { defaultValues: {
quantity: fulfillableItems.reduce((acc, item) => { quantity: fulfillableItems.reduce(
(acc, item) => {
acc[item.id] = getFulfillableQuantity(item) acc[item.id] = getFulfillableQuantity(item)
return acc return acc
}, {} as Record<string, number>), },
{} as Record<string, number>
),
send_notification: !order.no_notification, send_notification: !order.no_notification,
}, },
resolver: zodResolver(CreateFulfillmentSchema), resolver: zodResolver(CreateFulfillmentSchema),
}) })
const selectedLocationId = useWatch({
name: "location_id",
control: form.control,
})
const { stock_locations = [] } = useStockLocations() const { stock_locations = [] } = useStockLocations()
const { shipping_options = [], isLoading: isShippingOptionsLoading } =
useShippingOptions({
stock_location_id: selectedLocationId,
// is_return: false, // TODO: 500 when enabled
fields: "+service_zone.fulfillment_set.location.id",
})
const shippingOptionId = useWatch({
name: "shipping_option_id",
control: form.control,
})
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
try { const selectedShippingOption = shipping_options.find(
await createOrderFulfillment({ (o) => o.id === shippingOptionId
location_id: data.location_id, )
// shipping_option_id: data.shipping_option_id,
if (!selectedShippingOption) {
form.setError("shipping_option_id", {
type: "manual",
message: t("orders.fulfillment.error.noShippingOption"),
})
return
}
if (!selectedLocationId) {
form.setError("location_id", {
type: "manual",
message: t("orders.fulfillment.error.noLocation"),
})
return
}
const selectedShippingProfileId =
selectedShippingOption?.shipping_profile_id
const itemShippingProfileMap = order.items.reduce(
(acc, item) => {
acc[item.id] = item.variant?.product?.shipping_profile?.id
return acc
},
{} as Record<string, string | null>
)
const payload: HttpTypes.AdminCreateOrderFulfillment = {
location_id: selectedLocationId,
shipping_option_id: shippingOptionId,
no_notification: !data.send_notification, no_notification: !data.send_notification,
items: Object.entries(data.quantity) items: Object.entries(data.quantity)
.filter(([, value]) => !!value) .filter(
([id, value]) =>
!!value && itemShippingProfileMap[id] === selectedShippingProfileId
)
.map(([id, quantity]) => ({ .map(([id, quantity]) => ({
id, id,
quantity, quantity,
})), })),
}) }
try {
await createOrderFulfillment(payload)
toast.success(t("orders.fulfillment.toast.created")) toast.success(t("orders.fulfillment.toast.created"))
handleSuccess(`/orders/${order.id}`) handleSuccess(`/orders/${order.id}`)
@@ -89,15 +148,28 @@ export function OrderCreateFulfillmentForm({
}) })
useEffect(() => { useEffect(() => {
if (stock_locations?.length) { if (stock_locations?.length && shipping_options?.length) {
form.setValue("location_id", stock_locations[0].id) const initialShippingOptionId =
} order.shipping_methods?.[0]?.shipping_option_id
}, [stock_locations?.length])
const selectedLocationId = useWatch({ if (initialShippingOptionId) {
name: "location_id", const shippingOption = shipping_options.find(
control: form.control, (o) => o.id === initialShippingOptionId
}) )
if (shippingOption) {
const locationId =
shippingOption.service_zone.fulfillment_set.location.id
form.setValue("location_id", locationId)
form.setValue(
"shipping_option_id",
initialShippingOptionId || undefined
)
} // else -> TODO: what if original shipping option is deleted?
}
}
}, [stock_locations?.length, shipping_options?.length])
const fulfilledQuantityArray = (order.items || []).map( const fulfilledQuantityArray = (order.items || []).map(
(item) => (item) =>
@@ -124,14 +196,21 @@ export function OrderCreateFulfillmentForm({
}) })
} }
const quantityMap = itemsToFulfill.reduce((acc, item) => { const quantityMap = itemsToFulfill.reduce(
(acc, item) => {
acc[item.id] = getFulfillableQuantity(item as OrderLineItemDTO) acc[item.id] = getFulfillableQuantity(item as OrderLineItemDTO)
return acc return acc
}, {} as Record<string, number>) },
{} as Record<string, number>
)
form.setValue("quantity", quantityMap) form.setValue("quantity", quantityMap)
}, [...fulfilledQuantityArray, requiresShipping]) }, [...fulfilledQuantityArray, requiresShipping])
const differentOptionSelected =
shippingOptionId &&
order.shipping_methods?.[0]?.shipping_option_id !== shippingOptionId
return ( return (
<RouteFocusModal.Form form={form}> <RouteFocusModal.Form form={form}>
<KeyboundForm <KeyboundForm
@@ -185,48 +264,69 @@ export function OrderCreateFulfillmentForm({
/> />
</div> </div>
{/* <div className="py-8">*/} <div className="py-8">
{/* <Form.Field*/} <Form.Field
{/* control={form.control}*/} control={form.control}
{/* name="shipping_option_id"*/} name="shipping_option_id"
{/* render={({ field: { onChange, ref, ...field } }) => {*/} render={({ field: { onChange, ref, ...field } }) => {
{/* return (*/} return (
{/* <Form.Item>*/} <Form.Item>
{/* <div className="flex flex-col gap-2 xl:flex-row xl:items-center">*/} <div className="flex flex-col gap-2 xl:flex-row xl:items-center">
{/* <div className="flex-1">*/} <div className="flex-1">
{/* <Form.Label>*/} <Form.Label>
{/* {t("fields.shippingMethod")}*/} {t("fields.shippingMethod")}
{/* </Form.Label>*/} </Form.Label>
{/* <Form.Hint>*/} <Form.Hint>
{/* {t("orders.fulfillment.methodDescription")}*/} {t("orders.fulfillment.methodDescription")}
{/* </Form.Hint>*/} </Form.Hint>
{/* </div>*/} </div>
{/* <div className="flex-1">*/} <div className="flex-1">
{/* <Form.Control>*/} <Form.Control>
{/* <Select onValueChange={onChange} {...field}>*/} <Select
{/* <Select.Trigger*/} onValueChange={onChange}
{/* className="bg-ui-bg-base"*/} {...field}
{/* ref={ref}*/} disabled={!selectedLocationId}
{/* >*/} >
{/* <Select.Value />*/} <Select.Trigger
{/* </Select.Trigger>*/} className="bg-ui-bg-base"
{/* <Select.Content>*/} ref={ref}
{/* {shipping_options.map((o) => (*/} >
{/* <Select.Item key={o.id} value={o.id}>*/} {isShippingOptionsLoading ? (
{/* {o.name}*/} <span className="text-right">
{/* </Select.Item>*/} {t("labels.loading")}...
{/* ))}*/} </span>
{/* </Select.Content>*/} ) : (
{/* </Select>*/} <Select.Value />
{/* </Form.Control>*/} )}
{/* </div>*/} </Select.Trigger>
{/* </div>*/} <Select.Content>
{/* <Form.ErrorMessage />*/} {shipping_options.map((o) => (
{/* </Form.Item>*/} <Select.Item key={o.id} value={o.id}>
{/* )*/} {o.name}
{/* }}*/} </Select.Item>
{/* />*/} ))}
{/* </div>*/} </Select.Content>
</Select>
</Form.Control>
</div>
</div>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
{differentOptionSelected && (
<Alert className="mt-4 p-4" variant="warning">
<span className="-mt-[3px] block font-semibold">
{t("labels.beaware")}
</span>
<span className="text-ui-fg-muted">
{t("orders.fulfillment.differentOptionSelected")}
</span>
</Alert>
)}
</div>
<div> <div>
<Form.Item className="mt-8"> <Form.Item className="mt-8">
<Form.Label> <Form.Label>
@@ -238,12 +338,19 @@ export function OrderCreateFulfillmentForm({
<div className="flex flex-col gap-y-1"> <div className="flex flex-col gap-y-1">
{fulfillableItems.map((item) => { {fulfillableItems.map((item) => {
const isShippingProfileMatching =
shipping_options.find(
(o) => o.id === shippingOptionId
)?.shipping_profile_id ===
item.variant?.product?.shipping_profile?.id
return ( return (
<OrderCreateFulfillmentItem <OrderCreateFulfillmentItem
key={item.id} key={item.id}
form={form} form={form}
item={item} item={item}
locationId={selectedLocationId} locationId={selectedLocationId}
disabled={isShippingProfileMatching}
itemReservedQuantitiesMap={ itemReservedQuantitiesMap={
itemReservedQuantitiesMap itemReservedQuantitiesMap
} }
@@ -305,7 +412,12 @@ export function OrderCreateFulfillmentForm({
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</RouteFocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}> <Button
size="small"
type="submit"
isLoading={isMutating}
disabled={!shippingOptionId}
>
{t("orders.fulfillment.create")} {t("orders.fulfillment.create")}
</Button> </Button>
</div> </div>
@@ -1,7 +1,7 @@
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { Input, Text } from "@medusajs/ui" import { clx, Input, Text, Tooltip } from "@medusajs/ui"
import { UseFormReturn } from "react-hook-form" import { UseFormReturn } from "react-hook-form"
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
@@ -10,6 +10,7 @@ import { Thumbnail } from "../../../../../components/common/thumbnail/index"
import { useProductVariant } from "../../../../../hooks/api/products" import { useProductVariant } from "../../../../../hooks/api/products"
import { getFulfillableQuantity } from "../../../../../lib/order-item" import { getFulfillableQuantity } from "../../../../../lib/order-item"
import { CreateFulfillmentSchema } from "./constants" import { CreateFulfillmentSchema } from "./constants"
import { InformationCircleSolid } from "@medusajs/icons"
type OrderEditItemProps = { type OrderEditItemProps = {
item: HttpTypes.AdminOrderLineItem item: HttpTypes.AdminOrderLineItem
@@ -18,6 +19,7 @@ type OrderEditItemProps = {
onItemRemove: (itemId: string) => void onItemRemove: (itemId: string) => void
itemReservedQuantitiesMap: Map<string, number> itemReservedQuantitiesMap: Map<string, number>
form: UseFormReturn<zod.infer<typeof CreateFulfillmentSchema>> form: UseFormReturn<zod.infer<typeof CreateFulfillmentSchema>>
disabled: boolean
} }
export function OrderCreateFulfillmentItem({ export function OrderCreateFulfillmentItem({
@@ -25,6 +27,7 @@ export function OrderCreateFulfillmentItem({
form, form,
locationId, locationId,
itemReservedQuantitiesMap, itemReservedQuantitiesMap,
disabled,
}: OrderEditItemProps) { }: OrderEditItemProps) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -70,8 +73,25 @@ export function OrderCreateFulfillmentItem({
) )
return ( return (
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl "> <div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl">
<div className="flex flex-col gap-x-2 gap-y-2 border-b p-3 text-sm sm:flex-row"> <div className="flex flex-row items-center">
{disabled && (
<div className="inline-flex items-center ml-4">
<Tooltip
content={t("orders.fulfillment.disabledItemTooltip")}
side="top"
>
<InformationCircleSolid className="text-ui-tag-orange-icon" />
</Tooltip>
</div>
)}
<div
className={clx(
"flex flex-col flex-1 gap-x-2 gap-y-2 border-b p-3 text-sm sm:flex-row",
disabled && "opacity-50 pointer-events-none"
)}
>
<div className="flex flex-1 items-center gap-x-3"> <div className="flex flex-1 items-center gap-x-3">
<Thumbnail src={item.thumbnail} /> <Thumbnail src={item.thumbnail} />
<div className="flex flex-col"> <div className="flex flex-col">
@@ -170,5 +190,6 @@ export function OrderCreateFulfillmentItem({
</div> </div>
</div> </div>
</div> </div>
</div>
) )
} }
@@ -10,7 +10,8 @@ export function OrderCreateFulfillment() {
const requiresShipping = searchParams.get("requires_shipping") === "true" const requiresShipping = searchParams.get("requires_shipping") === "true"
const { order, isLoading, isError, error } = useOrder(id!, { const { order, isLoading, isError, error } = useOrder(id!, {
fields: "currency_code,*items,*items.variant,*shipping_address", fields:
"currency_code,*items,*items.variant,+items.variant.product.shipping_profile.id,*shipping_address,+shipping_methods.shipping_option_id",
}) })
if (isError) { if (isError) {
@@ -79,10 +79,13 @@ export const ProductCreateForm = ({
return {} return {}
} }
return regions.reduce((acc, reg) => { return regions.reduce(
(acc, reg) => {
acc[reg.id] = reg.currency_code acc[reg.id] = reg.currency_code
return acc return acc
}, {} as Record<string, string>) },
{} as Record<string, string>
)
}, [regions]) }, [regions])
/** /**
@@ -178,6 +181,15 @@ export const ProductCreateForm = ({
} }
if (currentTab === Tab.ORGANIZE) { if (currentTab === Tab.ORGANIZE) {
// TODO: this is temp until we add partial validation per tab
if (!form.getValues("shipping_profile_id")) {
form.setError("shipping_profile_id", {
type: "required",
message: t("products.shippingProfile.create.errors.required"),
})
return
}
setTab(Tab.VARIANTS) setTab(Tab.VARIANTS)
} }
@@ -208,7 +220,8 @@ export const ProductCreateForm = ({
} }
setTabState({ ...currentState }) setTabState({ ...currentState })
}, [tab, tabState]) // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want this effect to run when the tab changes
}, [tab])
return ( return (
<RouteFocusModal.Form form={form}> <RouteFocusModal.Form form={form}>
@@ -51,6 +51,16 @@ export const ProductCreateOrganizationSection = ({
})), })),
}) })
const shippingProfiles = useComboboxData({
queryKey: ["shipping_profiles"],
queryFn: (params) => sdk.admin.shippingProfile.list(params),
getOptions: (data) =>
data.shipping_profiles.map((shippingProfile) => ({
label: shippingProfile.name,
value: shippingProfile.id,
})),
})
const { fields, remove, replace } = useFieldArray({ const { fields, remove, replace } = useFieldArray({
control: form.control, control: form.control,
name: "sales_channels", name: "sales_channels",
@@ -161,6 +171,34 @@ export const ProductCreateOrganizationSection = ({
}} }}
/> />
</div> </div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<Form.Label>{t("products.fields.shipping_profile.label")}</Form.Label>
<Form.Hint>
<Trans i18nKey={"products.fields.shipping_profile.hint"} />
</Form.Hint>
</div>
<Form.Field
control={form.control}
name="shipping_profile_id"
render={({ field }) => {
return (
<Form.Item>
<Form.Control>
<Combobox
{...field}
options={shippingProfiles.options}
searchValue={shippingProfiles.searchValue}
onSearchValueChange={shippingProfiles.onSearchValueChange}
fetchNextPage={shippingProfiles.fetchNextPage}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<div className="grid grid-cols-1 gap-y-4"> <div className="grid grid-cols-1 gap-y-4">
<Form.Field <Form.Field
control={form.control} control={form.control}
@@ -64,6 +64,7 @@ export const ProductCreateSchema = z
discountable: z.boolean(), discountable: z.boolean(),
type_id: z.string().optional(), type_id: z.string().optional(),
collection_id: z.string().optional(), collection_id: z.string().optional(),
shipping_profile_id: z.string(), // TODO: require min(1) when partial validation per tab is added
categories: z.array(z.string()), categories: z.array(z.string()),
tags: z.array(z.string()).optional(), tags: z.array(z.string()).optional(),
sales_channels: z sales_channels: z
@@ -145,6 +146,7 @@ export const PRODUCT_CREATE_FORM_DEFAULTS: Partial<
media: [], media: [],
categories: [], categories: [],
collection_id: "", collection_id: "",
shipping_profile_id: "",
description: "", description: "",
handle: "", handle: "",
height: "", height: "",
@@ -24,6 +24,7 @@ export const normalizeProductFormValues = (
: undefined, : undefined,
images, images,
collection_id: values.collection_id || undefined, collection_id: values.collection_id || undefined,
shipping_profile_id: values.shipping_profile_id,
categories: values.categories.map((id) => ({ id })), categories: values.categories.map((id) => ({ id })),
type_id: values.type_id || undefined, type_id: values.type_id || undefined,
handle: values.handle || undefined, handle: values.handle || undefined,
@@ -0,0 +1 @@
export * from "./product-shipping-profile-section"
@@ -0,0 +1,53 @@
import { PencilSquare, ShoppingBag } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { SidebarLink } from "../../../../../components/common/sidebar-link/sidebar-link"
import { ActionMenu } from "../../../../../components/common/action-menu"
type ProductShippingProfileSectionProps = {
product: HttpTypes.AdminProduct & {
shipping_profile: HttpTypes.AdminShippingProfile
}
}
export const ProductShippingProfileSection = ({
product,
}: ProductShippingProfileSectionProps) => {
const { t } = useTranslation()
const shippingProfile = product.shipping_profile
if (!shippingProfile) {
return null
}
return (
<Container className="p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("products.shippingProfile.header")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
to: "shipping-profile",
icon: <PencilSquare />,
},
],
},
]}
/>
</div>
<SidebarLink
to={`/settings/locations/shipping-profiles/${shippingProfile.id}`}
labelKey={shippingProfile.name}
descriptionKey={shippingProfile.type}
icon={<ShoppingBag />}
/>
</Container>
)
}
@@ -2,5 +2,5 @@ import { getLinkedFields } from "../../../extensions"
export const PRODUCT_DETAIL_FIELDS = getLinkedFields( export const PRODUCT_DETAIL_FIELDS = getLinkedFields(
"product", "product",
"*categories,-variants" "*categories,*shipping_profile,-variants"
) )
@@ -14,6 +14,7 @@ import { PRODUCT_DETAIL_FIELDS } from "./constants"
import { productLoader } from "./loader" import { productLoader } from "./loader"
import { useDashboardExtension } from "../../../extensions" import { useDashboardExtension } from "../../../extensions"
import { ProductShippingProfileSection } from "./components/product-shipping-profile-section"
export const ProductDetail = () => { export const ProductDetail = () => {
const initialData = useLoaderData() as Awaited< const initialData = useLoaderData() as Awaited<
@@ -71,6 +72,7 @@ export const ProductDetail = () => {
</TwoColumnPage.Main> </TwoColumnPage.Main>
<TwoColumnPage.Sidebar> <TwoColumnPage.Sidebar>
<ProductSalesChannelSection product={product} /> <ProductSalesChannelSection product={product} />
<ProductShippingProfileSection product={product} />
<ProductOrganizationSection product={product} /> <ProductOrganizationSection product={product} />
<ProductAttributeSection product={product} /> <ProductAttributeSection product={product} />
</TwoColumnPage.Sidebar> </TwoColumnPage.Sidebar>
@@ -0,0 +1 @@
export * from "./product-shipping-profile-form"
@@ -0,0 +1,122 @@
import { HttpTypes } from "@medusajs/types"
import { Button, toast } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import { Combobox } from "../../../../../components/inputs/combobox"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useExtendableForm } from "../../../../../extensions"
import { useUpdateProduct } from "../../../../../hooks/api/products"
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
import { sdk } from "../../../../../lib/client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
type ProductShippingProfileFormProps = {
product: HttpTypes.AdminProduct & {
shipping_profile?: HttpTypes.AdminShippingProfile
}
}
const ProductShippingProfileSchema = zod.object({
shipping_profile_id: zod.string(),
})
export const ProductShippingProfileForm = ({
product,
}: ProductShippingProfileFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const shippingProfiles = useComboboxData({
queryKey: ["shipping_profiles"],
queryFn: (params) => sdk.admin.shippingProfile.list(params),
getOptions: (data) =>
data.shipping_profiles.map((shippingProfile) => ({
label: shippingProfile.name,
value: shippingProfile.id,
})),
})
const form = useForm({
defaultValues: {
shipping_profile_id: product.shipping_profile?.id ?? "",
},
resolver: zodResolver(ProductShippingProfileSchema),
})
const { mutateAsync, isPending } = useUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
{
shipping_profile_id: data.shipping_profile_id,
},
{
onSuccess: ({ product }) => {
toast.success(
t("products.shippingProfile.edit.toasts.success", {
title: product.title,
})
)
handleSuccess()
},
onError: (error) => {
toast.error(error.message)
},
}
)
})
return (
<RouteDrawer.Form form={form}>
<KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col">
<RouteDrawer.Body>
<div className="flex h-full flex-col gap-y-4">
<Form.Field
control={form.control}
name="shipping_profile_id"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("products.fields.shipping_profile.label")}
</Form.Label>
<Form.Control>
<Combobox
{...field}
options={shippingProfiles.options}
searchValue={shippingProfiles.searchValue}
onSearchValueChange={
shippingProfiles.onSearchValueChange
}
fetchNextPage={shippingProfiles.fetchNextPage}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
{/* <FormExtensionZone fields={fields} form={form} /> */}
</div>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isPending}>
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</KeyboundForm>
</RouteDrawer.Form>
)
}
@@ -0,0 +1 @@
export { ProductShippingProfile as Component } from "./product-shipping-profile"
@@ -0,0 +1,34 @@
import { Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/modals"
import { useProduct } from "../../../hooks/api/products"
import { PRODUCT_DETAIL_FIELDS } from "../product-detail/constants"
import { ProductShippingProfileForm } from "./components/product-organization-form"
export const ProductShippingProfile = () => {
const { id } = useParams()
const { t } = useTranslation()
const { product, isLoading, isError, error } = useProduct(id!, {
fields: PRODUCT_DETAIL_FIELDS,
})
if (isError) {
throw error
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<RouteDrawer.Title asChild>
<Heading>{t("products.shippingProfile.edit.header")}</Heading>
</RouteDrawer.Title>
</RouteDrawer.Header>
{!isLoading && product && (
<ProductShippingProfileForm product={product} />
)}
</RouteDrawer>
)
}
@@ -82,10 +82,7 @@ export type CreateFulfillmentValidateOrderStepInput = {
*/ */
export const createFulfillmentValidateOrder = createStep( export const createFulfillmentValidateOrder = createStep(
"create-fulfillment-validate-order", "create-fulfillment-validate-order",
({ ({ order, inputItems }: CreateFulfillmentValidateOrderStepInput) => {
order,
inputItems,
}: CreateFulfillmentValidateOrderStepInput) => {
throwIfOrderIsCancelled({ order }) throwIfOrderIsCancelled({ order })
throwIfItemsDoesNotExistsInOrder({ order, inputItems }) throwIfItemsDoesNotExistsInOrder({ order, inputItems })
throwIfItemsAreNotGroupedByShippingRequirement({ order, inputItems }) throwIfItemsAreNotGroupedByShippingRequirement({ order, inputItems })
@@ -128,6 +125,7 @@ function prepareFulfillmentData({
id: string id: string
provider_id: string provider_id: string
service_zone: { fulfillment_set: { location?: { id: string } } } service_zone: { fulfillment_set: { location?: { id: string } } }
shipping_profile_id: string
} }
shippingMethod: { data?: Record<string, unknown> | null } shippingMethod: { data?: Record<string, unknown> | null }
reservations: ReservationItemDTO[] reservations: ReservationItemDTO[]
@@ -156,6 +154,18 @@ function prepareFulfillmentData({
const orderItem = orderItemsMap.get(i.id)! const orderItem = orderItemsMap.get(i.id)!
const reservation = reservationItemMap.get(i.id)! const reservation = reservationItemMap.get(i.id)!
if (
orderItem.requires_shipping &&
(orderItem as any).variant?.product &&
(orderItem as any).variant?.product.shipping_profile?.id !==
shippingOption.shipping_profile_id
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Shipping profile ${shippingOption.shipping_profile_id} does not match the shipping profile of the order item ${orderItem.id}`
)
}
return { return {
line_item_id: i.id, line_item_id: i.id,
inventory_item_id: reservation?.inventory_item_id, inventory_item_id: reservation?.inventory_item_id,
@@ -273,7 +283,8 @@ function prepareInventoryUpdate({
/** /**
* The details of the fulfillment to create, along with custom data that's passed to the workflow's hooks. * The details of the fulfillment to create, along with custom data that's passed to the workflow's hooks.
*/ */
export type CreateOrderFulfillmentWorkflowInput = OrderWorkflow.CreateOrderFulfillmentWorkflowInput & AdditionalData export type CreateOrderFulfillmentWorkflowInput =
OrderWorkflow.CreateOrderFulfillmentWorkflowInput & AdditionalData
export const createOrderFulfillmentWorkflowId = "create-order-fulfillment" export const createOrderFulfillmentWorkflowId = "create-order-fulfillment"
/** /**
@@ -309,9 +320,7 @@ export const createOrderFulfillmentWorkflowId = "create-order-fulfillment"
*/ */
export const createOrderFulfillmentWorkflow = createWorkflow( export const createOrderFulfillmentWorkflow = createWorkflow(
createOrderFulfillmentWorkflowId, createOrderFulfillmentWorkflowId,
( (input: WorkflowData<CreateOrderFulfillmentWorkflowInput>) => {
input: WorkflowData<CreateOrderFulfillmentWorkflowInput>
) => {
const order: OrderDTO = useRemoteQueryStep({ const order: OrderDTO = useRemoteQueryStep({
entry_point: "orders", entry_point: "orders",
fields: [ fields: [
@@ -323,12 +332,14 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
"items.variant.manage_inventory", "items.variant.manage_inventory",
"items.variant.allow_backorder", "items.variant.allow_backorder",
"items.variant.product.id", "items.variant.product.id",
"items.variant.product.shipping_profile.id",
"items.variant.weight", "items.variant.weight",
"items.variant.length", "items.variant.length",
"items.variant.height", "items.variant.height",
"items.variant.width", "items.variant.width",
"items.variant.material", "items.variant.material",
"shipping_address.*", "shipping_address.*",
"shipping_methods.id",
"shipping_methods.shipping_option_id", "shipping_methods.shipping_option_id",
"shipping_methods.data", "shipping_methods.data",
], ],
@@ -346,17 +357,29 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
}, {}) }, {})
}) })
const shippingMethod = transform(order, (data) => { const shippingOptionId = transform({ order, input }, (data) => {
return { data: data.shipping_methods?.[0]?.data } return (
data.input.shipping_option_id ??
data.order.shipping_methods?.[0]?.shipping_option_id
)
}) })
const shippingOptionId = transform(order, (data) => { const shippingMethod = transform({ order, shippingOptionId }, (data) => {
return data.shipping_methods?.[0]?.shipping_option_id return {
data: data.order.shipping_methods?.find(
(sm) => sm.shipping_option_id === data.shippingOptionId
)?.data,
}
}) })
const shippingOption = useRemoteQueryStep({ const shippingOption = useRemoteQueryStep({
entry_point: "shipping_options", entry_point: "shipping_options",
fields: ["id", "provider_id", "service_zone.fulfillment_set.location.id"], fields: [
"id",
"provider_id",
"service_zone.fulfillment_set.location.id",
"shipping_profile_id",
],
variables: { variables: {
id: shippingOptionId, id: shippingOptionId,
}, },
@@ -136,6 +136,11 @@ const normalizeProductForImport = (
return return
} }
if (normalizedKey.startsWith("shipping_profile_id")) {
response["shipping_profile_id"] = normalizedValue
return
}
if (normalizedKey.startsWith("product_category_")) { if (normalizedKey.startsWith("product_category_")) {
response["categories"] = [ response["categories"] = [
...(response["categories"] || []), ...(response["categories"] || []),
@@ -1,4 +1,8 @@
import { ProductTypes, SalesChannelTypes } from "@medusajs/framework/types" import {
ProductTypes,
SalesChannelTypes,
ShippingProfileDTO,
} from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils" import { MedusaError } from "@medusajs/framework/utils"
const basicFieldsToOmit = [ const basicFieldsToOmit = [
@@ -32,6 +36,7 @@ export const normalizeV1Products = (
productTypes: ProductTypes.ProductTypeDTO[] productTypes: ProductTypes.ProductTypeDTO[]
productCollections: ProductTypes.ProductCollectionDTO[] productCollections: ProductTypes.ProductCollectionDTO[]
salesChannels: SalesChannelTypes.SalesChannelDTO[] salesChannels: SalesChannelTypes.SalesChannelDTO[]
shippingProfiles: ShippingProfileDTO[]
} }
): object[] => { ): object[] => {
const productTypesMap = new Map( const productTypesMap = new Map(
@@ -43,6 +48,9 @@ export const normalizeV1Products = (
const salesChannelsMap = new Map( const salesChannelsMap = new Map(
supportingData.salesChannels.map((sc) => [sc.name, sc.id]) supportingData.salesChannels.map((sc) => [sc.name, sc.id])
) )
const shippingProfilesIds = new Set(
supportingData.shippingProfiles.map((sp) => sp.id)
)
return rawProducts.map((product) => { return rawProducts.map((product) => {
let finalRes = { let finalRes = {
@@ -140,6 +148,21 @@ export const normalizeV1Products = (
} }
} }
if (key.startsWith("Shipping Profile Id")) {
if (!value) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Shipping Profile Id is required when importing products"
)
}
if (!shippingProfilesIds.has(value)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Shipping profile: '${value}' does not exist`
)
}
}
if ( if (
key.startsWith("Product Category") && key.startsWith("Product Category") &&
(key.endsWith("Handle") || (key.endsWith("Handle") ||
@@ -1,4 +1,5 @@
import { import {
IFulfillmentModuleService,
IProductModuleService, IProductModuleService,
IRegionModuleService, IRegionModuleService,
ISalesChannelModuleService, ISalesChannelModuleService,
@@ -35,20 +36,25 @@ export const parseProductCsvStep = createStep(
Modules.SALES_CHANNEL Modules.SALES_CHANNEL
) )
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
Modules.FULFILLMENT
)
const csvProducts = convertCsvToJson(fileContent) const csvProducts = convertCsvToJson(fileContent)
const [productTypes, productCollections, salesChannels] = await Promise.all( const [productTypes, productCollections, salesChannels, shippingProfiles] =
[ await Promise.all([
productService.listProductTypes({}, {}), productService.listProductTypes({}, {}),
productService.listProductCollections({}, {}), productService.listProductCollections({}, {}),
salesChannelService.listSalesChannels({}, {}), salesChannelService.listSalesChannels({}, {}),
] fulfillmentService.listShippingProfiles({}, {}),
) ])
const v1Normalized = normalizeV1Products(csvProducts, { const v1Normalized = normalizeV1Products(csvProducts, {
productTypes, productTypes,
productCollections, productCollections,
salesChannels, salesChannels,
shippingProfiles,
}) })
// We use the handle to group products and variants correctly. // We use the handle to group products and variants correctly.
@@ -1,6 +1,7 @@
import { import {
AdditionalData, AdditionalData,
CreateProductWorkflowInputDTO, CreateProductWorkflowInputDTO,
LinkDefinition,
PricingTypes, PricingTypes,
ProductTypes, ProductTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
@@ -8,6 +9,7 @@ import {
ProductWorkflowEvents, ProductWorkflowEvents,
isPresent, isPresent,
MedusaError, MedusaError,
Modules,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
WorkflowData, WorkflowData,
@@ -17,7 +19,11 @@ import {
transform, transform,
createStep, createStep,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "../../common" import {
createRemoteLinkStep,
emitEventStep,
useQueryGraphStep,
} from "../../common"
import { associateProductsWithSalesChannelsStep } from "../../sales-channel" import { associateProductsWithSalesChannelsStep } from "../../sales-channel"
import { createProductsStep } from "../steps/create-products" import { createProductsStep } from "../steps/create-products"
import { createProductVariantsWorkflow } from "./create-product-variants" import { createProductVariantsWorkflow } from "./create-product-variants"
@@ -29,7 +35,12 @@ export interface ValidateProductInputStepInput {
/** /**
* The products to validate. * The products to validate.
*/ */
products: CreateProductWorkflowInputDTO[] products: Omit<CreateProductWorkflowInputDTO, "sales_channels">[]
/**
* The shipping profiles to validate.
*/
shippingProfiles: { id: string }[]
} }
const validateProductInputStepId = "validate-product-input" const validateProductInputStepId = "validate-product-input"
@@ -71,7 +82,7 @@ const validateProductInputStepId = "validate-product-input"
export const validateProductInputStep = createStep( export const validateProductInputStep = createStep(
validateProductInputStepId, validateProductInputStepId,
async (data: ValidateProductInputStepInput) => { async (data: ValidateProductInputStepInput) => {
const { products } = data const { products, shippingProfiles } = data
const missingOptionsProductTitles = products const missingOptionsProductTitles = products
.filter((product) => !product.options?.length) .filter((product) => !product.options?.length)
@@ -85,6 +96,25 @@ export const validateProductInputStep = createStep(
)}].` )}].`
) )
} }
const existingProfileIds = new Set(shippingProfiles.map((p) => p.id))
const missingShippingProfileProductTitles = products
.filter(
(product) =>
!product.shipping_profile_id ||
!existingProfileIds.has(product.shipping_profile_id)
)
.map((product) => product.title)
if (missingShippingProfileProductTitles.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Shipping profile is not provided for: [${missingShippingProfileProductTitles.join(
", "
)}].`
)
}
} }
) )
@@ -154,15 +184,34 @@ export const createProductsWorkflow = createWorkflow(
createProductsWorkflowId, createProductsWorkflowId,
(input: WorkflowData<CreateProductsWorkflowInput>) => { (input: WorkflowData<CreateProductsWorkflowInput>) => {
// Passing prices to the product module will fail, we want to keep them for after the product is created. // Passing prices to the product module will fail, we want to keep them for after the product is created.
const productWithoutExternalRelations = transform({ input }, (data) => const { products: productWithoutExternalRelations, shippingPorfileIds } =
data.input.products.map((p) => ({ transform({ input }, (data) => {
const shippingPorfileIds: string[] = []
const productsData = data.input.products.map((p) => {
if (p.shipping_profile_id) {
shippingPorfileIds.push(p.shipping_profile_id)
}
return {
...p, ...p,
sales_channels: undefined, sales_channels: undefined,
shipping_profile_id: undefined,
variants: undefined, variants: undefined,
})) }
) })
validateProductInputStep({ products: productWithoutExternalRelations }) return { products: productsData, shippingPorfileIds }
})
const { data: shippingProfiles } = useQueryGraphStep({
entity: "shipping_profile",
fields: ["id"],
filters: {
id: shippingPorfileIds,
},
})
validateProductInputStep({ products: input.products, shippingProfiles })
const createdProducts = createProductsStep(productWithoutExternalRelations) const createdProducts = createProductsStep(productWithoutExternalRelations)
@@ -182,6 +231,24 @@ export const createProductsWorkflow = createWorkflow(
associateProductsWithSalesChannelsStep({ links: salesChannelLinks }) associateProductsWithSalesChannelsStep({ links: salesChannelLinks })
const shippingProfileLinks = transform(
{ input, createdProducts },
(data) => {
return data.createdProducts.map((createdProduct, i) => {
return {
[Modules.PRODUCT]: {
product_id: createdProduct.id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id: data.input.products[i].shipping_profile_id,
},
}
})
}
)
createRemoteLinkStep(shippingProfileLinks as LinkDefinition[])
const variantsInput = transform({ input, createdProducts }, (data) => { const variantsInput = transform({ input, createdProducts }, (data) => {
// TODO: Move this to a unified place for all product workflow types // TODO: Move this to a unified place for all product workflow types
const productVariants: (ProductTypes.CreateProductVariantDTO & { const productVariants: (ProductTypes.CreateProductVariantDTO & {
@@ -47,6 +47,10 @@ export type UpdateProductsWorkflowInputSelector = {
* The variants to update. * The variants to update.
*/ */
variants?: UpdateProductVariantWorkflowInputDTO[] variants?: UpdateProductVariantWorkflowInputDTO[]
/**
* The shipping profile to set.
*/
shipping_profile_id?: string
} }
} & AdditionalData } & AdditionalData
@@ -66,6 +70,10 @@ export type UpdateProductsWorkflowInputProducts = {
* The variants to update. * The variants to update.
*/ */
variants?: UpdateProductVariantWorkflowInputDTO[] variants?: UpdateProductVariantWorkflowInputDTO[]
/**
* The shipping profile to set.
*/
shipping_profile_id?: string
})[] })[]
} & AdditionalData } & AdditionalData
@@ -90,6 +98,7 @@ function prepareUpdateProductInput({
products: input.products.map((p) => ({ products: input.products.map((p) => ({
...p, ...p,
sales_channels: undefined, sales_channels: undefined,
shipping_profile_id: undefined,
variants: p.variants?.map((v) => ({ variants: p.variants?.map((v) => ({
...v, ...v,
prices: undefined, prices: undefined,
@@ -103,6 +112,7 @@ function prepareUpdateProductInput({
update: { update: {
...input.update, ...input.update,
sales_channels: undefined, sales_channels: undefined,
shipping_profile_id: undefined,
variants: input.update?.variants?.map((v) => ({ variants: input.update?.variants?.map((v) => ({
...v, ...v,
prices: undefined, prices: undefined,
@@ -173,6 +183,44 @@ function prepareSalesChannelLinks({
return [] return []
} }
function prepareShippingProfileLinks({
input,
updatedProducts,
}: {
updatedProducts: ProductTypes.ProductDTO[]
input: UpdateProductWorkflowInput
}): Record<string, Record<string, any>>[] {
if ("products" in input) {
if (!input.products.length) {
return []
}
return input.products
.filter((p) => p.shipping_profile_id)
.map((p) => ({
[Modules.PRODUCT]: {
product_id: p.id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id: p.shipping_profile_id,
},
}))
}
if (input.selector && input.update?.shipping_profile_id) {
return updatedProducts.map((p) => ({
[Modules.PRODUCT]: {
product_id: p.id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id: input.update.shipping_profile_id,
},
}))
}
return []
}
function prepareVariantPrices({ function prepareVariantPrices({
input, input,
updatedProducts, updatedProducts,
@@ -243,6 +291,30 @@ function prepareToDeleteSalesChannelLinks({
})) }))
} }
function prepareToDeleteShippingProfileLinks({
currentShippingProfileLinks,
}: {
currentShippingProfileLinks: {
product_id: string
shipping_profile_id: string
}[]
}) {
if (!currentShippingProfileLinks.length) {
return []
}
return currentShippingProfileLinks.map(
({ product_id, shipping_profile_id }) => ({
[Modules.PRODUCT]: {
product_id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id,
},
})
)
}
export const updateProductsWorkflowId = "update-products" export const updateProductsWorkflowId = "update-products"
/** /**
* This workflow updates one or more products. It's used by the [Update Product Admin API Route](https://docs.medusajs.com/api/admin#products_postproductsid). * This workflow updates one or more products. It's used by the [Update Product Admin API Route](https://docs.medusajs.com/api/admin#products_postproductsid).
@@ -345,11 +417,20 @@ export const updateProductsWorkflow = createWorkflow(
const toUpdateInput = transform({ input }, prepareUpdateProductInput) const toUpdateInput = transform({ input }, prepareUpdateProductInput)
const updatedProducts = updateProductsStep(toUpdateInput) const updatedProducts = updateProductsStep(toUpdateInput)
const updatedPorductIds = transform({ updatedProducts }, (data) => {
return data.updatedProducts.map((p) => p.id)
})
const salesChannelLinks = transform( const salesChannelLinks = transform(
{ input, updatedProducts }, { input, updatedProducts },
prepareSalesChannelLinks prepareSalesChannelLinks
) )
const shippingProfileLinks = transform(
{ input, updatedProducts },
prepareShippingProfileLinks
)
const variantPrices = transform( const variantPrices = transform(
{ input, updatedProducts }, { input, updatedProducts },
prepareVariantPrices prepareVariantPrices
@@ -366,16 +447,33 @@ export const updateProductsWorkflow = createWorkflow(
variables: { filters: { product_id: productsWithSalesChannels } }, variables: { filters: { product_id: productsWithSalesChannels } },
}).config({ name: "get-current-sales-channel-links-step" }) }).config({ name: "get-current-sales-channel-links-step" })
const currentShippingProfileLinks = useRemoteQueryStep({
entry_point: "product_shipping_profile",
fields: ["product_id", "shipping_profile_id"],
variables: { filters: { product_id: updatedPorductIds } },
}).config({ name: "get-current-shipping-profile-links-step" })
const toDeleteSalesChannelLinks = transform( const toDeleteSalesChannelLinks = transform(
{ currentSalesChannelLinks }, { currentSalesChannelLinks },
prepareToDeleteSalesChannelLinks prepareToDeleteSalesChannelLinks
) )
const toDeleteShippingProfileLinks = transform(
{ currentShippingProfileLinks },
prepareToDeleteShippingProfileLinks
)
upsertVariantPricesWorkflow.runAsStep({ upsertVariantPricesWorkflow.runAsStep({
input: { variantPrices, previousVariantIds }, input: { variantPrices, previousVariantIds },
}) })
dismissRemoteLinkStep(toDeleteSalesChannelLinks) dismissRemoteLinkStep(toDeleteSalesChannelLinks).config({
name: "delete-sales-channel-links-step",
})
dismissRemoteLinkStep(toDeleteShippingProfileLinks).config({
name: "delete-shipping-profile-links-step",
})
const productIdEvents = transform( const productIdEvents = transform(
{ updatedProducts }, { updatedProducts },
@@ -387,7 +485,12 @@ export const updateProductsWorkflow = createWorkflow(
) )
parallelize( parallelize(
createRemoteLinkStep(salesChannelLinks), createRemoteLinkStep(salesChannelLinks).config({
name: "create-sales-channel-links-step",
}),
createRemoteLinkStep(shippingProfileLinks).config({
name: "create-shipping-profile-links-step",
}),
emitEventStep({ emitEventStep({
eventName: ProductWorkflowEvents.UPDATED, eventName: ProductWorkflowEvents.UPDATED,
data: productIdEvents, data: productIdEvents,
@@ -1,8 +1,31 @@
import { createWorkflow, WorkflowData } from "@medusajs/framework/workflows-sdk" import {
createStep,
createWorkflow,
WorkflowData,
} from "@medusajs/framework/workflows-sdk"
import { MedusaError, Modules } from "@medusajs/framework/utils"
import { deleteShippingProfilesStep } from "../steps" import { deleteShippingProfilesStep } from "../steps"
import { removeRemoteLinkStep } from "../../common" import { removeRemoteLinkStep, useQueryGraphStep } from "../../common"
import { Modules } from "@medusajs/framework/utils"
/**
* This step validates that the shipping profiles to delete are not linked to any products.
*/
const validateStepShippingProfileDelete = createStep(
"validate-step-shipping-profile-delete",
(data: { links: { product_id: string; shipping_profile_id: string }[] }) => {
const { links } = data
if (links.length > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot delete following shipping profiles because they are linked to products: ${links
.map((l) => l.product_id)
.join(", ")}`
)
}
}
)
/** /**
* The data to delete shipping profiles. * The data to delete shipping profiles.
@@ -19,6 +42,7 @@ export const deleteShippingProfileWorkflowId =
/** /**
* This workflow deletes one or more shipping profiles. It's used by the * This workflow deletes one or more shipping profiles. It's used by the
* [Delete Shipping Profile Admin API Route](https://docs.medusajs.com/api/admin#shipping-profiles_deleteshippingprofilesid). * [Delete Shipping Profile Admin API Route](https://docs.medusajs.com/api/admin#shipping-profiles_deleteshippingprofilesid).
* Shipping profiles that are linked to products cannot be deleted.
* *
* You can use this workflow within your customizations or your own custom workflows, allowing you to * You can use this workflow within your customizations or your own custom workflows, allowing you to
* delete shipping profiles within your custom flows. * delete shipping profiles within your custom flows.
@@ -38,6 +62,16 @@ export const deleteShippingProfileWorkflowId =
export const deleteShippingProfileWorkflow = createWorkflow( export const deleteShippingProfileWorkflow = createWorkflow(
deleteShippingProfileWorkflowId, deleteShippingProfileWorkflowId,
(input: WorkflowData<DeleteShippingProfilesWorkflowInput>) => { (input: WorkflowData<DeleteShippingProfilesWorkflowInput>) => {
const currentShippingProfileLinks = useQueryGraphStep({
entity: "product_shipping_profile",
fields: ["product_id", "shipping_profile_id"],
filters: { shipping_profile_id: input.ids },
})
validateStepShippingProfileDelete({
links: currentShippingProfileLinks.data,
})
deleteShippingProfilesStep(input.ids) deleteShippingProfilesStep(input.ids)
removeRemoteLinkStep({ removeRemoteLinkStep({
@@ -32,6 +32,11 @@ export interface AdminCreateOrderFulfillment {
* to fulfill the items from. * to fulfill the items from.
*/ */
location_id?: string location_id?: string
/**
* The ID of the shipping option to use for the fulfillment.
* Overrides the shipping option selected by the customer.
*/
shipping_option_id?: string
/** /**
* Whether to notify the customer about this change. * Whether to notify the customer about this change.
*/ */
@@ -210,6 +210,10 @@ export interface AdminCreateProduct {
* The ID of the product's collection. * The ID of the product's collection.
*/ */
collection_id?: string collection_id?: string
/**
* The ID of the product's shipping profile.
*/
shipping_profile_id: string
/** /**
* The product's categories. * The product's categories.
*/ */
@@ -363,7 +367,8 @@ export interface AdminUpdateProductVariant {
options?: Record<string, string> options?: Record<string, string>
} }
export interface AdminBatchUpdateProductVariant extends AdminUpdateProductVariant { export interface AdminBatchUpdateProductVariant
extends AdminUpdateProductVariant {
/** /**
* The ID of the variant to update. * The ID of the variant to update.
*/ */
@@ -459,6 +464,10 @@ export interface AdminUpdateProduct {
*/ */
id: string id: string
}[] }[]
/**
* The ID of the product's shipping profile.
*/
shipping_profile_id?: string
/** /**
* The product's weight. * The product's weight.
*/ */
@@ -62,6 +62,11 @@ export interface CreateOrderFulfillmentWorkflowInput {
*/ */
metadata?: Record<string, any> | null metadata?: Record<string, any> | null
/**
* Shipping option to be used for the fulfillment.
*/
shipping_option_id?: string
/** /**
* Whether the fulfillment should be shipped. * Whether the fulfillment should be shipped.
*/ */
@@ -33,6 +33,10 @@ export type CreateProductWorkflowInputDTO = Omit<
* The sales channels that the product is available in. * The sales channels that the product is available in.
*/ */
sales_channels?: { id: string }[] sales_channels?: { id: string }[]
/**
* The product's shipping profile.
*/
shipping_profile_id: string
/** /**
* The product's variants. * The product's variants.
*/ */
@@ -41,4 +45,5 @@ export type CreateProductWorkflowInputDTO = Omit<
export type UpdateProductWorkflowInputDTO = ProductTypes.UpsertProductDTO & { export type UpdateProductWorkflowInputDTO = ProductTypes.UpsertProductDTO & {
sales_channels?: { id: string }[] sales_channels?: { id: string }[]
shipping_profile_id?: string
} }
+6
View File
@@ -116,4 +116,10 @@ export const LINKS = {
Modules.FULFILLMENT, Modules.FULFILLMENT,
"fulfillment_id" "fulfillment_id"
), ),
ProductShippingProfile: composeLinkName(
Modules.PRODUCT,
"product_id",
Modules.FULFILLMENT,
"shipping_profile_id"
),
} }
@@ -75,6 +75,7 @@ export type AdminOrderCreateFulfillmentType = z.infer<
export const OrderCreateFulfillment = z.object({ export const OrderCreateFulfillment = z.object({
items: z.array(Item), items: z.array(Item),
location_id: z.string().nullish(), location_id: z.string().nullish(),
shipping_option_id: z.string().optional(),
no_notification: z.boolean().optional(), no_notification: z.boolean().optional(),
metadata: z.record(z.unknown()).nullish(), metadata: z.record(z.unknown()).nullish(),
}) })
@@ -232,6 +232,7 @@ export const CreateProduct = z
options: z.array(CreateProductOption).optional(), options: z.array(CreateProductOption).optional(),
variants: z.array(CreateProductVariant).optional(), variants: z.array(CreateProductVariant).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(), sales_channels: z.array(z.object({ id: z.string() })).optional(),
shipping_profile_id: z.string(),
weight: z.number().nullish(), weight: z.number().nullish(),
length: z.number().nullish(), length: z.number().nullish(),
height: z.number().nullish(), height: z.number().nullish(),
@@ -266,6 +267,7 @@ export const UpdateProduct = z
categories: z.array(IdAssociation).optional(), categories: z.array(IdAssociation).optional(),
tags: z.array(IdAssociation).optional(), tags: z.array(IdAssociation).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(), sales_channels: z.array(z.object({ id: z.string() })).optional(),
shipping_profile_id: z.string().optional(),
weight: z.number().nullish(), weight: z.number().nullish(),
length: z.number().nullish(), length: z.number().nullish(),
height: z.number().nullish(), height: z.number().nullish(),
@@ -0,0 +1,88 @@
import {
createRemoteLinkStep,
createShippingProfilesStep,
useQueryGraphStep,
} from "@medusajs/core-flows"
import { ExecArgs } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
import {
transform,
when,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
const assignProductsToShippingProfileWorkflow = createWorkflow(
"assign-products-to-shipping-profile",
() => {
const { data: shippingProfiles } = useQueryGraphStep({
entity: "shipping_profile",
fields: ["id", "name"],
}).config({ name: "get-shipping-profiles" })
const { data: products } = useQueryGraphStep({
entity: "product",
fields: ["id"],
}).config({ name: "get-products" })
const shippingProfileId = transform(
{ shippingProfiles },
({ shippingProfiles }) =>
shippingProfiles.find((sp) =>
sp.name.toLocaleLowerCase().includes("default")
)?.id ?? shippingProfiles[0]?.id
)
const createdShippingProfileId = when(
"create-shipping-profile",
{
shippingProfileId,
},
({ shippingProfileId }) => !shippingProfileId
).then(() => {
const createdShippingProfiles = createShippingProfilesStep([
{
name: "Default Shipping Profile",
type: "default",
},
])
return createdShippingProfiles[0].id
})
const links = transform(
{ products, shippingProfileId, createdShippingProfileId },
({ products, shippingProfileId, createdShippingProfileId }) => {
return products.map((product) => ({
[Modules.PRODUCT]: {
product_id: product.id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id: shippingProfileId ?? createdShippingProfileId,
},
}))
}
)
createRemoteLinkStep(links)
return new WorkflowResponse(void 0)
}
)
export default async function assignProductsToShippingProfile({
container,
}: ExecArgs) {
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
logger.info("Assigning products to shipping profile")
await assignProductsToShippingProfileWorkflow(container)
.run()
.then(() => {
logger.info("Products assigned to shipping profile")
})
.catch((e) => {
logger.error(e)
})
}
@@ -15,3 +15,4 @@ export * from "./readonly"
export * from "./region-payment-provider" export * from "./region-payment-provider"
export * from "./sales-channel-location" export * from "./sales-channel-location"
export * from "./shipping-option-price-set" export * from "./shipping-option-price-set"
export * from "./product-shipping-profile"
@@ -0,0 +1,72 @@
import { ModuleJoinerConfig } from "@medusajs/framework/types"
import { LINKS, Modules } from "@medusajs/framework/utils"
export const ProductShippingProfile: ModuleJoinerConfig = {
serviceName: LINKS.ProductShippingProfile,
isLink: true,
databaseConfig: {
tableName: "product_shipping_profile",
idPrefix: "prodsp",
},
alias: [
{
name: "product_shipping_profile",
},
{
name: "product_shipping_profiles",
},
],
primaryKeys: ["id", "product_id", "shipping_profile_id"],
relationships: [
{
serviceName: Modules.PRODUCT,
entity: "Product",
primaryKey: "id",
foreignKey: "product_id",
alias: "product",
args: {
methodSuffix: "Products",
},
},
{
serviceName: Modules.FULFILLMENT,
entity: "ShippingProfile",
primaryKey: "id",
foreignKey: "shipping_profile_id",
alias: "shipping_profile",
args: {
methodSuffix: "ShippingProfiles",
},
},
],
extends: [
{
serviceName: Modules.PRODUCT,
entity: "Product",
fieldAlias: {
shipping_profile: {
path: "shipping_profiles_link.shipping_profile",
isList: false,
},
},
relationship: {
serviceName: LINKS.ProductShippingProfile,
primaryKey: "product_id",
foreignKey: "id",
alias: "shipping_profiles_link",
isList: false,
},
},
{
serviceName: Modules.FULFILLMENT,
entity: "ShippingProfile",
relationship: {
serviceName: LINKS.ProductShippingProfile,
primaryKey: "shipping_profile_id",
foreignKey: "id",
alias: "products_link",
isList: true,
},
},
],
}