feat(medusa,workflows) Create cart workflow (#4685)
* chore: add baseline test for create cart * chore: add basic paths into handlers + make first tests pass * chore: move input alias to cart specific workflow * chore: move data around into buckets * chore: normalize handlers and introduce types * chore: move aliases to handlers concern * chore: add compensation step for create cart * chore: merge with latest develop * chore: handle error manually + type inputs * chore: handle error manually * chore: added types for each handler * chore: remove addresses * chore: added changset * chore: undo package changes * chore: added config settings to retreieve, cleanup of types * chore: capitalize cart handlers * chore: rename todo * chore: add feature flag for workflow * chore: reorder handlers * chore: add logger to route handler * chore: removed weird vscode moving around things * chore: refactor handlers * chore: refactor compensate step * chore: changed poistion * chore: aggregate config data * chore: moved handlers to their own domain + pr review addressing * chore: address pr reviews * chore: move types to type package * chore: update type to include config * chore: remove error scoping
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { AddressDTO } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
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 "../../helper"
|
||||
|
||||
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 "../../helper"
|
||||
|
||||
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 "../../helper"
|
||||
|
||||
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 "../../helper"
|
||||
|
||||
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,2 @@
|
||||
export * from "./set-config"
|
||||
export * from "./set-context"
|
||||
@@ -0,0 +1,31 @@
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type ConfigDTO = {
|
||||
retrieveConfig?: {
|
||||
select?: string[]
|
||||
relations?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
Config = "config",
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
config: {
|
||||
retrieveConfig?: {
|
||||
select?: string[]
|
||||
relations?: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setConfig({
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<ConfigDTO> {
|
||||
return {
|
||||
retrieveConfig: data[Aliases.Config].retrieveConfig,
|
||||
}
|
||||
}
|
||||
|
||||
setConfig.aliases = Aliases
|
||||
@@ -0,0 +1,27 @@
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
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 "../../helper"
|
||||
|
||||
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"
|
||||
@@ -1,3 +1,9 @@
|
||||
export * as ProductHandlers from "./product"
|
||||
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 ProductHandlers from "./product"
|
||||
export * as RegionHandlers from "./region"
|
||||
export * as SalesChannelHandlers from "./sales-channel"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { isDefined } from "medusa-core-utils"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
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 "../../helper"
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user