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ć
2025-01-27 12:00:20 +00:00
committed by GitHub
parent 3e81962503
commit 864d772e34
78 changed files with 3529 additions and 794 deletions
@@ -43,7 +43,8 @@ medusaIntegrationTestRunner({
salesChannel,
cart,
customer,
promotion
promotion,
shippingProfile
beforeAll(async () => {
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))
region = (
@@ -87,7 +96,7 @@ medusaIntegrationTestRunner({
).data.region
product = (
await api.post("/admin/products", medusaTshirtProduct, adminHeaders)
await api.post("/admin/products", { ...medusaTshirtProduct, shipping_profile_id: shippingProfile.id }, adminHeaders)
).data.product
salesChannel = (
@@ -34,6 +34,17 @@ medusaIntegrationTestRunner({
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
const region = (
await api.post(
"/admin/regions",
@@ -72,6 +83,7 @@ medusaIntegrationTestRunner({
{
title: "Test product",
options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -96,6 +108,7 @@ medusaIntegrationTestRunner({
{
title: "Extra product",
options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "my variant",
@@ -213,17 +226,6 @@ medusaIntegrationTestRunner({
customer_id: customer.id,
})
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
location = (
await api.post(
`/admin/stock-locations`,
@@ -16,6 +16,8 @@ medusaIntegrationTestRunner({
let baseProduct
let baseProduct1
let shippingProfile
beforeEach(async () => {
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
@@ -44,12 +46,21 @@ medusaIntegrationTestRunner({
)
).data.collection
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
baseProduct = (
await api.post(
"/admin/products",
{
title: "test-product",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
@@ -61,6 +72,7 @@ medusaIntegrationTestRunner({
{
title: "test-product1",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
@@ -41,6 +41,17 @@ medusaIntegrationTestRunner({
)
).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
const customer = (
await api.post(
"/admin/customers",
@@ -67,6 +78,7 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: "Test product",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }],
variants: [
{
@@ -91,6 +103,7 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: "Extra product",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }],
variants: [
{
@@ -203,17 +216,6 @@ medusaIntegrationTestRunner({
customer_id: customer.id,
})
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
location = (
await api.post(
`/admin/stock-locations`,
@@ -1,6 +1,7 @@
import {
AdminInventoryItem,
AdminProduct,
AdminShippingProfile,
AdminStockLocation,
MedusaContainer,
} from "@medusajs/types"
@@ -18,6 +19,8 @@ export async function createOrderSeeder({
additionalProducts,
stockChannelOverride,
inventoryItemOverride,
shippingProfileOverride,
withoutShipping,
}: {
api: any
container: MedusaContainer
@@ -26,6 +29,8 @@ export async function createOrderSeeder({
stockChannelOverride?: AdminStockLocation
additionalProducts?: { variant_id: string; quantity: number }[]
inventoryItemOverride?: AdminInventoryItem
shippingProfileOverride?: AdminShippingProfile
withoutShipping?: boolean
}) {
const publishableKey = await generatePublishableKey(container)
@@ -86,13 +91,15 @@ export async function createOrderSeeder({
adminHeaders
)
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: `test-${stockLocation.id}`, type: "default" },
adminHeaders
)
).data.shipping_profile
const shippingProfile =
shippingProfileOverride ??
(
await api.post(
`/admin/shipping-profiles`,
{ name: `test-${stockLocation.id}`, type: "default" },
adminHeaders
)
).data.shipping_profile
const product =
productOverride ??
@@ -101,6 +108,7 @@ export async function createOrderSeeder({
"/admin/products",
{
title: `Test fixture ${shippingProfile.id}`,
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
@@ -217,6 +225,14 @@ export async function createOrderSeeder({
)
).data.cart
if (!withoutShipping) {
await api.post(
`/store/carts/${cart.id}/shipping-methods`,
{ option_id: shippingOption.id },
storeHeaders
)
}
const paymentCollection = (
await api.post(
`/store/payment-collections`,
@@ -13,9 +13,19 @@ medusaIntegrationTestRunner({
let stockLocation1
let stockLocation2
let stockLocation3
let shippingProfile
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
stockLocation1 = (
await api.post(`/admin/stock-locations`, { name: "loc1" }, adminHeaders)
).data.stock_location
@@ -990,6 +1000,7 @@ medusaIntegrationTestRunner({
{
title: "product 1",
options: [{ title: "size", values: ["large"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "variant 1",
@@ -85,12 +85,24 @@ medusaIntegrationTestRunner({
)
).data.sales_channel
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
const product = (
await api.post(
"/admin/products",
{
title: "Test product",
options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -115,6 +127,7 @@ medusaIntegrationTestRunner({
{
title: "Extra product",
options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "my variant",
@@ -174,17 +187,6 @@ medusaIntegrationTestRunner({
customer_id: customer.id,
})
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
location = (
await api.post(
`/admin/stock-locations`,
@@ -11,13 +11,21 @@ jest.setTimeout(300000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
let order, seeder, inventoryItemOverride3, productOverride3
let order, seeder, inventoryItemOverride3, productOverride3, shippingProfile
beforeEach(async () => {
const container = getContainer()
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
await createAdminUser(dbConnection, adminHeaders, container)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
})
describe("POST /orders/:id", () => {
@@ -345,9 +353,19 @@ medusaIntegrationTestRunner({
describe("POST /orders/:id/cancel", () => {
beforeEach(async () => {
const inventoryItemOverride = (
await api.post(
`/admin/inventory-items`,
{ sku: "test-variant", requires_shipping: false },
adminHeaders
)
).data.inventory_item
seeder = await createOrderSeeder({
api,
container: getContainer(),
inventoryItemOverride,
withoutShipping: true,
})
order = seeder.order
@@ -536,8 +554,12 @@ medusaIntegrationTestRunner({
})
describe("POST /orders/:id/fulfillments", () => {
let productOverride4WithOverrideShippingProfile,
shippingProfileOverride,
stockChannelOverride
beforeEach(async () => {
const stockChannelOverride = (
stockChannelOverride = (
await api.post(
`/admin/stock-locations`,
{ name: "test location" },
@@ -558,6 +580,7 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: `Test fixture`,
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
@@ -605,6 +628,14 @@ medusaIntegrationTestRunner({
)
).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(
`/admin/inventory-items/${inventoryItemOverride2.id}/location-levels`,
{
@@ -623,11 +654,21 @@ medusaIntegrationTestRunner({
adminHeaders
)
await api.post(
`/admin/inventory-items/${inventoryItemOverride4RequiresShipping.id}/location-levels`,
{
location_id: stockChannelOverride.id,
stocked_quantity: 10,
},
adminHeaders
)
const productOverride2 = (
await api.post(
"/admin/products",
{
title: `Test fixture 2`,
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
@@ -664,6 +705,7 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: `Test fixture 3`,
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
@@ -695,6 +737,52 @@ medusaIntegrationTestRunner({
)
).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({
api,
container: getContainer(),
@@ -702,9 +790,15 @@ medusaIntegrationTestRunner({
additionalProducts: [
{ variant_id: productOverride2.variants[0].id, quantity: 1 },
{ variant_id: productOverride3.variants[0].id, quantity: 3 },
{
variant_id:
productOverride4WithOverrideShippingProfile.variants[0].id,
quantity: 1,
},
],
stockChannelOverride,
inventoryItemOverride,
shippingProfileOverride: shippingProfile,
})
order = seeder.order
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 () => {
const {
response: { data },
@@ -24,7 +24,21 @@ medusaIntegrationTestRunner({
await setupTaxStructure(container.resolve(Modules.TAX))
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
shippingProfile = (
@@ -24,6 +24,7 @@ medusaIntegrationTestRunner({
let region
let product
let cart
let shippingProfile
beforeEach(async () => {
region = (
@@ -34,12 +35,21 @@ medusaIntegrationTestRunner({
)
).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product = (
await api.post(
"/admin/products",
getProductFixture({
title: "test",
status: "published",
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -38,7 +38,22 @@ medusaIntegrationTestRunner({
beforeEach(async () => {
container = getContainer()
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
await api.post(
@@ -18,6 +18,7 @@ medusaIntegrationTestRunner({
let region1
let product1
let customerGroup1
let shippingProfile
beforeEach(async () => {
const container = getContainer()
@@ -34,10 +35,21 @@ medusaIntegrationTestRunner({
)
).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product1 = (
await api.post(
"/admin/products",
getProductFixture({ title: "Test product" }),
getProductFixture({
title: "Test product",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
).data.product
@@ -18,9 +18,19 @@ medusaIntegrationTestRunner({
let productCategoryChild2
let productCategoryChild3
let shippingProfile
beforeEach(async () => {
const appContainer = getContainer()
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", () => {
@@ -1349,6 +1359,7 @@ medusaIntegrationTestRunner({
title: "product 1",
options: [{ title: "size", values: ["x", "l"] }],
categories: [{ id: productCategory.id }],
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
@@ -1358,6 +1369,7 @@ medusaIntegrationTestRunner({
{
title: "product 2",
options: [{ title: "color", values: ["r", "g"] }],
shipping_profile_id: shippingProfile.id,
},
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
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
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,,
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,,
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,,,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
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
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;;
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;;
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;;;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
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,,
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,,,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
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
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,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
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
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,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
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,,
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,,,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
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
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
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
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,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
,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-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
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 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,,existing-variant-id,Test variant changed,test-sku-4,test-barcode-4,10,FALSE,TRUE,,,,,,,,,,,Size,Large,,,test-image.png
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,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,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,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,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,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>")
fixturesContent = fixturesContent.replace(dateRegex, "<DATE>")
fixturesContent = fixturesContent.replace(/,Shipping Profile Id*/g, "")
fixturesContent = fixturesContent.replace(/,import-shipping-profile*/g, "")
expect(fileContent).toEqual(fixturesContent)
}
@@ -56,6 +59,7 @@ medusaIntegrationTestRunner({
let baseTag1
let baseTag2
let newTag
let shippingProfile
let eventBus: IEventBusModuleService
beforeAll(async () => {
@@ -92,6 +96,14 @@ medusaIntegrationTestRunner({
)
).data.collection
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
baseType = (
await api.post(
"/admin/product-types",
@@ -130,6 +142,7 @@ medusaIntegrationTestRunner({
getProductFixture({
title: "Base product",
description: "test-product-description\ntest line 2",
shipping_profile_id: shippingProfile.id,
collection_id: baseCollection.id,
type_id: baseType.id,
categories: [{ id: baseCategory.id }],
@@ -191,6 +204,7 @@ medusaIntegrationTestRunner({
status: "proposed",
tags: [{ id: newTag.id }],
type_id: baseType.id,
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -281,6 +295,7 @@ medusaIntegrationTestRunner({
"/admin/products",
getProductFixture({
title: "Product with prices",
shipping_profile_id: shippingProfile.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
variants: [
{
@@ -40,6 +40,7 @@ medusaIntegrationTestRunner({
let baseTag2
let baseTag3
let newTag
let shippingProfile
let eventBus: IEventBusModuleService
beforeAll(async () => {
@@ -84,12 +85,21 @@ medusaIntegrationTestRunner({
)
).data.product_tag
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
baseProduct = (
await api.post(
"/admin/products",
getProductFixture({
title: "Base product",
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -150,6 +160,11 @@ medusaIntegrationTestRunner({
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({
name: "test.csv",
content: fileContent,
@@ -400,6 +415,11 @@ medusaIntegrationTestRunner({
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
fileContent = fileContent.replace(/pcat_\w*\d*/g, baseCategory.id)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({
name: "test.csv",
content: fileContent,
@@ -440,6 +460,11 @@ medusaIntegrationTestRunner({
{ encoding: "utf-8" }
)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({
name: "test.csv",
content: fileContent,
@@ -467,6 +492,11 @@ medusaIntegrationTestRunner({
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({
name: "test.csv",
content: fileContent,
@@ -517,6 +547,11 @@ medusaIntegrationTestRunner({
fileContent = fileContent.replace(/pcol_\w*\d*/g, baseCollection.id)
fileContent = fileContent.replace(/ptyp_\w*\d*/g, baseType.id)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({
name: "test.csv",
content: fileContent,
@@ -582,6 +617,11 @@ medusaIntegrationTestRunner({
baseCollection.handle
)
fileContent = fileContent.replace(
/import-shipping-profile*/g,
shippingProfile.id
)
const { form, meta } = getUploadReq({
name: "test.csv",
content: fileContent,
@@ -22,6 +22,8 @@ medusaIntegrationTestRunner({
let baseTag2
let newTag
let shippingProfile
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
@@ -65,6 +67,14 @@ medusaIntegrationTestRunner({
)
).data.product_tag
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
baseProduct = (
await api.post(
"/admin/products",
@@ -74,6 +84,7 @@ medusaIntegrationTestRunner({
// BREAKING: Type input changed from {type: {value: string}} to {type_id: string}
type_id: baseType.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
shipping_profile_id: shippingProfile.id,
images: [
{
url: "image-one",
@@ -95,6 +106,7 @@ medusaIntegrationTestRunner({
status: "proposed",
tags: [{ id: newTag.id }],
type_id: baseType.id,
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -108,6 +120,7 @@ medusaIntegrationTestRunner({
status: "published",
collection_id: publishedCollection.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -115,7 +128,10 @@ medusaIntegrationTestRunner({
deletedProduct = (
await api.post(
"/admin/products",
getProductFixture({ title: "Deleted product" }),
getProductFixture({
title: "Deleted product",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
).data.product
@@ -605,6 +621,7 @@ medusaIntegrationTestRunner({
is_giftcard: true,
description: "test-giftcard-description",
options: [{ title: "Denominations", values: ["100"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -693,6 +710,7 @@ medusaIntegrationTestRunner({
is_giftcard: true,
description: "test-giftcard-description",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -912,6 +930,7 @@ medusaIntegrationTestRunner({
getProductFixture({
title: "Test saleschannel",
sales_channels: [{ id: salesChannel.id }],
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -1098,6 +1117,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Custom inventory 1",
@@ -1141,6 +1161,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Custom inventory 1",
@@ -1202,6 +1223,7 @@ medusaIntegrationTestRunner({
collection_id: baseCollection.id,
type_id: baseType.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
@@ -1341,6 +1363,7 @@ medusaIntegrationTestRunner({
{
title: "Test create",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Price with rules",
@@ -1394,6 +1417,7 @@ medusaIntegrationTestRunner({
collection_id: baseCollection.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
options: [{ title: "size", values: ["large"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -1425,7 +1449,8 @@ medusaIntegrationTestRunner({
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
collection_id: baseCollection.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: [
{
title: "Test variant 1",
@@ -1477,6 +1502,7 @@ medusaIntegrationTestRunner({
is_giftcard: true,
description: "test-giftcard-description",
options: [{ title: "size", values: ["large"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -1637,6 +1663,7 @@ medusaIntegrationTestRunner({
it("updates product variants (update price on existing variant, create new variant)", async () => {
const payload = {
shipping_profile_id: shippingProfile.id,
variants: [
{
id: baseProduct.variants[0].id,
@@ -1815,6 +1842,7 @@ medusaIntegrationTestRunner({
"/admin/products",
getProductFixture({
title: "Test metadata",
shipping_profile_id: shippingProfile.id,
metadata: {
"test-key": "test-value",
"test-key-2": "test-value-2",
@@ -1877,6 +1905,7 @@ medusaIntegrationTestRunner({
getProductFixture({
title: "Test saleschannel",
sales_channels: [{ id: salesChannel1.id }],
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -2045,7 +2074,10 @@ medusaIntegrationTestRunner({
const plainProduct = (
await api.post(
"/admin/products",
{ title: "Test variant order" },
{
title: "Test variant order",
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
).data.product
@@ -2152,6 +2184,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Custom inventory 1",
@@ -2265,6 +2298,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Custom inventory 1",
@@ -2316,6 +2350,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Custom inventory 1",
@@ -2490,6 +2525,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Custom inventory 1",
@@ -2520,6 +2556,7 @@ medusaIntegrationTestRunner({
title: "Test product - 2",
handle: "test-2",
options: [{ title: "size", values: ["l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "W/ shared inventory item",
@@ -2604,6 +2641,7 @@ medusaIntegrationTestRunner({
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Custom inventory 1",
@@ -2740,6 +2778,7 @@ medusaIntegrationTestRunner({
title: baseProduct.title,
handle: baseProduct.handle,
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -2772,6 +2811,7 @@ medusaIntegrationTestRunner({
handle: baseProduct.handle,
description: "test-product-description",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -2948,6 +2988,7 @@ medusaIntegrationTestRunner({
const createPayload = getProductFixture({
title: "Test batch create",
handle: "test-batch-create",
shipping_profile_id: shippingProfile.id,
})
const updatePayload = {
@@ -3006,6 +3047,7 @@ medusaIntegrationTestRunner({
const productWithMultipleVariants = getProductFixture({
title: "Test batch variants",
handle: "test-batch-variants",
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Variant 1",
@@ -11,7 +11,7 @@ medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
let baseProduct
let baseRegion
let shippingProfile
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
// BREAKING: Creating a region no longer takes tax_rate, payment_providers, fulfillment_providers, countriesr
@@ -26,11 +26,20 @@ medusaIntegrationTestRunner({
)
).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
baseProduct = (
await api.post(
"/admin/products",
getProductFixture({
title: "Base product",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -67,6 +76,7 @@ medusaIntegrationTestRunner({
{ title: "First variant", prices: [] },
{ title: "Second variant", prices: [] },
],
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -801,6 +811,7 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: "product 1",
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "medium", "small"] },
],
@@ -41,6 +41,7 @@ medusaIntegrationTestRunner({
let publishableKey
let storeHeadersWithCustomer
let customer
let shippingProfile
const createProducts = async (data) => {
const response = await api.post(
@@ -135,6 +136,14 @@ medusaIntegrationTestRunner({
adminHeaders
)
).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
})
describe("Get products based on publishable key", () => {
@@ -145,7 +154,11 @@ medusaIntegrationTestRunner({
product1 = (
await api.post(
"/admin/products",
getProductFixture({ title: "test1", status: "published" }),
getProductFixture({
title: "test1",
status: "published",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
).data.product
@@ -153,7 +166,11 @@ medusaIntegrationTestRunner({
product2 = (
await api.post(
"/admin/products",
getProductFixture({ title: "test2", status: "published" }),
getProductFixture({
title: "test2",
status: "published",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
).data.product
@@ -161,7 +178,11 @@ medusaIntegrationTestRunner({
product3 = (
await api.post(
"/admin/products",
getProductFixture({ title: "test3", status: "published" }),
getProductFixture({
title: "test3",
status: "published",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
).data.product
@@ -500,6 +521,7 @@ medusaIntegrationTestRunner({
title: "test product 1",
collection_id: collection.id,
status: ProductStatus.PUBLISHED,
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
@@ -538,6 +560,7 @@ medusaIntegrationTestRunner({
;[product2, [variant2]] = await createProducts({
title: "test product 2 uniquely",
status: ProductStatus.PUBLISHED,
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "material", values: ["cotton", "polyester"] },
@@ -557,6 +580,7 @@ medusaIntegrationTestRunner({
;[product3, [variant3]] = await createProducts({
title: "product not in price list",
status: ProductStatus.PUBLISHED,
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }],
variants: [
{ title: "test variant 3", prices: [], options: { size: "large" } },
@@ -565,6 +589,7 @@ medusaIntegrationTestRunner({
;[product4, [variant4]] = await createProducts({
title: "draft product",
status: ProductStatus.DRAFT,
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }],
variants: [
{ title: "test variant 4", prices: [], options: { size: "large" } },
@@ -1727,6 +1752,7 @@ medusaIntegrationTestRunner({
;[product, [variant]] = await createProducts({
title: "test product 1",
status: ProductStatus.PUBLISHED,
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large"] }],
variants: [
{
@@ -2206,6 +2232,7 @@ medusaIntegrationTestRunner({
getProductFixture({
title: "test1",
status: "published",
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test taxes",
@@ -2239,6 +2266,7 @@ medusaIntegrationTestRunner({
getProductFixture({
title: "test2",
status: "published",
shipping_profile_id: shippingProfile.id,
}),
adminHeaders
)
@@ -56,6 +56,7 @@ medusaIntegrationTestRunner({
let appContainer
let promotion
let standardPromotion
let shippingProfile
const promotionRule = {
operator: "eq",
@@ -90,6 +91,14 @@ medusaIntegrationTestRunner({
adminHeaders
)
).data.promotion
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
})
describe("GET /admin/promotions/:id", () => {
@@ -512,7 +521,10 @@ medusaIntegrationTestRunner({
const product = (
await api.post(
"/admin/products",
medusaTshirtProduct,
{
...medusaTshirtProduct,
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
).data.product
@@ -1494,6 +1506,7 @@ medusaIntegrationTestRunner({
{
title: "Test product 1",
options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
@@ -1505,6 +1518,7 @@ medusaIntegrationTestRunner({
{
title: "Test product 2",
options: [{ title: "size", values: ["large", "small"] }],
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
@@ -25,12 +25,24 @@ medusaIntegrationTestRunner({
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
const product = (
await api.post(
"/admin/products",
{
title: "Test product",
options: [{ title: "size", values: ["x", "l"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -149,17 +161,6 @@ medusaIntegrationTestRunner({
],
})
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{
name: "Test",
type: "default",
},
adminHeaders
)
).data.shipping_profile
location = (
await api.post(
`/admin/stock-locations`,
@@ -354,11 +354,20 @@ medusaIntegrationTestRunner({
// to: /admin/sales-channels/:id/products
let product
beforeEach(async () => {
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product = (
await api.post(
"/admin/products",
{
title: "test name",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large"] }],
},
adminHeaders
@@ -58,6 +58,14 @@ medusaIntegrationTestRunner({
)
).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
salesChannel = (
await api.post(
"/admin/sales-channels",
@@ -71,6 +79,7 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: "Test fixture",
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
@@ -114,14 +123,6 @@ medusaIntegrationTestRunner({
adminHeaders
)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const fulfillmentSets = (
await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
@@ -57,6 +57,14 @@ medusaIntegrationTestRunner({
)
).data.region
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
salesChannel = (
await api.post(
"/admin/sales-channels",
@@ -74,6 +82,7 @@ medusaIntegrationTestRunner({
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
],
shipping_profile_id: shippingProfile.id,
variants: [
{
title: "Test variant",
@@ -113,14 +122,6 @@ medusaIntegrationTestRunner({
adminHeaders
)
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const fulfillmentSets = (
await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`,
@@ -658,6 +658,7 @@ medusaIntegrationTestRunner({
describe("POST /store/carts/:id/line-items", () => {
let region
const productData = {
title: "Medusa 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 () => {
const shippingProfile =
await fulfillmentModule.createShippingProfiles({
name: "Test",
type: "default",
})
const product = (
await api.post(`/admin/products`, productData, adminHeaders)
await api.post(
`/admin/products`,
{
...productData,
shipping_profile_id: shippingProfile.id,
},
adminHeaders
)
).data.product
const cart = (
@@ -1203,6 +1217,7 @@ medusaIntegrationTestRunner({
"/admin/products",
{
title: "Test fixture",
shipping_profile_id: shippingProfile.id,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["green"] },
@@ -30,11 +30,20 @@ medusaIntegrationTestRunner({
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product = (
await api.post(
"/admin/products",
{
title: "product 1",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["x", "l"] }],
variants: [
{
@@ -31,9 +31,18 @@ medusaIntegrationTestRunner({
describe("Index engine", () => {
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 = {
title: "Test Giftcard",
is_giftcard: true,
shipping_profile_id: shippingProfile.id,
description: "test-giftcard-description",
options: [{ title: "Denominations", values: ["100"] }],
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 () => {
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const payload = {
title: "Test Giftcard",
is_giftcard: true,
description: "test-giftcard-description",
shipping_profile_id: shippingProfile.id,
options: [{ title: "Denominations", values: ["100"] }],
variants: new Array(10).fill(0).map((_, 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 () => {
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) => ({
title: "Test Giftcard-" + a,
is_giftcard: true,
shipping_profile_id: shippingProfile.id,
description: "test-giftcard-description" + a,
options: [{ title: "Denominations", values: ["100"] }],
variants: new Array(10).fill(0).map((_, i) => ({
@@ -209,9 +209,18 @@ medusaIntegrationTestRunner({
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, appContainer)
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
const payload = {
title: "Test Giftcard",
is_giftcard: true,
shipping_profile_id: shippingProfile.id,
description: "test-giftcard-description",
options: [{ title: "Denominations", values: ["100"] }],
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([
{
[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([
{
[Modules.STOCK_LOCATION]: {
@@ -4,7 +4,10 @@ import {
batchProductVariantsWorkflow,
batchProductVariantsWorkflowId,
} from "@medusajs/core-flows"
import { IProductModuleService } from "@medusajs/types"
import {
IFulfillmentModuleService,
IProductModuleService,
} from "@medusajs/types"
import { Modules } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
@@ -17,9 +20,20 @@ medusaIntegrationTestRunner({
let appContainer
let service: IProductModuleService
let fulfullmentService: IFulfillmentModuleService
let shippingProfile
beforeAll(async () => {
appContainer = getContainer()
service = appContainer.resolve(Modules.PRODUCT)
fulfullmentService = appContainer.resolve(Modules.FULFILLMENT)
})
beforeEach(async () => {
shippingProfile = await fulfullmentService.createShippingProfiles({
name: "Test",
type: "default",
})
})
describe("batchProductWorkflow", () => {
@@ -43,7 +57,8 @@ medusaIntegrationTestRunner({
create: [
{
title: "test3",
options: [{ title: "size", options: ["x"] }],
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["x"] }],
},
],
update: [{ id: product1.id, title: "test1-updated" }],
@@ -94,6 +109,7 @@ medusaIntegrationTestRunner({
{
title: "test1",
options: [{ title: "size", values: ["x", "l", "m"] }],
shipping_profile_id: shippingProfile.id,
variants: [
{
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",
"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": {
"label": "Type"
},
@@ -681,6 +685,20 @@
"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": {
"delete": {
"success": {
@@ -1249,6 +1267,8 @@
"fulfillment": {
"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.",
"differentOptionSelected": "The selected shipping option is different from the one selected by the customer.",
"disabledItemTooltip": "The shipping option you have selected dont allow fulfillment of this item",
"unfulfilledItems": "Unfulfilled Items",
"statusLabel": "Fulfillment status",
"statusTitle": "Fulfillment Status",
@@ -1269,7 +1289,9 @@
"error": {
"wrongQuantity": "Only one item is available for fulfillment",
"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": {
"notFulfilled": "Not fulfilled",
@@ -2749,7 +2771,9 @@
"added": "Added",
"removed": "Removed",
"from": "From",
"to": "To"
"to": "To",
"beaware": "Be aware",
"loading": "Loading"
},
"fields": {
"amount": "Amount",
@@ -102,6 +102,13 @@ export const RouteMap: RouteObject[] = [
lazy: () =>
import("../../routes/products/product-organization"),
},
{
path: "shipping-profile",
lazy: () =>
import(
"../../routes/products/product-shipping-profile"
),
},
{
path: "media",
lazy: () =>
@@ -13,6 +13,7 @@ import { IconAvatar } from "../../../components/common/icon-avatar"
import { TwoColumnPage } from "../../../components/layout/pages"
import { useDashboardExtension } from "../../../extensions"
import { LocationListHeader } from "./components/location-list-header"
import { SidebarLink } from "../../../components/common/sidebar-link/sidebar-link"
export function LocationList() {
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 { t } = useTranslation()
@@ -2,7 +2,6 @@ import { z } from "zod"
export const CreateFulfillmentSchema = z.object({
quantity: z.record(z.string(), z.number()),
location_id: z.string(),
shipping_option_id: z.string().optional(),
send_notification: z.boolean().optional(),
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
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 { useForm, useWatch } from "react-hook-form"
@@ -19,7 +19,10 @@ import { useStockLocations } from "../../../../../hooks/api/stock-locations"
import { getFulfillableQuantity } from "../../../../../lib/order-item"
import { CreateFulfillmentSchema } from "./constants"
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
import { useReservationItems } from "../../../../../hooks/api"
import {
useReservationItems,
useShippingOptions,
} from "../../../../../hooks/api"
type OrderCreateFulfillmentFormProps = {
order: AdminOrder
@@ -56,30 +59,86 @@ export function OrderCreateFulfillmentForm({
const form = useForm<zod.infer<typeof CreateFulfillmentSchema>>({
defaultValues: {
quantity: fulfillableItems.reduce((acc, item) => {
acc[item.id] = getFulfillableQuantity(item)
return acc
}, {} as Record<string, number>),
quantity: fulfillableItems.reduce(
(acc, item) => {
acc[item.id] = getFulfillableQuantity(item)
return acc
},
{} as Record<string, number>
),
send_notification: !order.no_notification,
},
resolver: zodResolver(CreateFulfillmentSchema),
})
const selectedLocationId = useWatch({
name: "location_id",
control: form.control,
})
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) => {
try {
await createOrderFulfillment({
location_id: data.location_id,
// shipping_option_id: data.shipping_option_id,
no_notification: !data.send_notification,
items: Object.entries(data.quantity)
.filter(([, value]) => !!value)
.map(([id, quantity]) => ({
id,
quantity,
})),
const selectedShippingOption = shipping_options.find(
(o) => o.id === shippingOptionId
)
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,
items: Object.entries(data.quantity)
.filter(
([id, value]) =>
!!value && itemShippingProfileMap[id] === selectedShippingProfileId
)
.map(([id, quantity]) => ({
id,
quantity,
})),
}
try {
await createOrderFulfillment(payload)
toast.success(t("orders.fulfillment.toast.created"))
handleSuccess(`/orders/${order.id}`)
@@ -89,15 +148,28 @@ export function OrderCreateFulfillmentForm({
})
useEffect(() => {
if (stock_locations?.length) {
form.setValue("location_id", stock_locations[0].id)
}
}, [stock_locations?.length])
if (stock_locations?.length && shipping_options?.length) {
const initialShippingOptionId =
order.shipping_methods?.[0]?.shipping_option_id
const selectedLocationId = useWatch({
name: "location_id",
control: form.control,
})
if (initialShippingOptionId) {
const shippingOption = shipping_options.find(
(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(
(item) =>
@@ -124,14 +196,21 @@ export function OrderCreateFulfillmentForm({
})
}
const quantityMap = itemsToFulfill.reduce((acc, item) => {
acc[item.id] = getFulfillableQuantity(item as OrderLineItemDTO)
return acc
}, {} as Record<string, number>)
const quantityMap = itemsToFulfill.reduce(
(acc, item) => {
acc[item.id] = getFulfillableQuantity(item as OrderLineItemDTO)
return acc
},
{} as Record<string, number>
)
form.setValue("quantity", quantityMap)
}, [...fulfilledQuantityArray, requiresShipping])
const differentOptionSelected =
shippingOptionId &&
order.shipping_methods?.[0]?.shipping_option_id !== shippingOptionId
return (
<RouteFocusModal.Form form={form}>
<KeyboundForm
@@ -185,48 +264,69 @@ export function OrderCreateFulfillmentForm({
/>
</div>
{/* <div className="py-8">*/}
{/* <Form.Field*/}
{/* control={form.control}*/}
{/* name="shipping_option_id"*/}
{/* render={({ field: { onChange, ref, ...field } }) => {*/}
{/* return (*/}
{/* <Form.Item>*/}
{/* <div className="flex flex-col gap-2 xl:flex-row xl:items-center">*/}
{/* <div className="flex-1">*/}
{/* <Form.Label>*/}
{/* {t("fields.shippingMethod")}*/}
{/* </Form.Label>*/}
{/* <Form.Hint>*/}
{/* {t("orders.fulfillment.methodDescription")}*/}
{/* </Form.Hint>*/}
{/* </div>*/}
{/* <div className="flex-1">*/}
{/* <Form.Control>*/}
{/* <Select onValueChange={onChange} {...field}>*/}
{/* <Select.Trigger*/}
{/* className="bg-ui-bg-base"*/}
{/* ref={ref}*/}
{/* >*/}
{/* <Select.Value />*/}
{/* </Select.Trigger>*/}
{/* <Select.Content>*/}
{/* {shipping_options.map((o) => (*/}
{/* <Select.Item key={o.id} value={o.id}>*/}
{/* {o.name}*/}
{/* </Select.Item>*/}
{/* ))}*/}
{/* </Select.Content>*/}
{/* </Select>*/}
{/* </Form.Control>*/}
{/* </div>*/}
{/* </div>*/}
{/* <Form.ErrorMessage />*/}
{/* </Form.Item>*/}
{/* )*/}
{/* }}*/}
{/* />*/}
{/* </div>*/}
<div className="py-8">
<Form.Field
control={form.control}
name="shipping_option_id"
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<div className="flex flex-col gap-2 xl:flex-row xl:items-center">
<div className="flex-1">
<Form.Label>
{t("fields.shippingMethod")}
</Form.Label>
<Form.Hint>
{t("orders.fulfillment.methodDescription")}
</Form.Hint>
</div>
<div className="flex-1">
<Form.Control>
<Select
onValueChange={onChange}
{...field}
disabled={!selectedLocationId}
>
<Select.Trigger
className="bg-ui-bg-base"
ref={ref}
>
{isShippingOptionsLoading ? (
<span className="text-right">
{t("labels.loading")}...
</span>
) : (
<Select.Value />
)}
</Select.Trigger>
<Select.Content>
{shipping_options.map((o) => (
<Select.Item key={o.id} value={o.id}>
{o.name}
</Select.Item>
))}
</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>
<Form.Item className="mt-8">
<Form.Label>
@@ -238,12 +338,19 @@ export function OrderCreateFulfillmentForm({
<div className="flex flex-col gap-y-1">
{fulfillableItems.map((item) => {
const isShippingProfileMatching =
shipping_options.find(
(o) => o.id === shippingOptionId
)?.shipping_profile_id ===
item.variant?.product?.shipping_profile?.id
return (
<OrderCreateFulfillmentItem
key={item.id}
form={form}
item={item}
locationId={selectedLocationId}
disabled={isShippingProfileMatching}
itemReservedQuantitiesMap={
itemReservedQuantitiesMap
}
@@ -305,7 +412,12 @@ export function OrderCreateFulfillmentForm({
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isMutating}>
<Button
size="small"
type="submit"
isLoading={isMutating}
disabled={!shippingOptionId}
>
{t("orders.fulfillment.create")}
</Button>
</div>
@@ -1,7 +1,7 @@
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
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 { HttpTypes } from "@medusajs/types"
@@ -10,6 +10,7 @@ import { Thumbnail } from "../../../../../components/common/thumbnail/index"
import { useProductVariant } from "../../../../../hooks/api/products"
import { getFulfillableQuantity } from "../../../../../lib/order-item"
import { CreateFulfillmentSchema } from "./constants"
import { InformationCircleSolid } from "@medusajs/icons"
type OrderEditItemProps = {
item: HttpTypes.AdminOrderLineItem
@@ -18,6 +19,7 @@ type OrderEditItemProps = {
onItemRemove: (itemId: string) => void
itemReservedQuantitiesMap: Map<string, number>
form: UseFormReturn<zod.infer<typeof CreateFulfillmentSchema>>
disabled: boolean
}
export function OrderCreateFulfillmentItem({
@@ -25,6 +27,7 @@ export function OrderCreateFulfillmentItem({
form,
locationId,
itemReservedQuantitiesMap,
disabled,
}: OrderEditItemProps) {
const { t } = useTranslation()
@@ -70,102 +73,120 @@ export function OrderCreateFulfillmentItem({
)
return (
<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-1 items-center gap-x-3">
<Thumbnail src={item.thumbnail} />
<div className="flex flex-col">
<div>
<Text className="txt-small" as="span" weight="plus">
{item.title}
</Text>
{item.variant_sku && <span>({item.variant_sku})</span>}
</div>
<Text as="div" className="text-ui-fg-subtle txt-small">
{item.variant_title}
</Text>
<div className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl">
<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>
)}
<div className="flex flex-1 items-center gap-x-1">
<div className="mr-2 block h-[16px] w-[2px] bg-gray-200" />
<div className="text-small flex flex-1 flex-col">
<span className="text-ui-fg-subtle font-medium">
{t("orders.fulfillment.available")}
</span>
<span className="text-ui-fg-subtle">
{availableQuantity || "N/A"}
</span>
<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">
<Thumbnail src={item.thumbnail} />
<div className="flex flex-col">
<div>
<Text className="txt-small" as="span" weight="plus">
{item.title}
</Text>
{item.variant_sku && <span>({item.variant_sku})</span>}
</div>
<Text as="div" className="text-ui-fg-subtle txt-small">
{item.variant_title}
</Text>
</div>
</div>
<div className="flex flex-1 items-center gap-x-1">
<div className="mr-2 block h-[16px] w-[2px] bg-gray-200" />
<div className="flex flex-col">
<div className="text-small flex flex-1 flex-col">
<span className="text-ui-fg-subtle font-medium">
{t("orders.fulfillment.inStock")}
{t("orders.fulfillment.available")}
</span>
<span className="text-ui-fg-subtle">
{inStockQuantity || "N/A"}{" "}
{inStockQuantity && (
<span className="font-medium text-red-500">
-{form.getValues(`quantity.${item.id}`)}
</span>
)}
{availableQuantity || "N/A"}
</span>
</div>
</div>
<div className="flex flex-1 items-center gap-1">
<Form.Field
control={form.control}
name={`quantity.${item.id}`}
rules={{ required: true, min: minValue, max: maxValue }}
render={({ field }) => {
return (
<Form.Item>
<Form.Control>
<Input
className="bg-ui-bg-base txt-small w-[50px] rounded-lg text-right [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
type="number"
{...field}
onChange={(e) => {
const val =
e.target.value === ""
? null
: Number(e.target.value)
<div className="flex flex-1 items-center gap-x-1">
<div className="mr-2 block h-[16px] w-[2px] bg-gray-200" />
field.onChange(val)
<div className="flex flex-col">
<span className="text-ui-fg-subtle font-medium">
{t("orders.fulfillment.inStock")}
</span>
<span className="text-ui-fg-subtle">
{inStockQuantity || "N/A"}{" "}
{inStockQuantity && (
<span className="font-medium text-red-500">
-{form.getValues(`quantity.${item.id}`)}
</span>
)}
</span>
</div>
</div>
if (!isNaN(val)) {
if (val < minValue || val > maxValue) {
form.setError(`quantity.${item.id}`, {
type: "manual",
message: t(
"orders.fulfillment.error.wrongQuantity",
{
count: maxValue,
number: maxValue,
}
),
})
} else {
form.clearErrors(`quantity.${item.id}`)
<div className="flex flex-1 items-center gap-1">
<Form.Field
control={form.control}
name={`quantity.${item.id}`}
rules={{ required: true, min: minValue, max: maxValue }}
render={({ field }) => {
return (
<Form.Item>
<Form.Control>
<Input
className="bg-ui-bg-base txt-small w-[50px] rounded-lg text-right [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
type="number"
{...field}
onChange={(e) => {
const val =
e.target.value === ""
? null
: Number(e.target.value)
field.onChange(val)
if (!isNaN(val)) {
if (val < minValue || val > maxValue) {
form.setError(`quantity.${item.id}`, {
type: "manual",
message: t(
"orders.fulfillment.error.wrongQuantity",
{
count: maxValue,
number: maxValue,
}
),
})
} else {
form.clearErrors(`quantity.${item.id}`)
}
}
}
}}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
}}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<span className="text-ui-fg-subtle">
/ {item.quantity} {t("fields.qty")}
</span>
<span className="text-ui-fg-subtle">
/ {item.quantity} {t("fields.qty")}
</span>
</div>
</div>
</div>
</div>
@@ -10,7 +10,8 @@ export function OrderCreateFulfillment() {
const requiresShipping = searchParams.get("requires_shipping") === "true"
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) {
@@ -79,10 +79,13 @@ export const ProductCreateForm = ({
return {}
}
return regions.reduce((acc, reg) => {
acc[reg.id] = reg.currency_code
return acc
}, {} as Record<string, string>)
return regions.reduce(
(acc, reg) => {
acc[reg.id] = reg.currency_code
return acc
},
{} as Record<string, string>
)
}, [regions])
/**
@@ -178,6 +181,15 @@ export const ProductCreateForm = ({
}
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)
}
@@ -208,7 +220,8 @@ export const ProductCreateForm = ({
}
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 (
<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({
control: form.control,
name: "sales_channels",
@@ -161,6 +171,34 @@ export const ProductCreateOrganizationSection = ({
}}
/>
</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">
<Form.Field
control={form.control}
@@ -64,6 +64,7 @@ export const ProductCreateSchema = z
discountable: z.boolean(),
type_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()),
tags: z.array(z.string()).optional(),
sales_channels: z
@@ -145,6 +146,7 @@ export const PRODUCT_CREATE_FORM_DEFAULTS: Partial<
media: [],
categories: [],
collection_id: "",
shipping_profile_id: "",
description: "",
handle: "",
height: "",
@@ -24,6 +24,7 @@ export const normalizeProductFormValues = (
: undefined,
images,
collection_id: values.collection_id || undefined,
shipping_profile_id: values.shipping_profile_id,
categories: values.categories.map((id) => ({ id })),
type_id: values.type_id || 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(
"product",
"*categories,-variants"
"*categories,*shipping_profile,-variants"
)
@@ -14,6 +14,7 @@ import { PRODUCT_DETAIL_FIELDS } from "./constants"
import { productLoader } from "./loader"
import { useDashboardExtension } from "../../../extensions"
import { ProductShippingProfileSection } from "./components/product-shipping-profile-section"
export const ProductDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -71,6 +72,7 @@ export const ProductDetail = () => {
</TwoColumnPage.Main>
<TwoColumnPage.Sidebar>
<ProductSalesChannelSection product={product} />
<ProductShippingProfileSection product={product} />
<ProductOrganizationSection product={product} />
<ProductAttributeSection product={product} />
</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>
)
}
@@ -58,14 +58,14 @@ export type CreateFulfillmentValidateOrderStepInput = {
* This step validates that a fulfillment can be created for an order. If the order
* is canceled, the items don't exist in the order, or the items aren't grouped by
* shipping requirement, the step throws an error.
*
*
* :::note
*
*
* You can retrieve an order's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query),
* or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep).
*
*
* :::
*
*
* @example
* const data = createFulfillmentValidateOrder({
* order: {
@@ -82,10 +82,7 @@ export type CreateFulfillmentValidateOrderStepInput = {
*/
export const createFulfillmentValidateOrder = createStep(
"create-fulfillment-validate-order",
({
order,
inputItems,
}: CreateFulfillmentValidateOrderStepInput) => {
({ order, inputItems }: CreateFulfillmentValidateOrderStepInput) => {
throwIfOrderIsCancelled({ order })
throwIfItemsDoesNotExistsInOrder({ order, inputItems })
throwIfItemsAreNotGroupedByShippingRequirement({ order, inputItems })
@@ -128,6 +125,7 @@ function prepareFulfillmentData({
id: string
provider_id: string
service_zone: { fulfillment_set: { location?: { id: string } } }
shipping_profile_id: string
}
shippingMethod: { data?: Record<string, unknown> | null }
reservations: ReservationItemDTO[]
@@ -156,6 +154,18 @@ function prepareFulfillmentData({
const orderItem = orderItemsMap.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 {
line_item_id: i.id,
inventory_item_id: reservation?.inventory_item_id,
@@ -273,17 +283,18 @@ function prepareInventoryUpdate({
/**
* 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"
/**
* This workflow creates a fulfillment for an order. It's used by the [Create Order Fulfillment Admin API Route](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments).
*
* This workflow has a hook that allows you to perform custom actions on the created fulfillment. For example, you can pass under `additional_data` custom data that
*
* This workflow has a hook that allows you to perform custom actions on the created fulfillment. For example, you can pass under `additional_data` custom data that
* allows you to create custom data models linked to the fulfillment.
*
*
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around creating a fulfillment.
*
*
* @example
* const { result } = await createOrderFulfillmentWorkflow(container)
* .run({
@@ -300,18 +311,16 @@ export const createOrderFulfillmentWorkflowId = "create-order-fulfillment"
* }
* }
* })
*
*
* @summary
*
*
* Creates a fulfillment for an order.
*
*
* @property hooks.fulfillmentCreated - This hook is executed after the fulfillment is created. You can consume this hook to perform custom actions on the created fulfillment.
*/
export const createOrderFulfillmentWorkflow = createWorkflow(
createOrderFulfillmentWorkflowId,
(
input: WorkflowData<CreateOrderFulfillmentWorkflowInput>
) => {
(input: WorkflowData<CreateOrderFulfillmentWorkflowInput>) => {
const order: OrderDTO = useRemoteQueryStep({
entry_point: "orders",
fields: [
@@ -323,12 +332,14 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
"items.variant.manage_inventory",
"items.variant.allow_backorder",
"items.variant.product.id",
"items.variant.product.shipping_profile.id",
"items.variant.weight",
"items.variant.length",
"items.variant.height",
"items.variant.width",
"items.variant.material",
"shipping_address.*",
"shipping_methods.id",
"shipping_methods.shipping_option_id",
"shipping_methods.data",
],
@@ -346,17 +357,29 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
}, {})
})
const shippingMethod = transform(order, (data) => {
return { data: data.shipping_methods?.[0]?.data }
const shippingOptionId = transform({ order, input }, (data) => {
return (
data.input.shipping_option_id ??
data.order.shipping_methods?.[0]?.shipping_option_id
)
})
const shippingOptionId = transform(order, (data) => {
return data.shipping_methods?.[0]?.shipping_option_id
const shippingMethod = transform({ order, shippingOptionId }, (data) => {
return {
data: data.order.shipping_methods?.find(
(sm) => sm.shipping_option_id === data.shippingOptionId
)?.data,
}
})
const shippingOption = useRemoteQueryStep({
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: {
id: shippingOptionId,
},
@@ -136,6 +136,11 @@ const normalizeProductForImport = (
return
}
if (normalizedKey.startsWith("shipping_profile_id")) {
response["shipping_profile_id"] = normalizedValue
return
}
if (normalizedKey.startsWith("product_category_")) {
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"
const basicFieldsToOmit = [
@@ -32,6 +36,7 @@ export const normalizeV1Products = (
productTypes: ProductTypes.ProductTypeDTO[]
productCollections: ProductTypes.ProductCollectionDTO[]
salesChannels: SalesChannelTypes.SalesChannelDTO[]
shippingProfiles: ShippingProfileDTO[]
}
): object[] => {
const productTypesMap = new Map(
@@ -43,6 +48,9 @@ export const normalizeV1Products = (
const salesChannelsMap = new Map(
supportingData.salesChannels.map((sc) => [sc.name, sc.id])
)
const shippingProfilesIds = new Set(
supportingData.shippingProfiles.map((sp) => sp.id)
)
return rawProducts.map((product) => {
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 (
key.startsWith("Product Category") &&
(key.endsWith("Handle") ||
@@ -1,4 +1,5 @@
import {
IFulfillmentModuleService,
IProductModuleService,
IRegionModuleService,
ISalesChannelModuleService,
@@ -16,9 +17,9 @@ export type ParseProductCsvStepInput = string
export const parseProductCsvStepId = "parse-product-csv"
/**
* This step parses a CSV file holding products to import, returning the products as
* This step parses a CSV file holding products to import, returning the products as
* objects that can be imported.
*
*
* @example
* const data = parseProductCsvStep("products.csv")
*/
@@ -35,20 +36,25 @@ export const parseProductCsvStep = createStep(
Modules.SALES_CHANNEL
)
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
Modules.FULFILLMENT
)
const csvProducts = convertCsvToJson(fileContent)
const [productTypes, productCollections, salesChannels] = await Promise.all(
[
const [productTypes, productCollections, salesChannels, shippingProfiles] =
await Promise.all([
productService.listProductTypes({}, {}),
productService.listProductCollections({}, {}),
salesChannelService.listSalesChannels({}, {}),
]
)
fulfillmentService.listShippingProfiles({}, {}),
])
const v1Normalized = normalizeV1Products(csvProducts, {
productTypes,
productCollections,
salesChannels,
shippingProfiles,
})
// We use the handle to group products and variants correctly.
@@ -1,6 +1,7 @@
import {
AdditionalData,
CreateProductWorkflowInputDTO,
LinkDefinition,
PricingTypes,
ProductTypes,
} from "@medusajs/framework/types"
@@ -8,6 +9,7 @@ import {
ProductWorkflowEvents,
isPresent,
MedusaError,
Modules,
} from "@medusajs/framework/utils"
import {
WorkflowData,
@@ -17,7 +19,11 @@ import {
transform,
createStep,
} from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "../../common"
import {
createRemoteLinkStep,
emitEventStep,
useQueryGraphStep,
} from "../../common"
import { associateProductsWithSalesChannelsStep } from "../../sales-channel"
import { createProductsStep } from "../steps/create-products"
import { createProductVariantsWorkflow } from "./create-product-variants"
@@ -29,14 +35,19 @@ export interface ValidateProductInputStepInput {
/**
* The products to validate.
*/
products: CreateProductWorkflowInputDTO[]
products: Omit<CreateProductWorkflowInputDTO, "sales_channels">[]
/**
* The shipping profiles to validate.
*/
shippingProfiles: { id: string }[]
}
const validateProductInputStepId = "validate-product-input"
/**
* This step validates that all provided products have options.
* If a product is missing options, an error is thrown.
*
*
* @example
* const data = validateProductInputStep({
* products: [
@@ -71,7 +82,7 @@ const validateProductInputStepId = "validate-product-input"
export const validateProductInputStep = createStep(
validateProductInputStepId,
async (data: ValidateProductInputStepInput) => {
const { products } = data
const { products, shippingProfiles } = data
const missingOptionsProductTitles = products
.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(
", "
)}].`
)
}
}
)
@@ -102,11 +132,11 @@ export const createProductsWorkflowId = "create-products"
/**
* This workflow creates one or more products. It's used by the [Create Product Admin API Route](https://docs.medusajs.com/api/admin#products_postproducts).
* It can also be useful to you when creating [seed scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/seed-data), for example.
*
*
* This workflow has a hook that allows you to perform custom actions on the created products. You can see an example in [this guide](https://docs.medusajs.com/resources/commerce-modules/product/extend).
*
*
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product creation.
*
*
* @example
* const { result } = await createProductsWorkflow(container)
* .run({
@@ -143,26 +173,45 @@ export const createProductsWorkflowId = "create-products"
* }
* }
* })
*
*
* @summary
*
*
* Create one or more products with options and variants.
*
*
* @property hooks.productCreated - This hook is executed after the products are created. You can consume this hook to perform custom actions on the created products.
*/
export const createProductsWorkflow = createWorkflow(
createProductsWorkflowId,
(input: WorkflowData<CreateProductsWorkflowInput>) => {
// Passing prices to the product module will fail, we want to keep them for after the product is created.
const productWithoutExternalRelations = transform({ input }, (data) =>
data.input.products.map((p) => ({
...p,
sales_channels: undefined,
variants: undefined,
}))
)
const { products: productWithoutExternalRelations, shippingPorfileIds } =
transform({ input }, (data) => {
const shippingPorfileIds: string[] = []
const productsData = data.input.products.map((p) => {
if (p.shipping_profile_id) {
shippingPorfileIds.push(p.shipping_profile_id)
}
validateProductInputStep({ products: productWithoutExternalRelations })
return {
...p,
sales_channels: undefined,
shipping_profile_id: undefined,
variants: undefined,
}
})
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)
@@ -182,6 +231,24 @@ export const createProductsWorkflow = createWorkflow(
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) => {
// TODO: Move this to a unified place for all product workflow types
const productVariants: (ProductTypes.CreateProductVariantDTO & {
@@ -47,6 +47,10 @@ export type UpdateProductsWorkflowInputSelector = {
* The variants to update.
*/
variants?: UpdateProductVariantWorkflowInputDTO[]
/**
* The shipping profile to set.
*/
shipping_profile_id?: string
}
} & AdditionalData
@@ -66,6 +70,10 @@ export type UpdateProductsWorkflowInputProducts = {
* The variants to update.
*/
variants?: UpdateProductVariantWorkflowInputDTO[]
/**
* The shipping profile to set.
*/
shipping_profile_id?: string
})[]
} & AdditionalData
@@ -90,6 +98,7 @@ function prepareUpdateProductInput({
products: input.products.map((p) => ({
...p,
sales_channels: undefined,
shipping_profile_id: undefined,
variants: p.variants?.map((v) => ({
...v,
prices: undefined,
@@ -103,6 +112,7 @@ function prepareUpdateProductInput({
update: {
...input.update,
sales_channels: undefined,
shipping_profile_id: undefined,
variants: input.update?.variants?.map((v) => ({
...v,
prices: undefined,
@@ -173,6 +183,44 @@ function prepareSalesChannelLinks({
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({
input,
updatedProducts,
@@ -243,18 +291,42 @@ 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"
/**
* 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 has a hook that allows you to perform custom actions on the updated products. For example, you can pass under `additional_data` custom data that
*
* This workflow has a hook that allows you to perform custom actions on the updated products. For example, you can pass under `additional_data` custom data that
* allows you to update custom data models linked to the products.
*
*
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product update.
*
*
* @example
* To update products by their IDs:
*
*
* ```ts
* const { result } = await updateProductsWorkflow(container)
* .run({
@@ -282,9 +354,9 @@ export const updateProductsWorkflowId = "update-products"
* }
* })
* ```
*
*
* You can also update products by a selector:
*
*
* ```ts
* const { result } = await updateProductsWorkflow(container)
* .run({
@@ -301,11 +373,11 @@ export const updateProductsWorkflowId = "update-products"
* }
* })
* ```
*
*
* @summary
*
*
* Update one or more products with options and variants.
*
*
* @property hooks.productsUpdated - This hook is executed after the products are updated. You can consume this hook to perform custom actions on the updated products.
*/
export const updateProductsWorkflow = createWorkflow(
@@ -345,11 +417,20 @@ export const updateProductsWorkflow = createWorkflow(
const toUpdateInput = transform({ input }, prepareUpdateProductInput)
const updatedProducts = updateProductsStep(toUpdateInput)
const updatedPorductIds = transform({ updatedProducts }, (data) => {
return data.updatedProducts.map((p) => p.id)
})
const salesChannelLinks = transform(
{ input, updatedProducts },
prepareSalesChannelLinks
)
const shippingProfileLinks = transform(
{ input, updatedProducts },
prepareShippingProfileLinks
)
const variantPrices = transform(
{ input, updatedProducts },
prepareVariantPrices
@@ -366,16 +447,33 @@ export const updateProductsWorkflow = createWorkflow(
variables: { filters: { product_id: productsWithSalesChannels } },
}).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(
{ currentSalesChannelLinks },
prepareToDeleteSalesChannelLinks
)
const toDeleteShippingProfileLinks = transform(
{ currentShippingProfileLinks },
prepareToDeleteShippingProfileLinks
)
upsertVariantPricesWorkflow.runAsStep({
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(
{ updatedProducts },
@@ -387,7 +485,12 @@ export const updateProductsWorkflow = createWorkflow(
)
parallelize(
createRemoteLinkStep(salesChannelLinks),
createRemoteLinkStep(salesChannelLinks).config({
name: "create-sales-channel-links-step",
}),
createRemoteLinkStep(shippingProfileLinks).config({
name: "create-shipping-profile-links-step",
}),
emitEventStep({
eventName: ProductWorkflowEvents.UPDATED,
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 { removeRemoteLinkStep } from "../../common"
import { Modules } from "@medusajs/framework/utils"
import { removeRemoteLinkStep, useQueryGraphStep } from "../../common"
/**
* 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.
@@ -19,10 +42,11 @@ export const deleteShippingProfileWorkflowId =
/**
* 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).
*
* 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
* delete shipping profiles within your custom flows.
*
*
* @example
* const { result } = await deleteShippingProfileWorkflow(container)
* .run({
@@ -30,14 +54,24 @@ export const deleteShippingProfileWorkflowId =
* ids: ["sp_123"]
* }
* })
*
*
* @summary
*
*
* Delete shipping profiles.
*/
export const deleteShippingProfileWorkflow = createWorkflow(
deleteShippingProfileWorkflowId,
(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)
removeRemoteLinkStep({
@@ -32,6 +32,11 @@ export interface AdminCreateOrderFulfillment {
* to fulfill the items from.
*/
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.
*/
@@ -5,7 +5,7 @@ export interface AdminExportProductRequest {}
export interface AdminImportProductRequest {
/**
* The CSV file to import the products from.
*
*
* It's an uploaded file of type [File](https://developer.mozilla.org/en-US/docs/Web/API/File).
*/
file: File
@@ -35,7 +35,7 @@ export interface AdminBatchProductVariantInventoryItemRequest
export interface AdminCreateProductVariantPrice {
/**
* The price's currency code.
*
*
* @example
* usd
*/
@@ -54,11 +54,11 @@ export interface AdminCreateProductVariantPrice {
max_quantity?: number | null
/**
* The price's rules.
*
*
* @privateRemarks
* Note: Although the BE is generic, we only use region_id for price rules for now, so it's better to keep the typings stricter.
*/
rules?: {
rules?: {
/**
* The ID of the region that the price applies in.
*/
@@ -180,11 +180,11 @@ export interface AdminCreateProduct {
/**
* The product's images.
*/
images?: {
images?: {
/**
* The image's URL.
*/
url: string
url: string
}[]
/**
* The product's thumbnail URL.
@@ -210,10 +210,14 @@ export interface AdminCreateProduct {
* The ID of the product's collection.
*/
collection_id?: string
/**
* The ID of the product's shipping profile.
*/
shipping_profile_id: string
/**
* The product's categories.
*/
categories?: {
categories?: {
/**
* The ID of a product category that the product belongs to.
*/
@@ -222,7 +226,7 @@ export interface AdminCreateProduct {
/**
* The product's tags.
*/
tags?: {
tags?: {
/**
* The ID of the associated product tag.
*/
@@ -239,7 +243,7 @@ export interface AdminCreateProduct {
/**
* The sales channels that the product is available in.
*/
sales_channels?: {
sales_channels?: {
/**
* The ID of a sales channel that the product is available in.
*/
@@ -363,7 +367,8 @@ export interface AdminUpdateProductVariant {
options?: Record<string, string>
}
export interface AdminBatchUpdateProductVariant extends AdminUpdateProductVariant {
export interface AdminBatchUpdateProductVariant
extends AdminUpdateProductVariant {
/**
* The ID of the variant to update.
*/
@@ -394,7 +399,7 @@ export interface AdminUpdateProduct {
/**
* The product's images.
*/
images?: {
images?: {
/**
* The image's URL.
*/
@@ -427,20 +432,20 @@ export interface AdminUpdateProduct {
/**
* The product's categories.
*/
categories?: {
categories?: {
/**
* The ID of the category that the product belongs to.
*/
id: string
id: string
}[]
/**
* The product's tags.
*/
tags?: {
tags?: {
/**
* The ID of a tag that the product is associated with.
*/
id: string
id: string
}[]
/**
* The product's options.
@@ -453,12 +458,16 @@ export interface AdminUpdateProduct {
/**
* The sales channels that the product is available in.
*/
sales_channels?: {
sales_channels?: {
/**
* The ID of a sales channel that the product is available in.
*/
id: string
id: string
}[]
/**
* The ID of the product's shipping profile.
*/
shipping_profile_id?: string
/**
* The product's weight.
*/
@@ -62,6 +62,11 @@ export interface CreateOrderFulfillmentWorkflowInput {
*/
metadata?: Record<string, any> | null
/**
* Shipping option to be used for the fulfillment.
*/
shipping_option_id?: string
/**
* Whether the fulfillment should be shipped.
*/
@@ -33,6 +33,10 @@ export type CreateProductWorkflowInputDTO = Omit<
* The sales channels that the product is available in.
*/
sales_channels?: { id: string }[]
/**
* The product's shipping profile.
*/
shipping_profile_id: string
/**
* The product's variants.
*/
@@ -41,4 +45,5 @@ export type CreateProductWorkflowInputDTO = Omit<
export type UpdateProductWorkflowInputDTO = ProductTypes.UpsertProductDTO & {
sales_channels?: { id: string }[]
shipping_profile_id?: string
}
+6
View File
@@ -116,4 +116,10 @@ export const LINKS = {
Modules.FULFILLMENT,
"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({
items: z.array(Item),
location_id: z.string().nullish(),
shipping_option_id: z.string().optional(),
no_notification: z.boolean().optional(),
metadata: z.record(z.unknown()).nullish(),
})
@@ -232,6 +232,7 @@ export const CreateProduct = z
options: z.array(CreateProductOption).optional(),
variants: z.array(CreateProductVariant).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(),
shipping_profile_id: z.string(),
weight: z.number().nullish(),
length: z.number().nullish(),
height: z.number().nullish(),
@@ -266,6 +267,7 @@ export const UpdateProduct = z
categories: z.array(IdAssociation).optional(),
tags: z.array(IdAssociation).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(),
shipping_profile_id: z.string().optional(),
weight: z.number().nullish(),
length: 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 "./sales-channel-location"
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,
},
},
],
}