chore(workflows, core-flows): Split workflows tooling and definitions (#5705)

This commit is contained in:
Adrien de Peretti
2023-11-24 13:55:48 +00:00
committed by GitHub
parent fc1ef29ed9
commit ddbeed4ea6
138 changed files with 274 additions and 241 deletions
@@ -0,0 +1,112 @@
import { AddressDTO } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type AddressesDTO = {
shipping_address_id?: string
billing_address_id?: string
}
type HandlerInputData = {
addresses: AddressesDTO & {
billing_address?: AddressDTO
shipping_address?: AddressDTO
}
region: {
region_id?: string
}
}
enum Aliases {
Addresses = "addresses",
Region = "region",
}
export async function findOrCreateAddresses({
container,
data,
}: WorkflowArguments<HandlerInputData>): Promise<AddressesDTO> {
const regionService = container.resolve("regionService")
const addressRepository = container.resolve("addressRepository")
const shippingAddress = data[Aliases.Addresses].shipping_address
const shippingAddressId = data[Aliases.Addresses].shipping_address_id
const billingAddress = data[Aliases.Addresses].billing_address
const billingAddressId = data[Aliases.Addresses].billing_address_id
const addressesDTO: AddressesDTO = {}
const region = await regionService.retrieve(data[Aliases.Region].region_id, {
relations: ["countries"],
})
const regionCountries = region.countries.map(({ iso_2 }) => iso_2)
if (!shippingAddress && !shippingAddressId) {
if (region.countries.length === 1) {
const shippingAddress = addressRepository.create({
country_code: regionCountries[0],
})
addressesDTO.shipping_address_id = shippingAddress?.id
}
} else {
if (shippingAddress) {
if (!regionCountries.includes(shippingAddress.country_code!)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Shipping country not in region"
)
}
}
if (shippingAddressId) {
const address = await regionService.findOne({
where: { id: shippingAddressId },
})
if (
address?.country_code &&
!regionCountries.includes(address.country_code)
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Shipping country not in region"
)
}
addressesDTO.shipping_address_id = address.id
}
}
if (billingAddress) {
if (!regionCountries.includes(billingAddress.country_code!)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Billing country not in region"
)
}
}
if (billingAddressId) {
const address = await regionService.findOne({
where: { id: billingAddressId },
})
if (
address?.country_code &&
!regionCountries.includes(address.country_code)
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Billing country not in region"
)
}
addressesDTO.billing_address_id = billingAddressId
}
return addressesDTO
}
findOrCreateAddresses.aliases = Aliases
@@ -0,0 +1 @@
export * from "./find-or-create-addresses"
@@ -0,0 +1,57 @@
import { CartWorkflow } from "@medusajs/types"
import { SalesChannelFeatureFlag } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type HandlerInputData = {
line_items: {
items?: CartWorkflow.CreateLineItemInputDTO[]
}
cart: {
id: string
customer_id: string
region_id: string
}
}
enum Aliases {
LineItems = "line_items",
Cart = "cart",
}
export async function attachLineItemsToCart({
container,
context,
data,
}: WorkflowArguments<HandlerInputData>): Promise<void> {
const { manager } = context
const featureFlagRouter = container.resolve("featureFlagRouter")
const lineItemService = container.resolve("lineItemService")
const cartService = container.resolve("cartService")
const lineItemServiceTx = lineItemService.withTransaction(manager)
const cartServiceTx = cartService.withTransaction(manager)
let lineItems = data[Aliases.LineItems].items
const cart = data[Aliases.Cart]
if (lineItems?.length) {
const generateInputData = lineItems.map((item) => ({
variantId: item.variant_id,
quantity: item.quantity,
}))
lineItems = await lineItemServiceTx.generate(generateInputData, {
region_id: cart.region_id,
customer_id: cart.customer_id,
})
await cartServiceTx.addOrUpdateLineItems(cart.id, lineItems, {
validateSalesChannels: featureFlagRouter.isFeatureEnabled(
SalesChannelFeatureFlag.key
),
})
}
}
attachLineItemsToCart.aliases = Aliases
@@ -0,0 +1,57 @@
import { CartDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
enum Aliases {
SalesChannel = "SalesChannel",
Addresses = "addresses",
Customer = "customer",
Region = "region",
Context = "context",
}
type HandlerInputData = {
sales_channel: {
sales_channel_id?: string
}
addresses: {
shipping_address_id: string
billing_address_id: string
}
customer: {
customer_id?: string
email?: string
}
region: {
region_id: string
}
context: {
context: Record<any, any>
}
}
type HandlerOutputData = {
cart: CartDTO
}
export async function createCart({
container,
context,
data,
}: WorkflowArguments<HandlerInputData>): Promise<HandlerOutputData> {
const { manager } = context
const cartService = container.resolve("cartService")
const cartServiceTx = cartService.withTransaction(manager)
const cart = await cartServiceTx.create({
...data[Aliases.SalesChannel],
...data[Aliases.Addresses],
...data[Aliases.Customer],
...data[Aliases.Region],
...data[Aliases.Context],
})
return cart
}
createCart.aliases = Aliases
@@ -0,0 +1,4 @@
export * from "./attach-line-items-to-cart"
export * from "./create-cart"
export * from "./remove-cart"
export * from "./retrieve-cart"
@@ -0,0 +1,28 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
enum Aliases {
Cart = "cart",
}
type HandlerInputData = {
cart: {
id: string
}
}
export async function removeCart({
container,
context,
data,
}: WorkflowArguments<HandlerInputData>): Promise<void> {
const { manager } = context
const cartService = container.resolve("cartService")
const cartServiceTx = cartService.withTransaction(manager)
const cart = data[Aliases.Cart]
await cartServiceTx.delete(cart.id)
}
removeCart.aliases = Aliases
@@ -0,0 +1,40 @@
import { CartDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type HandlerInputData = {
cart: {
id: string
}
config: {
retrieveConfig: {
select: string[]
relations: string[]
}
}
}
enum Aliases {
Cart = "cart",
Config = "config",
}
export async function retrieveCart({
container,
context,
data,
}: WorkflowArguments<HandlerInputData>): Promise<CartDTO> {
const { manager } = context
const cartService = container.resolve("cartService")
const cartServiceTx = cartService.withTransaction(manager)
const retrieved = await cartServiceTx.retrieve(
data[Aliases.Cart].id,
data[Aliases.Config].retrieveConfig
)
return retrieved
}
retrieveCart.aliases = Aliases
@@ -0,0 +1 @@
export * from "./set-context"
@@ -0,0 +1,27 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type ContextDTO = {
context?: Record<any, any>
}
enum Aliases {
Context = "context",
}
type HandlerInputData = {
context: {
context?: Record<any, any>
}
}
export async function setContext({
data,
}: WorkflowArguments<HandlerInputData>): Promise<ContextDTO> {
const contextDTO: ContextDTO = {
context: data[Aliases.Context].context,
}
return contextDTO
}
setContext.aliases = Aliases
@@ -0,0 +1,63 @@
import { validateEmail } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type CustomerDTO = {
customer_id?: string
email?: string
}
type HandlerInputData = {
customer: {
customer_id?: string
email?: string
}
}
enum Aliases {
Customer = "customer",
}
export async function findOrCreateCustomer({
container,
context,
data,
}: WorkflowArguments<HandlerInputData>): Promise<CustomerDTO> {
const { manager } = context
const customerService = container.resolve("customerService")
const customerDTO: CustomerDTO = {}
const customerId = data[Aliases.Customer].customer_id
const customerServiceTx = customerService.withTransaction(manager)
if (customerId) {
const customer = await customerServiceTx
.retrieve(customerId)
.catch(() => undefined)
customerDTO.customer_id = customer?.id
customerDTO.email = customer?.email
}
const customerEmail = data[Aliases.Customer].email
if (customerEmail) {
const validatedEmail = validateEmail(customerEmail)
let customer = await customerServiceTx
.retrieveUnregisteredByEmail(validatedEmail)
.catch(() => undefined)
if (!customer) {
customer = await customerServiceTx.create({ email: validatedEmail })
}
customerDTO.customer_id = customer.id
customerDTO.email = customer.email
}
return customerDTO
}
findOrCreateCustomer.aliases = Aliases
@@ -0,0 +1 @@
export * from "./find-or-create-customer"
+10
View File
@@ -0,0 +1,10 @@
export * as AddressHandlers from "./address"
export * as CartHandlers from "./cart"
export * as CommonHandlers from "./common"
export * as CustomerHandlers from "./customer"
export * as InventoryHandlers from "./inventory"
export * as MiddlewaresHandlers from "./middlewares"
export * as PriceListHandlers from "./price-list"
export * as ProductHandlers from "./product"
export * as RegionHandlers from "./region"
export * as SalesChannelHandlers from "./sales-channel"
@@ -0,0 +1,35 @@
import { InventoryItemDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
export async function attachInventoryItems({
container,
context,
data,
}: WorkflowArguments<{
inventoryItems: {
tag: string
inventoryItem: InventoryItemDTO
}[]
}>) {
const { manager } = context
const productVariantInventoryService = container
.resolve("productVariantInventoryService")
.withTransaction(manager)
if (!data?.inventoryItems?.length) {
return []
}
const inventoryData = data.inventoryItems.map(({ tag, inventoryItem }) => ({
variantId: tag,
inventoryItemId: inventoryItem.id,
}))
await productVariantInventoryService.attachInventoryItem(inventoryData)
return data.inventoryItems
}
attachInventoryItems.aliases = {
inventoryItems: "inventoryItems",
}
@@ -0,0 +1,48 @@
import { IInventoryService, InventoryItemDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { promiseAll } from "@medusajs/utils"
type Result = {
tag: string
inventoryItem: InventoryItemDTO
}[]
export async function createInventoryItems({
container,
data,
}: WorkflowArguments<{
inventoryItems: (InventoryItemDTO & { _associationTag?: string })[]
}>): Promise<Result | void> {
const inventoryService: IInventoryService =
container.resolve("inventoryService")
if (!inventoryService) {
const logger = container.resolve("logger")
logger.warn(
`Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'createInventoryItems' will be skipped.`
)
return void 0
}
return await promiseAll(
data.inventoryItems.map(async (item) => {
const inventoryItem = await inventoryService!.createInventoryItem({
sku: item.sku!,
origin_country: item.origin_country!,
hs_code: item.hs_code!,
mid_code: item.mid_code!,
material: item.material!,
weight: item.weight!,
length: item.length!,
height: item.height!,
width: item.width!,
})
return { tag: item._associationTag ?? inventoryItem.id, inventoryItem }
})
)
}
createInventoryItems.aliases = {
payload: "payload",
}
@@ -0,0 +1,39 @@
import { InventoryItemDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { promiseAll } from "@medusajs/utils"
export async function detachInventoryItems({
container,
context,
data,
}: WorkflowArguments<{
inventoryItems: {
tag: string
inventoryItem: InventoryItemDTO
}[]
}>) {
const { manager } = context
const productVariantInventoryService = container
.resolve("productVariantInventoryService")
.withTransaction(manager)
if (!data?.inventoryItems?.length) {
return []
}
await promiseAll(
data.inventoryItems.map(async ({ tag, inventoryItem }) => {
return await productVariantInventoryService.detachInventoryItem(
inventoryItem.id,
tag
)
})
)
return data.inventoryItems
}
detachInventoryItems.aliases = {
inventoryItems: "inventoryItems",
}
@@ -0,0 +1,5 @@
export * from "./detach-inventory-items"
export * from "./attach-inventory-items"
export * from "./create-inventory-items"
export * from "./remove-inventory-items"
export * from "./restore-inventory-items"
@@ -0,0 +1,29 @@
import { InventoryItemDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
export async function removeInventoryItems({
container,
data,
}: WorkflowArguments<{
inventoryItems: { inventoryItem: InventoryItemDTO }[]
}>) {
const inventoryService = container.resolve("inventoryService")
if (!inventoryService) {
const logger = container.resolve("logger")
logger.warn(
`Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'removeInventoryItems' will be skipped.`
)
return []
}
await inventoryService!.deleteInventoryItem(
data.inventoryItems.map(({ inventoryItem }) => inventoryItem.id)
)
return data.inventoryItems
}
removeInventoryItems.aliases = {
inventoryItems: "inventoryItems",
}
@@ -0,0 +1,35 @@
import {
IInventoryService,
InventoryItemDTO,
SharedContext,
} from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
export async function restoreInventoryItems({
container,
context,
data,
}: WorkflowArguments<{
inventoryItems: { inventoryItem: InventoryItemDTO }[]
}>) {
const { manager } = context as SharedContext
const inventoryService: IInventoryService =
container.resolve("inventoryService")
if (!inventoryService) {
const logger = container.resolve("logger")
logger.warn(
`Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'removeInventoryItems' will be skipped.`
)
return
}
return await inventoryService!.restoreInventoryItem(
data.inventoryItems.map(({ inventoryItem }) => inventoryItem.id),
{ transactionManager: manager }
)
}
restoreInventoryItems.aliases = {
inventoryItems: "inventoryItems",
}
@@ -0,0 +1,55 @@
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type ProductHandle = string
type VariantIndexAndPrices = {
index: number
prices: WorkflowTypes.ProductWorkflow.CreateProductVariantPricesInputDTO[]
}
export async function createProductsPrepareCreatePricesCompensation({
data,
}: WorkflowArguments<{
preparedData: {
productsHandleVariantsIndexPricesMap: Map<
ProductHandle,
VariantIndexAndPrices[]
>
}
products: ProductTypes.ProductDTO[]
}>) {
const productsHandleVariantsIndexPricesMap =
data.preparedData.productsHandleVariantsIndexPricesMap
const products = data.products
const updatedProductsHandleVariantsIndexPricesMap = new Map()
productsHandleVariantsIndexPricesMap.forEach(
(existingItems, productHandle) => {
const items =
updatedProductsHandleVariantsIndexPricesMap.get(productHandle) ?? []
existingItems.forEach(({ index }) => {
items.push({
index,
prices: [],
})
})
updatedProductsHandleVariantsIndexPricesMap.set(productHandle, items)
}
)
return {
alias: createProductsPrepareCreatePricesCompensation.aliases.output,
value: {
productsHandleVariantsIndexPricesMap:
updatedProductsHandleVariantsIndexPricesMap,
products,
},
}
}
createProductsPrepareCreatePricesCompensation.aliases = {
preparedData: "preparedData",
output: "createProductsPrepareCreatePricesCompensationOutput",
}
@@ -0,0 +1,28 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
export async function extractVariants({
data,
}: WorkflowArguments<{
object: { variants?: ProductTypes.ProductVariantDTO[] }[]
}>) {
const variants = data.object.reduce((acc, object) => {
if (object.variants?.length) {
return acc.concat(object.variants)
}
return acc
}, [] as ProductTypes.ProductVariantDTO[])
return {
alias: extractVariants.aliases.output,
value: {
variants,
},
}
}
extractVariants.aliases = {
output: "extractVariantsFromProductOutput",
object: "object",
}
@@ -0,0 +1,6 @@
export * from "./create-products-prepare-create-prices-compensation"
export * from "./update-products-extract-created-variants"
export * from "./update-products-extract-deleted-variants"
export * from "./use-variants-inventory-items"
export * from "./extract-variants"
export * from "./map-data"
@@ -0,0 +1,16 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
/**
* Middleware for map input data to a key/s.
*
* @param mapFn - apply function on the input data and return result as the middleware output
* @param alias - key to save output under (if `merge === false`)
*/
export function mapData<T, S>(mapFn: (arg: T) => S, alias = "mapData") {
return async function ({ data }: WorkflowArguments<T>) {
return {
alias,
value: mapFn(data),
}
}
}
@@ -0,0 +1,40 @@
import { ProductTypes, ProductVariantDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { UpdateProductsPreparedData } from "../product"
export async function updateProductsExtractCreatedVariants({
data,
}: WorkflowArguments<{
preparedData: UpdateProductsPreparedData // products state before the update
products: ProductTypes.ProductDTO[] // updated products
}>) {
const createdVariants: ProductVariantDTO[] = []
data.products.forEach((product) => {
const addedVariants: ProductVariantDTO[] = []
const originalProduct = data.preparedData.originalProducts.find(
(p) => p.id === product.id
)!
product.variants.forEach((variant) => {
if (!originalProduct.variants.find((v) => v.id === variant.id)) {
addedVariants.push(variant)
}
})
createdVariants.push(...addedVariants)
})
return {
alias: updateProductsExtractCreatedVariants.aliases.output,
value: [{ variants: createdVariants }],
}
}
updateProductsExtractCreatedVariants.aliases = {
preparedData: "preparedData",
products: "products",
output: "products",
}
@@ -0,0 +1,43 @@
import { ProductTypes, ProductVariantDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { UpdateProductsPreparedData } from "../product"
export async function updateProductsExtractDeletedVariants({
data,
container,
}: WorkflowArguments<{
preparedData: UpdateProductsPreparedData // products state before the update
products: ProductTypes.ProductDTO[] // updated products
}>) {
const deletedVariants: ProductVariantDTO[] = []
data.products.forEach((product) => {
const removedVariants: ProductVariantDTO[] = []
const originalProduct = data.preparedData.originalProducts.find(
(p) => p.id === product.id
)!
originalProduct.variants.forEach((variant) => {
if (!product.variants.find((v) => v.id === variant.id)) {
removedVariants.push(variant)
}
})
deletedVariants.push(...removedVariants)
})
return {
alias: updateProductsExtractDeletedVariants.aliases.output,
value: {
variants: deletedVariants,
},
}
}
updateProductsExtractDeletedVariants.aliases = {
preparedData: "preparedData",
products: "products",
output: "updateProductsExtractDeletedVariantsOutput",
}
@@ -0,0 +1,46 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { IInventoryService, ProductVariantDTO } from "@medusajs/types"
export async function useVariantsInventoryItems({
data,
container,
}: WorkflowArguments<{
updateProductsExtractDeletedVariantsOutput: { variants: ProductVariantDTO[] }
}>) {
const inventoryService: IInventoryService =
container.resolve("inventoryService")
if (!inventoryService) {
const logger = container.resolve("logger")
logger.warn(
`Inventory service not found. You should install the @medusajs/inventory package to use inventory. The 'useVariantsInventoryItems' will be skipped.`
)
return {
alias: useVariantsInventoryItems.aliases.output,
value: null,
}
}
const [inventoryItems] = await inventoryService!.listInventoryItems({
sku: data.updateProductsExtractDeletedVariantsOutput.variants.map(
(v) => v.id
),
})
const variantItems = inventoryItems.map((item) => ({
inventoryItem: item,
tag: data.updateProductsExtractDeletedVariantsOutput.variants.find(
(variant) => variant.sku === item.sku
)!.id,
}))
return {
alias: useVariantsInventoryItems.aliases.output,
value: { inventoryItems: variantItems },
}
}
useVariantsInventoryItems.aliases = {
variants: "variants",
output: "useVariantsInventoryItemsOutput",
}
@@ -0,0 +1,42 @@
import {
CreatePriceListDTO,
IPricingModuleService,
PriceListDTO,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type Result = {
priceList: PriceListDTO
}[]
type Input = {
tag?: string
priceList: CreatePriceListDTO
}[]
export async function createPriceLists({
container,
data,
}: WorkflowArguments<{
priceLists: Input
}>): Promise<Result> {
const pricingService: IPricingModuleService = container.resolve(
ModuleRegistrationName.PRICING
)
return await Promise.all(
data.priceLists.map(async (item) => {
const [priceList] = await pricingService!.createPriceLists([
item.priceList,
])
return { tag: item.tag ?? priceList.id, priceList }
})
)
}
createPriceLists.aliases = {
priceLists: "priceLists",
}
@@ -0,0 +1,10 @@
export * from "./create-price-list"
export * from "./prepare-create-price-list"
export * from "./prepare-update-price-lists"
export * from "./remove-price-list"
export * from "./update-price-lists"
export * from "./prepare-remove-product-prices"
export * from "./remove-price-set-price-list-prices"
export * from "./prepare-remove-variant-prices"
export * from "./prepare-remove-price-list-prices"
export * from "./remove-prices"
@@ -0,0 +1,87 @@
import { CreatePriceListDTO, PriceListWorkflow } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type Result = {
tag?: string
priceList: CreatePriceListDTO
}[]
export async function prepareCreatePriceLists({
container,
data,
}: WorkflowArguments<{
price_lists: (PriceListWorkflow.CreatePriceListWorkflowDTO & {
_associationTag?: string
})[]
}>): Promise<Result | void> {
const remoteQuery = container.resolve("remoteQuery")
const { price_lists } = data
const variantIds = price_lists
.map((priceList) => priceList.prices.map((price) => price.variant_id))
.flat()
const variables = {
variant_id: variantIds,
}
const query = {
product_variant_price_set: {
__args: variables,
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
const variantIdPriceSetIdMap: Map<string, string> = new Map(
variantPriceSets.map((variantPriceSet) => [
variantPriceSet.variant_id,
variantPriceSet.price_set_id,
])
)
const variantsWithoutPriceSets: string[] = []
for (const variantId of variantIds) {
if (!variantIdPriceSetIdMap.has(variantId)) {
variantsWithoutPriceSets.push(variantId)
}
}
if (variantsWithoutPriceSets.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`No priceSet exist for variants: ${variantsWithoutPriceSets.join(", ")}`
)
}
return price_lists.map((priceListDTO) => {
priceListDTO.title ??= priceListDTO.name
const { _associationTag, name, prices, ...rest } = priceListDTO
const priceList = rest as CreatePriceListDTO
priceList.rules ??= {}
priceList.prices =
prices?.map((price) => {
const price_set_id = variantIdPriceSetIdMap.get(price.variant_id)!
return {
currency_code: price.currency_code,
amount: price.amount,
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
price_set_id,
}
}) ?? []
return { priceList, tag: _associationTag }
})
}
prepareCreatePriceLists.aliases = {
payload: "payload",
}
@@ -0,0 +1,48 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPricingModuleService } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type Result = {
moneyAmountIds: string[]
priceListId: string
}
export async function prepareRemovePriceListPrices({
container,
data,
}: WorkflowArguments<{
money_amount_ids: string[]
price_list_id: string
}>): Promise<Result | void> {
const pricingService: IPricingModuleService = container.resolve(
ModuleRegistrationName.PRICING
)
const {
price_list_id: priceListId,
money_amount_ids: moneyAmountIdsToDelete,
} = data
const moneyAmounts = await pricingService.listMoneyAmounts(
{ id: moneyAmountIdsToDelete },
{
relations: [
"price_set_money_amount",
"price_set_money_amount.price_list",
],
}
)
const moneyAmountIds = moneyAmounts
.filter(
(moneyAmount) =>
moneyAmount?.price_set_money_amount?.price_list?.id === priceListId
)
.map((ma) => ma.id)
return { moneyAmountIds, priceListId }
}
prepareRemovePriceListPrices.aliases = {
payload: "payload",
}
@@ -0,0 +1,62 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { prepareCreatePriceLists } from "./prepare-create-price-list"
type Result = {
priceSetIds: string[]
priceListId: string
}
export async function prepareRemoveProductPrices({
container,
data,
}: WorkflowArguments<{
product_ids: string[]
price_list_id: string
}>): Promise<Result | void> {
const remoteQuery = container.resolve("remoteQuery")
const { price_list_id, product_ids } = data
const variables = {
id: product_ids,
}
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const productsWithVariantPriceSets: QueryResult[] = await remoteQuery(query)
const priceSetIds = productsWithVariantPriceSets
.map(({ variants }) => variants.map(({ price }) => price.price_set_id))
.flat()
return { priceSetIds, priceListId: price_list_id }
}
prepareCreatePriceLists.aliases = {
payload: "payload",
}
type QueryResult = {
id: string
variants: {
id: string
price: {
price_set_id: string
variant_id: string
}
}[]
}
const defaultAdminProductRemoteQueryObject = {
fields: ["id"],
variants: {
price: {
fields: ["variant_id", "price_set_id"],
},
},
}
@@ -0,0 +1,56 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { prepareCreatePriceLists } from "./prepare-create-price-list"
type Result = {
priceSetIds: string[]
priceListId: string
}
export async function prepareRemoveVariantPrices({
container,
data,
}: WorkflowArguments<{
variant_ids: string[]
price_list_id: string
}>): Promise<Result | void> {
const remoteQuery = container.resolve("remoteQuery")
const { price_list_id, variant_ids } = data
const variables = {
variant_id: variant_ids,
}
const query = {
product_variant_price_set: {
__args: variables,
fields: ["variant_id", "price_set_id"],
},
}
const productsWithVariantPriceSets: QueryResult[] = await remoteQuery(query)
const priceSetIds = productsWithVariantPriceSets.map(
(variantPriceSet) => variantPriceSet.price_set_id
)
return { priceSetIds, priceListId: price_list_id }
}
prepareCreatePriceLists.aliases = {
payload: "payload",
}
type QueryResult = {
price_set_id: string
variant_id: string
}
const defaultAdminProductRemoteQueryObject = {
fields: ["id"],
variants: {
price: {
fields: ["variant_id", "price_set_id"],
},
},
}
@@ -0,0 +1,87 @@
import {
PriceListPriceDTO,
UpdatePriceListDTO,
WorkflowTypes,
} from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type Result = {
priceLists: UpdatePriceListDTO[]
priceListPricesMap: Map<string, PriceListPriceDTO[]>
}
export async function prepareUpdatePriceLists({
container,
data,
}: WorkflowArguments<{
price_lists: WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowDTO[]
}>): Promise<Result> {
const { price_lists: priceListsData } = data
const remoteQuery = container.resolve("remoteQuery")
const variantPriceSetMap = new Map<string, string>()
const priceListPricesMap = new Map<string, PriceListPriceDTO[]>()
const variantIds = priceListsData
.map((priceListData) => priceListData.prices?.map((p) => p.variant_id))
.flat()
const variables = {
variant_id: variantIds,
}
const query = {
product_variant_price_set: {
__args: variables,
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
for (const { variant_id, price_set_id } of variantPriceSets) {
variantPriceSetMap.set(variant_id, price_set_id)
}
const priceLists = priceListsData.map((priceListData) => {
const priceListPrices: PriceListPriceDTO[] = []
priceListData.prices?.forEach((price) => {
const { variant_id, ...priceData } = price
if (!variant_id) {
return
}
priceListPrices.push({
id: priceData.id,
price_set_id: variantPriceSetMap.get(variant_id) as string,
currency_code: priceData.currency_code as string,
amount: priceData.amount,
min_quantity: priceData.min_quantity,
max_quantity: priceData.max_quantity,
})
return
})
priceListPricesMap.set(priceListData.id, priceListPrices)
delete priceListData?.prices
const priceListDataClone: UpdatePriceListDTO = {
...priceListData,
}
if (priceListData.name) {
priceListDataClone.title = priceListData.name
}
return priceListDataClone
})
return { priceLists, priceListPricesMap }
}
prepareUpdatePriceLists.aliases = {
payload: "prepare",
}
@@ -0,0 +1,31 @@
import { IPricingModuleService, PriceListDTO } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
export async function removePriceLists({
container,
data,
}: WorkflowArguments<{
price_lists: {
price_list: PriceListDTO
}[]
}>): Promise<
{
price_list: PriceListDTO
}[]
> {
const pricingService: IPricingModuleService = container.resolve(
ModuleRegistrationName.PRICING
)
await pricingService!.deletePriceLists(
data.price_lists.map(({ price_list }) => price_list.id)
)
return data.price_lists
}
removePriceLists.aliases = {
priceLists: "priceLists",
}
@@ -0,0 +1,39 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPricingModuleService } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { prepareCreatePriceLists } from "./prepare-create-price-list"
export async function removePriceListPriceSetPrices({
container,
data,
}: WorkflowArguments<{
priceSetIds: string[]
priceListId: string
}>): Promise<string[]> {
const { priceSetIds, priceListId } = data
const pricingService: IPricingModuleService = container.resolve(
ModuleRegistrationName.PRICING
)
const priceSetMoneyAmounts = await pricingService.listPriceSetMoneyAmounts(
{
price_set_id: priceSetIds,
price_list_id: [priceListId],
},
{
relations: ["money_amount"],
}
)
const moneyAmountIDs = priceSetMoneyAmounts
.map((priceSetMoneyAmount) => priceSetMoneyAmount.money_amount?.id)
.filter((moneyAmountId): moneyAmountId is string => !!moneyAmountId)
await pricingService.deleteMoneyAmounts(moneyAmountIDs)
return moneyAmountIDs
}
prepareCreatePriceLists.aliases = {
payload: "payload",
}
@@ -0,0 +1,29 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPricingModuleService } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type Result = {
deletedPriceIds: string[]
}
export async function removePrices({
container,
data,
}: WorkflowArguments<{
moneyAmountIds: string[]
}>): Promise<Result> {
const { moneyAmountIds } = data
const pricingService: IPricingModuleService = container.resolve(
ModuleRegistrationName.PRICING
)
await pricingService.deleteMoneyAmounts(moneyAmountIds)
return {
deletedPriceIds: moneyAmountIds,
}
}
removePrices.aliases = {
payload: "payload",
}
@@ -0,0 +1,63 @@
import {
AddPriceListPricesDTO,
IPricingModuleService,
PriceListDTO,
PriceListPriceDTO,
UpdateMoneyAmountDTO,
UpdatePriceListDTO,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type Result = {
priceLists: PriceListDTO[]
}
export async function updatePriceLists({
container,
data,
}: WorkflowArguments<{
priceLists: UpdatePriceListDTO[]
priceListPricesMap: Map<string, PriceListPriceDTO[]>
}>): Promise<Result> {
const { priceLists: priceListsData, priceListPricesMap } = data
const pricingService: IPricingModuleService = container.resolve(
ModuleRegistrationName.PRICING
)
const priceLists = await pricingService.updatePriceLists(priceListsData)
const addPriceListPricesData: AddPriceListPricesDTO[] = []
const moneyAmountsToUpdate: UpdateMoneyAmountDTO[] = []
for (const [priceListId, prices] of priceListPricesMap.entries()) {
const moneyAmountsToCreate: PriceListPriceDTO[] = []
for (const price of prices) {
if (price.id) {
moneyAmountsToUpdate.push(price as UpdateMoneyAmountDTO)
} else {
moneyAmountsToCreate.push(price)
}
}
addPriceListPricesData.push({
priceListId,
prices: moneyAmountsToCreate,
})
}
if (addPriceListPricesData.length) {
await pricingService.addPriceListPrices(addPriceListPricesData)
}
if (moneyAmountsToUpdate.length) {
await pricingService.updateMoneyAmounts(moneyAmountsToUpdate)
}
return { priceLists }
}
updatePriceLists.aliases = {
payload: "updatePriceLists",
}
@@ -0,0 +1,52 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { promiseAll } from "@medusajs/utils"
type ProductHandle = string
type SalesChannelId = string
type PartialProduct = { handle: string; id: string }
type HandlerInput = {
productsHandleSalesChannelsMap: Map<ProductHandle, SalesChannelId[]>
products: PartialProduct[]
}
export async function attachSalesChannelToProducts({
container,
context,
data,
}: WorkflowArguments<HandlerInput>): Promise<void> {
const { manager } = context
const productsHandleSalesChannelsMap = data.productsHandleSalesChannelsMap
const products = data.products
const salesChannelService = container.resolve("salesChannelService")
const salesChannelServiceTx = salesChannelService.withTransaction(manager)
const salesChannelIdProductIdsMap = new Map<ProductHandle, SalesChannelId[]>()
products.forEach((product) => {
const salesChannelIds = productsHandleSalesChannelsMap.get(product.handle!)
if (salesChannelIds) {
salesChannelIds.forEach((salesChannelId) => {
const productIds = salesChannelIdProductIdsMap.get(salesChannelId) || []
productIds.push(product.id)
salesChannelIdProductIdsMap.set(salesChannelId, productIds)
})
}
})
await promiseAll(
Array.from(salesChannelIdProductIdsMap.entries()).map(
async ([salesChannelId, productIds]) => {
return await salesChannelServiceTx.addProducts(
salesChannelId,
productIds
)
}
)
)
}
attachSalesChannelToProducts.aliases = {
products: "products",
}
@@ -0,0 +1,48 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { promiseAll } from "@medusajs/utils"
type ProductHandle = string
type ShippingProfileId = string
type PartialProduct = { handle: string; id: string }
type handlerInput = {
productsHandleShippingProfileIdMap: Map<ProductHandle, ShippingProfileId>
products: PartialProduct[]
}
export async function attachShippingProfileToProducts({
container,
context,
data,
}: WorkflowArguments<handlerInput>): Promise<void> {
const { manager } = context
const productsHandleShippingProfileIdMap =
data.productsHandleShippingProfileIdMap
const products = data.products
const shippingProfileService = container.resolve("shippingProfileService")
const shippingProfileServiceTx =
shippingProfileService.withTransaction(manager)
const profileIdProductIdsMap = new Map<ShippingProfileId, ProductHandle[]>()
products.forEach((product) => {
const profileId = productsHandleShippingProfileIdMap.get(product.handle!)
if (profileId) {
const productIds = profileIdProductIdsMap.get(profileId) || []
productIds.push(product.id)
profileIdProductIdsMap.set(profileId, productIds)
}
})
await promiseAll(
Array.from(profileIdProductIdsMap.entries()).map(
async ([profileId, productIds]) => {
return await shippingProfileServiceTx.addProducts(profileId, productIds)
}
)
)
}
attachShippingProfileToProducts.aliases = {
products: "products",
}
@@ -0,0 +1,67 @@
import { ProductWorkflow, WorkflowTypes } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type VariantPrice = {
region_id?: string
currency_code?: string
amount: number
min_quantity?: number
max_quantity?: number
}
export type CreateProductVariantsPreparedData = {
productVariants: ProductWorkflow.CreateProductVariantsInputDTO[]
variantIndexPricesMap: Map<number, VariantPrice[]>
productVariantsMap: Map<
string,
ProductWorkflow.CreateProductVariantsInputDTO[]
>
}
export async function createProductVariantsPrepareData({
container,
data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.CreateProductVariantsWorkflowInputDTO>): Promise<CreateProductVariantsPreparedData> {
const featureFlagRouter = container.resolve("featureFlagRouter")
const productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] =
data.productVariants || []
const variantIndexPricesMap = new Map<number, VariantPrice[]>()
const productVariantsMap = new Map<
string,
ProductWorkflow.CreateProductVariantsInputDTO[]
>()
for (const [index, productVariantData] of productVariants.entries()) {
if (!productVariantData.product_id) {
continue
}
variantIndexPricesMap.set(index, productVariantData.prices || [])
delete productVariantData.prices
const productVariants = productVariantsMap.get(
productVariantData.product_id
)
if (productVariants) {
productVariants.push(productVariantData)
} else {
productVariantsMap.set(productVariantData.product_id, [
productVariantData,
])
}
}
return {
productVariants,
variantIndexPricesMap,
productVariantsMap,
}
}
createProductVariantsPrepareData.aliases = {
payload: "payload",
}
@@ -0,0 +1,46 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { ProductTypes } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type VariantPrice = {
region_id?: string
currency_code?: string
amount: number
min_quantity?: number
max_quantity?: number
}
type HandlerInput = {
productVariantsMap: Map<string, ProductTypes.CreateProductVariantDTO[]>
variantIndexPricesMap: Map<number, VariantPrice[]>
}
export async function createProductVariants({
container,
data,
}: WorkflowArguments<HandlerInput>): Promise<{
productVariants: ProductTypes.ProductVariantDTO[]
variantPricesMap: Map<string, VariantPrice[]>
}> {
const { productVariantsMap, variantIndexPricesMap } = data
const variantPricesMap = new Map<string, VariantPrice[]>()
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
const productVariants = await productModuleService.createVariants(
[...productVariantsMap.values()].flat()
)
productVariants.forEach((variant, index) => {
variantPricesMap.set(variant.id, variantIndexPricesMap.get(index) || [])
})
return {
productVariants,
variantPricesMap,
}
}
createProductVariants.aliases = {
payload: "payload",
}
@@ -0,0 +1,146 @@
import { ProductTypes, SalesChannelTypes, WorkflowTypes } from "@medusajs/types"
import {
FeatureFlagUtils,
kebabCase,
ShippingProfileUtils,
} from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type ShippingProfileId = string
type SalesChannelId = string
type ProductHandle = string
type VariantIndexAndPrices = {
index: number
prices: {
region_id?: string
currency_code?: string
amount: number
min_quantity?: number
max_quantity?: number
}[]
}
export type CreateProductsPreparedData = {
products: ProductTypes.CreateProductDTO[]
productsHandleShippingProfileIdMap: Map<ProductHandle, ShippingProfileId>
productsHandleSalesChannelsMap: Map<ProductHandle, SalesChannelId[]>
productsHandleVariantsIndexPricesMap: Map<
ProductHandle,
VariantIndexAndPrices[]
>
}
export async function createProductsPrepareData({
container,
context,
data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.CreateProductsWorkflowInputDTO>): Promise<CreateProductsPreparedData> {
const { manager } = context
let products = data.products
const shippingProfileService = container
.resolve("shippingProfileService")
.withTransaction(manager)
const featureFlagRouter = container.resolve("featureFlagRouter")
const salesChannelService = container
.resolve("salesChannelService")
.withTransaction(manager)
const salesChannelFeatureFlagKey =
FeatureFlagUtils.SalesChannelFeatureFlag.key
const shippingProfileServiceTx =
shippingProfileService.withTransaction(manager)
const shippingProfiles = await shippingProfileServiceTx.list({
type: [
ShippingProfileUtils.ShippingProfileType.DEFAULT,
ShippingProfileUtils.ShippingProfileType.GIFT_CARD,
],
})
const defaultShippingProfile = shippingProfiles.find(
(sp) => sp.type === ShippingProfileUtils.ShippingProfileType.DEFAULT
)
const gitCardShippingProfile = shippingProfiles.find(
(sp) => sp.type === ShippingProfileUtils.ShippingProfileType.GIFT_CARD
)
let defaultSalesChannel: SalesChannelTypes.SalesChannelDTO | undefined
if (featureFlagRouter.isFeatureEnabled(salesChannelFeatureFlagKey)) {
defaultSalesChannel = await salesChannelService
.withTransaction(manager)
.retrieveDefault()
}
const productsHandleShippingProfileIdMap = new Map<
ProductHandle,
ShippingProfileId
>()
const productsHandleSalesChannelsMap = new Map<
ProductHandle,
SalesChannelId[]
>()
const productsHandleVariantsIndexPricesMap = new Map<
ProductHandle,
VariantIndexAndPrices[]
>()
for (const product of products) {
product.handle ??= kebabCase(product.title)
if (product.is_giftcard) {
productsHandleShippingProfileIdMap.set(
product.handle!,
gitCardShippingProfile!.id
)
} else {
productsHandleShippingProfileIdMap.set(
product.handle!,
defaultShippingProfile!.id
)
}
if (
featureFlagRouter.isFeatureEnabled(salesChannelFeatureFlagKey) &&
!product.sales_channels?.length
) {
productsHandleSalesChannelsMap.set(product.handle!, [
defaultSalesChannel!.id,
])
} else {
productsHandleSalesChannelsMap.set(
product.handle!,
product.sales_channels!.map((s) => s.id)
)
}
if (product.variants) {
const items =
productsHandleVariantsIndexPricesMap.get(product.handle!) ?? []
product.variants.forEach((variant, index) => {
items.push({
index,
prices: variant.prices!,
})
})
productsHandleVariantsIndexPricesMap.set(product.handle!, items)
}
}
products = products.map((productData) => {
delete productData.sales_channels
return productData
})
return {
products: products as ProductTypes.CreateProductDTO[],
productsHandleShippingProfileIdMap,
productsHandleSalesChannelsMap,
productsHandleVariantsIndexPricesMap,
}
}
createProductsPrepareData.aliases = {
payload: "payload",
}
@@ -0,0 +1,23 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
type HandlerInput = {
products: ProductTypes.CreateProductDTO[]
}
export async function createProducts({
container,
data,
}: WorkflowArguments<HandlerInput>): Promise<ProductTypes.ProductDTO[]> {
const data_ = data.products
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
return await productModuleService.create(data_)
}
createProducts.aliases = {
payload: "payload",
}
@@ -0,0 +1,50 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { promiseAll } from "@medusajs/utils"
type ProductHandle = string
type SalesChannelId = string
type PartialProduct = { handle: string; id: string }
type HandlerInput = {
productsHandleSalesChannelsMap: Map<ProductHandle, SalesChannelId[]>
products: PartialProduct[]
}
export async function detachSalesChannelFromProducts({
container,
context,
data,
}: WorkflowArguments<HandlerInput>): Promise<void> {
const { manager } = context
const productsHandleSalesChannelsMap = data.productsHandleSalesChannelsMap
const products = data.products
const salesChannelService = container.resolve("salesChannelService")
const salesChannelServiceTx = salesChannelService.withTransaction(manager)
const salesChannelIdProductIdsMap = new Map<ProductHandle, SalesChannelId[]>()
products.forEach((product) => {
const salesChannelIds = productsHandleSalesChannelsMap.get(product.handle!)
if (salesChannelIds) {
salesChannelIds.forEach((salesChannelId) => {
const productIds = salesChannelIdProductIdsMap.get(salesChannelId) || []
productIds.push(product.id)
salesChannelIdProductIdsMap.set(salesChannelId, productIds)
})
}
})
await promiseAll(
Array.from(salesChannelIdProductIdsMap.entries()).map(
async ([salesChannelId, productIds]) => {
return await salesChannelServiceTx.removeProducts(
salesChannelId,
productIds
)
}
)
)
}
detachSalesChannelFromProducts.aliases = {
products: "products",
}
@@ -0,0 +1,50 @@
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { promiseAll } from "@medusajs/utils"
type ProductHandle = string
type ShippingProfileId = string
type PartialProduct = { handle: string; id: string }
type HandlerInput = {
productsHandleShippingProfileIdMap: Map<ProductHandle, ShippingProfileId>
products: PartialProduct[]
}
export async function detachShippingProfileFromProducts({
container,
context,
data,
}: WorkflowArguments<HandlerInput>): Promise<void> {
const { manager } = context
const productsHandleShippingProfileIdMap =
data.productsHandleShippingProfileIdMap
const products = data.products
const shippingProfileService = container.resolve("shippingProfileService")
const shippingProfileServiceTx =
shippingProfileService.withTransaction(manager)
const profileIdProductIdsMap = new Map<ShippingProfileId, ProductHandle[]>()
products.forEach((product) => {
const profileId = productsHandleShippingProfileIdMap.get(product.handle!)
if (profileId) {
const productIds = profileIdProductIdsMap.get(profileId) || []
productIds.push(product.id)
profileIdProductIdsMap.set(profileId, productIds)
}
})
await promiseAll(
Array.from(profileIdProductIdsMap.entries()).map(
async ([profileId, productIds]) => {
return await shippingProfileServiceTx.removeProducts(
profileId,
productIds
)
}
)
)
}
detachShippingProfileFromProducts.aliases = {
products: "products",
}
@@ -0,0 +1,19 @@
export * from "./attach-sales-channel-to-products"
export * from "./attach-shipping-profile-to-products"
export * from "./create-product-variants"
export * from "./create-product-variants-prepare-data"
export * from "./create-products"
export * from "./create-products-prepare-data"
export * from "./detach-sales-channel-from-products"
export * from "./detach-shipping-profile-from-products"
export * from "./list-products"
export * from "./remove-product-variants"
export * from "./remove-products"
export * from "./revert-update-products"
export * from "./revert-variant-prices"
export * from "./update-product-variants"
export * from "./update-product-variants-prepare-data"
export * from "./update-products"
export * from "./update-products-prepare-data"
export * from "./update-products-variants-prices"
export * from "./upsert-variant-prices"
@@ -0,0 +1,47 @@
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type HandlerInput = {
ids: string[]
config?: WorkflowTypes.CommonWorkflow.WorkflowInputConfig
}
export async function listProducts({
container,
context,
data,
}: // TODO: should return product DTO or priced product but needs to be created in the types package
WorkflowArguments<HandlerInput>): Promise<ProductTypes.ProductDTO[]> {
const { manager } = context
const productIds = data.ids
const listConfig = data.config?.listConfig ?? {}
const productService = container.resolve("productService")
const pricingService = container.resolve("pricingService")
const config = {}
let shouldUseConfig = false
if (listConfig.select) {
shouldUseConfig = !!listConfig.select.length
Object.assign(config, { select: listConfig.select })
}
if (listConfig.relations) {
shouldUseConfig = shouldUseConfig || !!listConfig.relations.length
Object.assign(config, { relations: listConfig.relations })
}
const rawProducts = await productService
.withTransaction(manager as any)
.list({ id: productIds }, shouldUseConfig ? config : undefined)
return await pricingService
.withTransaction(manager as any)
.setProductPrices(rawProducts)
}
listProducts.aliases = {
ids: "ids",
}
@@ -0,0 +1,26 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type HandlerInput = { productVariants: { id: string }[] }
export async function removeProductVariants({
container,
data,
}: WorkflowArguments<HandlerInput>): Promise<void> {
if (!data.productVariants.length) {
return
}
const productModuleService: IProductModuleService = container.resolve(
ModulesDefinition[Modules.PRODUCT].registrationName
)
await productModuleService.deleteVariants(
data.productVariants.map((p) => p.id)
)
}
removeProductVariants.aliases = {
productVariants: "productVariants",
}
@@ -0,0 +1,23 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
type HandlerInput = { products: { id: string }[] }
export async function removeProducts({
container,
data,
}: WorkflowArguments<HandlerInput>): Promise<void> {
if (!data.products.length) {
return
}
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
await productModuleService.softDelete(data.products.map((p) => p.id))
}
removeProducts.aliases = {
products: "products",
}
@@ -0,0 +1,38 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import {
ProductDTO,
ProductTypes,
ProductVariantDTO,
UpdateProductDTO,
} from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
import { UpdateProductsPreparedData } from "./update-products-prepare-data"
type HandlerInput = UpdateProductsPreparedData & {
variants: ProductVariantDTO[]
}
export async function revertUpdateProducts({
container,
data,
}: WorkflowArguments<HandlerInput>): Promise<ProductDTO[]> {
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
// restore variants that have been soft deleted during update products step
await productModuleService.restoreVariants(data.variants.map((v) => v.id))
data.originalProducts.forEach((product) => {
// @ts-ignore
product.variants = product.variants.map((v) => ({ id: v.id }))
})
return await productModuleService.update(
data.originalProducts as unknown as UpdateProductDTO[]
)
}
revertUpdateProducts.aliases = {
preparedData: "preparedData",
variants: "variants",
}
@@ -0,0 +1,49 @@
import { PricingTypes } from "@medusajs/types"
import { MedusaV2Flag } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type HandlerInput = {
createdLinks: Record<any, any>[]
originalMoneyAmounts: PricingTypes.MoneyAmountDTO[]
createdPriceSets: PricingTypes.PriceSetDTO[]
}
export async function revertVariantPrices({
container,
context,
data,
}: WorkflowArguments<HandlerInput>): Promise<void> {
const {
createdLinks = [],
originalMoneyAmounts = [],
createdPriceSets = [],
} = data
const featureFlagRouter = container.resolve("featureFlagRouter")
const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
if (!isPricingDomainEnabled) {
return
}
const pricingModuleService = container.resolve("pricingModuleService")
const remoteLink = container.resolve("remoteLink")
await remoteLink.remove(createdLinks)
if (originalMoneyAmounts.length) {
await pricingModuleService.updateMoneyAmounts(originalMoneyAmounts)
}
if (createdPriceSets.length) {
await pricingModuleService.delete({
id: createdPriceSets.map((cps) => cps.id),
})
}
}
revertVariantPrices.aliases = {
productVariantsPrices: "productVariantsPrices",
}
@@ -0,0 +1,95 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { ProductTypes, ProductWorkflow, WorkflowTypes } from "@medusajs/types"
import { MedusaV2Flag } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type VariantPrice = {
region_id?: string
currency_code?: string
amount: number
min_quantity?: number
max_quantity?: number
}
export type UpdateProductVariantsPreparedData = {
productVariants: ProductWorkflow.UpdateProductVariantsInputDTO[]
variantPricesMap: Map<string, VariantPrice[]>
productVariantsMap: Map<
string,
ProductWorkflow.UpdateProductVariantsInputDTO[]
>
}
export async function updateProductVariantsPrepareData({
container,
context,
data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.UpdateProductVariantsWorkflowInputDTO>): Promise<UpdateProductVariantsPreparedData> {
const featureFlagRouter = container.resolve("featureFlagRouter")
const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
let productVariants: ProductWorkflow.UpdateProductVariantsInputDTO[] =
data.productVariants || []
const variantsDataMap = new Map<
string,
ProductWorkflow.UpdateProductVariantsInputDTO
>(
productVariants.map((productVariantData) => [
productVariantData.id,
productVariantData,
])
)
const variantIds = productVariants.map((pv) => pv.id) as string[]
const productVariantsMap = new Map<
string,
ProductWorkflow.UpdateProductVariantsInputDTO[]
>()
const variantPricesMap = new Map<string, VariantPrice[]>()
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
const variantsWithProductIds = await productModuleService.listVariants(
{
id: variantIds,
},
{
select: ["id", "product_id"],
}
)
for (const variantWithProductID of variantsWithProductIds) {
const variantData = variantsDataMap.get(variantWithProductID.id)
if (!variantData) {
continue
}
variantPricesMap.set(variantWithProductID.id, variantData.prices || [])
if (isPricingDomainEnabled) {
delete variantData.prices
}
const variantsData: ProductWorkflow.UpdateProductVariantsInputDTO[] =
productVariantsMap.get(variantWithProductID.product_id) || []
if (variantData) {
variantsData.push(variantData)
}
productVariantsMap.set(variantWithProductID.product_id, variantsData)
}
return {
productVariants,
variantPricesMap,
productVariantsMap,
}
}
updateProductVariantsPrepareData.aliases = {
payload: "payload",
}
@@ -0,0 +1,40 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { ProductTypes, UpdateProductVariantOnlyDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type HandlerInput = {
productVariantsMap: Map<string, ProductTypes.UpdateProductVariantDTO[]>
}
export async function updateProductVariants({
container,
data,
}: WorkflowArguments<HandlerInput>): Promise<
ProductTypes.UpdateProductVariantDTO[]
> {
const { productVariantsMap } = data
const productsVariants: ProductTypes.UpdateProductVariantDTO[] = []
const updateVariantsData: ProductTypes.UpdateProductVariantOnlyDTO[] = []
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
for (const [product_id, variantsUpdateData = []] of productVariantsMap) {
updateVariantsData.push(
...(variantsUpdateData as unknown as UpdateProductVariantOnlyDTO[]).map(
(update) => ({ ...update, product_id })
)
)
productsVariants.push(...variantsUpdateData)
}
if (updateVariantsData.length) {
await productModuleService.updateVariants(updateVariantsData)
}
return productsVariants
}
updateProductVariants.aliases = {
payload: "payload",
}
@@ -0,0 +1,107 @@
import { ProductDTO, SalesChannelDTO, WorkflowTypes } from "@medusajs/types"
import { MedusaV2Flag } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type ProductWithSalesChannelsDTO = ProductDTO & {
sales_channels?: SalesChannelDTO[]
}
type VariantPrice = {
region_id?: string
currency_code?: string
amount: number
min_quantity?: number
max_quantity?: number
}
export type UpdateProductsPreparedData = {
originalProducts: ProductWithSalesChannelsDTO[]
productHandleAddedChannelsMap: Map<string, string[]>
productHandleRemovedChannelsMap: Map<string, string[]>
variantPricesMap: Map<string, VariantPrice[]>
}
export async function updateProductsPrepareData({
container,
context,
data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.UpdateProductsWorkflowInputDTO>): Promise<UpdateProductsPreparedData> {
const featureFlagRouter = container.resolve("featureFlagRouter")
const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
const variantPricesMap = new Map<string, VariantPrice[]>()
const ids = data.products.map((product) => product.id)
const productHandleAddedChannelsMap = new Map<string, string[]>()
const productHandleRemovedChannelsMap = new Map<string, string[]>()
const productService = container.resolve("productService")
const productServiceTx = productService.withTransaction(context.manager)
const products = await productServiceTx.list(
// TODO: use RemoteQuery - sales_channels needs to be added to the joiner config
{ id: ids },
{
relations: [
"variants",
"variants.options",
"images",
"options",
"tags",
"collection",
"sales_channels",
],
}
)
data.products.forEach((productInput) => {
const removedChannels: string[] = []
const addedChannels: string[] = []
const currentProduct = products.find(
(p) => p.id === productInput.id
) as unknown as ProductWithSalesChannelsDTO
if (productInput.sales_channels) {
productInput.sales_channels.forEach((channel) => {
if (
!currentProduct.sales_channels?.find((sc) => sc.id === channel.id)
) {
addedChannels.push(channel.id)
}
})
currentProduct.sales_channels?.forEach((channel) => {
if (!productInput.sales_channels!.find((sc) => sc.id === channel.id)) {
removedChannels.push(channel.id)
}
})
}
for (const variantInput of productInput.variants || []) {
if (variantInput.id) {
variantPricesMap.set(variantInput.id, variantInput.prices || [])
}
if (isPricingDomainEnabled) {
delete variantInput.prices
}
}
productHandleAddedChannelsMap.set(currentProduct.handle!, addedChannels)
productHandleRemovedChannelsMap.set(currentProduct.handle!, removedChannels)
})
return {
originalProducts: products,
productHandleAddedChannelsMap,
productHandleRemovedChannelsMap,
variantPricesMap,
}
}
updateProductsPrepareData.aliases = {
preparedData: "preparedData",
}
@@ -0,0 +1,132 @@
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { MedusaError, MedusaV2Flag } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type ProductHandle = string
type VariantIndexAndPrices = {
index: number
prices: WorkflowTypes.ProductWorkflow.CreateProductVariantPricesInputDTO[]
}
type HandlerInput = {
productsHandleVariantsIndexPricesMap: Map<
ProductHandle,
VariantIndexAndPrices[]
>
products: ProductTypes.ProductDTO[]
}
export async function updateProductsVariantsPrices({
container,
context,
data,
}: WorkflowArguments<HandlerInput>) {
const { manager } = context
const products = data.products
const productsHandleVariantsIndexPricesMap =
data.productsHandleVariantsIndexPricesMap
const productVariantService = container.resolve("productVariantService")
const regionService = container.resolve("regionService")
const featureFlagRouter = container.resolve("featureFlagRouter")
const productVariantServiceTx = productVariantService.withTransaction(manager)
const variantIdsPricesData: any[] = []
const variantPricesMap = new Map<string, any[]>()
const productsMap = new Map<string, ProductTypes.ProductDTO>(
products.map((p) => [p.handle!, p])
)
const regionIds = new Set()
for (const mapData of productsHandleVariantsIndexPricesMap.entries()) {
const [handle, variantData] = mapData
const product = productsMap.get(handle)
if (!product) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Product with handle ${handle} not found`
)
}
variantData.forEach((item, index) => {
const variant = product.variants[index]
variantIdsPricesData.push({
variantId: variant.id,
prices: item.prices,
})
const prices: any[] = []
variantPricesMap.set(variant.id, prices)
item.prices.forEach((price) => {
const obj = {
amount: price.amount,
currency_code: price.currency_code,
rules: {},
}
if (price.region_id) {
regionIds.add(price.region_id)
;(obj as any).region_id = price.region_id
}
prices.push(obj)
})
})
}
if (regionIds.size) {
const regions = await regionService.list({
id: [...regionIds],
})
const regionMap = new Map<string, any>(regions.map((r) => [r.id, r]))
for (const [, prices] of variantPricesMap.entries()) {
prices.forEach((price) => {
if (price.region_id) {
const region = regionMap.get(price.region_id)
price.currency_code = region?.currency_code
price.rules = {
region_id: price.region_id,
}
delete price.region_id
}
})
}
}
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const remoteLink = container.resolve("remoteLink")
const pricingModuleService = container.resolve(
ModuleRegistrationName.PRICING
)
const priceSetsToCreate = variantIdsPricesData.map(({ variantId }) => ({
rules: [{ rule_attribute: "region_id" }],
prices: variantPricesMap.get(variantId),
}))
const priceSets = await pricingModuleService.create(priceSetsToCreate)
const links = priceSets.map((priceSet, index) => ({
productService: {
variant_id: variantIdsPricesData[index].variantId,
},
pricingService: {
price_set_id: priceSet.id,
},
}))
await remoteLink.create(links)
} else {
await productVariantServiceTx.updateVariantPrices(variantIdsPricesData)
}
}
updateProductsVariantsPrices.aliases = {
products: "products",
}
@@ -0,0 +1,44 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { ProductDTO, ProductTypes } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type HandlerInput = {
products: ProductTypes.UpdateProductDTO[]
}
export async function updateProducts({
container,
context,
data,
}: WorkflowArguments<HandlerInput>): Promise<ProductDTO[]> {
if (!data.products.length) {
return []
}
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
const products = await productModuleService.update(data.products)
return await productModuleService.list(
{ id: products.map((p) => p.id) },
{
relations: [
"variants",
"variants.options",
"images",
"options",
"tags",
// "type",
"collection",
// "profiles",
// "sales_channels",
],
}
)
}
updateProducts.aliases = {
products: "products",
}
@@ -0,0 +1,171 @@
import { PricingTypes } from "@medusajs/types"
import { MedusaV2Flag } from "@medusajs/utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type VariantPrice = {
id?: string
region_id?: string
currency_code: string
amount: number
min_quantity?: number
max_quantity?: number
rules: Record<string, string>
}
type RegionDTO = {
id: string
currency_code: string
}
type HandlerInput = {
variantPricesMap: Map<string, VariantPrice[]>
}
export async function upsertVariantPrices({
container,
data,
}: WorkflowArguments<HandlerInput>) {
const { variantPricesMap } = data
const featureFlagRouter = container.resolve("featureFlagRouter")
if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
return {
createdLinks: [],
originalMoneyAmounts: [],
createdPriceSets: [],
}
}
const pricingModuleService = container.resolve("pricingModuleService")
const regionService = container.resolve("regionService")
const remoteLink = container.resolve("remoteLink")
const remoteQuery = container.resolve("remoteQuery")
const variables = {
variant_id: [...variantPricesMap.keys()],
}
const query = {
product_variant_price_set: {
__args: variables,
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
const variantIdToPriceSetIdMap: Map<string, string> = new Map(
variantPriceSets.map((variantPriceSet) => [
variantPriceSet.variant_id,
variantPriceSet.price_set_id,
])
)
const moneyAmountsToUpdate: PricingTypes.UpdateMoneyAmountDTO[] = []
const createdPriceSets: PricingTypes.PriceSetDTO[] = []
const ruleSetPricesToAdd: PricingTypes.CreatePricesDTO[] = []
const linksToCreate: any[] = []
for (const [variantId, prices = []] of variantPricesMap) {
const priceSetToCreate: PricingTypes.CreatePriceSetDTO = {
rules: [{ rule_attribute: "region_id" }],
prices: [],
}
const regionIds = prices.map((price) => price.region_id)
const regions = await regionService.list({ id: regionIds })
const regionsMap: Map<string, RegionDTO> = new Map(
regions.map((region: RegionDTO) => [region.id, region])
)
for (const price of prices) {
const region = price.region_id && regionsMap.get(price.region_id)
let region_currency_code: string | undefined
let region_rules: Record<string, string> | undefined
if (region) {
region_currency_code = region.currency_code
region_rules = {
region_id: region.id,
}
}
if (price.id) {
const priceToUpdate = {
id: price.id,
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
amount: price.amount,
currency_code: region_currency_code ?? price.currency_code,
}
moneyAmountsToUpdate.push(priceToUpdate)
} else {
const variantPrice: PricingTypes.CreatePricesDTO = {
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
amount: price.amount,
currency_code: region_currency_code ?? price.currency_code,
rules: region_rules ?? {},
}
delete price.region_id
if (variantIdToPriceSetIdMap.get(variantId)) {
ruleSetPricesToAdd.push(variantPrice)
} else {
priceSetToCreate.prices?.push(variantPrice)
}
}
}
let priceSetId = variantIdToPriceSetIdMap.get(variantId)
if (priceSetId) {
await pricingModuleService.addPrices({
priceSetId,
prices: ruleSetPricesToAdd,
})
} else {
const createdPriceSet = await pricingModuleService.create(
priceSetToCreate
)
priceSetId = createdPriceSet?.id
createdPriceSets.push(createdPriceSet)
linksToCreate.push({
productService: {
variant_id: variantId,
},
pricingService: {
price_set_id: priceSetId,
},
})
}
}
const createdLinks = await remoteLink.create(linksToCreate)
let originalMoneyAmounts = await pricingModuleService.listMoneyAmounts(
{
id: moneyAmountsToUpdate.map((matu) => matu.id),
},
{
select: ["id", "currency_code", "amount", "min_quantity", "max_quantity"],
}
)
if (moneyAmountsToUpdate.length) {
await pricingModuleService.updateMoneyAmounts(moneyAmountsToUpdate)
}
return {
createdLinks,
originalMoneyAmounts,
createdPriceSets,
}
}
upsertVariantPrices.aliases = {
productVariantsPrices: "productVariantsPrices",
}
@@ -0,0 +1,49 @@
import { MedusaError } from "@medusajs/utils"
import { isDefined } from "medusa-core-utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type RegionDTO = {
region_id?: string
}
type HandlerInputData = {
region: {
region_id: string
}
}
enum Aliases {
Region = "region",
}
export async function findRegion({
container,
data,
}: WorkflowArguments<HandlerInputData>): Promise<RegionDTO> {
const regionService = container.resolve("regionService")
let regionId: string
const regionDTO: RegionDTO = {}
if (isDefined(data[Aliases.Region].region_id)) {
regionId = data[Aliases.Region].region_id
} else {
const regions = await regionService.list({}, {})
if (!regions?.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`A region is required to create a cart`
)
}
regionId = regions[0].id
}
regionDTO.region_id = regionId
return regionDTO
}
findRegion.aliases = Aliases
@@ -0,0 +1 @@
export * from "./find-region"
@@ -0,0 +1,74 @@
import { MedusaError } from "@medusajs/utils"
import { isDefined } from "medusa-core-utils"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type AttachSalesChannelDTO = {
sales_channel_id?: string
}
type HandlerInputData = {
sales_channel: {
sales_channel_id?: string
publishableApiKeyScopes?: {
sales_channel_ids?: string[]
}
}
}
enum Aliases {
SalesChannel = "sales_channel",
}
export async function findSalesChannel({
container,
data,
}: WorkflowArguments<HandlerInputData>): Promise<AttachSalesChannelDTO> {
const salesChannelService = container.resolve("salesChannelService")
const storeService = container.resolve("storeService")
let salesChannelId = data[Aliases.SalesChannel].sales_channel_id
let salesChannel
const salesChannelDTO: AttachSalesChannelDTO = {}
const publishableApiKeyScopes =
data[Aliases.SalesChannel].publishableApiKeyScopes || {}
delete data[Aliases.SalesChannel].publishableApiKeyScopes
if (
!isDefined(salesChannelId) &&
publishableApiKeyScopes?.sales_channel_ids?.length
) {
if (publishableApiKeyScopes.sales_channel_ids.length > 1) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
"The provided PublishableApiKey has multiple associated sales channels."
)
}
salesChannelId = publishableApiKeyScopes.sales_channel_ids[0]
}
if (isDefined(salesChannelId)) {
salesChannel = await salesChannelService.retrieve(salesChannelId)
} else {
salesChannel = (
await storeService.retrieve({
relations: ["default_sales_channel"],
})
).default_sales_channel
}
if (salesChannel.is_disabled) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Unable to assign the cart to a disabled Sales Channel "${salesChannel.name}"`
)
}
salesChannelDTO.sales_channel_id = salesChannel?.id
return salesChannelDTO
}
findSalesChannel.aliases = Aliases
@@ -0,0 +1 @@
export * from "./find-sales-channel"