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:
Riqwan Thamir
2023-08-08 12:10:27 +02:00
committed by GitHub
parent a42c41e8ab
commit 281b0746cf
35 changed files with 1140 additions and 13 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/workflows": patch
"@medusajs/medusa": patch
"@medusajs/types": patch
---
feat(medusa,workflows,types) Create cart workflow

View File

@@ -0,0 +1,210 @@
import { MoneyAmount, PriceList, Region } from "@medusajs/medusa"
import path from "path"
import { bootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import setupServer from "../../../../environment-helpers/setup-server"
import { setPort, useApi } from "../../../../environment-helpers/use-api"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { simpleProductFactory } from "../../../../factories"
jest.setTimeout(30000)
describe("/store/carts", () => {
let medusaProcess
let dbConnection
let express
const doAfterEach = async () => {
const db = useDb()
return await db.teardown()
}
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd, verbose: true })
const { app, port } = await bootstrapApp({ cwd })
setPort(port)
express = app.listen(port, () => {
process.send?.(port)
})
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
medusaProcess.kill()
})
describe("POST /store/carts", () => {
let prod1
let prodSale
beforeEach(async () => {
const manager = dbConnection.manager
await manager.insert(Region, {
id: "region",
name: "Test Region",
currency_code: "usd",
tax_rate: 0,
})
await manager.query(
`UPDATE "country"
SET region_id='region'
WHERE iso_2 = 'us'`
)
prod1 = await simpleProductFactory(dbConnection, {
id: "test-product",
variants: [{ id: "test-variant_1" }],
})
prodSale = await simpleProductFactory(dbConnection, {
id: "test-product-sale",
variants: [
{
id: "test-variant-sale",
prices: [{ amount: 1000, currency: "usd" }],
},
],
})
})
afterEach(async () => {
await doAfterEach()
})
it("should create a cart", async () => {
const api = useApi()
const response = await api.post("/store/carts")
expect(response.status).toEqual(200)
const getRes = await api.post(`/store/carts/${response.data.cart.id}`)
expect(getRes.status).toEqual(200)
})
it("should fail to create a cart when no region exist", async () => {
const api = useApi()
await dbConnection.manager.query(
`UPDATE "country"
SET region_id=null
WHERE iso_2 = 'us'`
)
await dbConnection.manager.query(`DELETE from region`)
try {
await api.post("/store/carts")
} catch (error) {
expect(error.response.status).toEqual(400)
expect(error.response.data.message).toEqual(
"A region is required to create a cart"
)
}
})
it("should create a cart with items", async () => {
const yesterday = ((today) =>
new Date(today.setDate(today.getDate() - 1)))(new Date())
const tomorrow = ((today) =>
new Date(today.setDate(today.getDate() + 1)))(new Date())
const priceList1 = await dbConnection.manager.create(PriceList, {
id: "pl_current",
name: "Past winter sale",
description: "Winter sale for key accounts.",
type: "sale",
status: "active",
starts_at: yesterday,
ends_at: tomorrow,
})
await dbConnection.manager.save(priceList1)
const ma_sale_1 = dbConnection.manager.create(MoneyAmount, {
variant_id: prodSale.variants[0].id,
currency_code: "usd",
amount: 800,
price_list_id: "pl_current",
})
await dbConnection.manager.save(ma_sale_1)
const api = useApi()
const response = await api
.post("/store/carts", {
items: [
{
variant_id: prod1.variants[0].id,
quantity: 1,
},
{
variant_id: prodSale.variants[0].id,
quantity: 2,
},
],
})
.catch((err) => console.log(err))
response.data.cart.items.sort((a, b) => a.quantity - b.quantity)
expect(response.status).toEqual(200)
expect(response.data.cart.items).toHaveLength(2)
expect(response.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
variant_id: prod1.variants[0].id,
quantity: 1,
}),
expect.objectContaining({
variant_id: prodSale.variants[0].id,
quantity: 2,
unit_price: 800,
}),
])
)
const getRes = await api.post(`/store/carts/${response.data.cart.id}`)
expect(getRes.status).toEqual(200)
})
it("should create a cart with country", async () => {
const api = useApi()
const response = await api.post("/store/carts", {
country_code: "us",
})
expect(response.status).toEqual(200)
expect(response.data.cart.shipping_address.country_code).toEqual("us")
const getRes = await api.post(`/store/carts/${response.data.cart.id}`)
expect(getRes.status).toEqual(200)
})
it("should create a cart with context", async () => {
const api = useApi()
const response = await api.post("/store/carts", {
context: {
test_id: "test",
},
})
expect(response.status).toEqual(200)
const getRes = await api.post(`/store/carts/${response.data.cart.id}`)
expect(getRes.status).toEqual(200)
const cart = getRes.data.cart
expect(cart.context).toEqual({
ip: expect.any(String),
user_agent: expect.stringContaining("axios/0.21."),
test_id: "test",
})
})
})
})

View File

@@ -35,6 +35,7 @@ module.exports = {
featureFlags: {
workflows: {
[Workflows.CreateProducts]: true,
[Workflows.CreateCart]: true,
},
},
modules: {

View File

@@ -1,3 +1,8 @@
import { MedusaContainer } from "@medusajs/modules-sdk"
import {
Workflows,
createCart as createCartWorkflow,
} from "@medusajs/workflows"
import { Type } from "class-transformer"
import {
IsArray,
@@ -7,10 +12,11 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { isDefined, MedusaError } from "medusa-core-utils"
import { MedusaError, isDefined } from "medusa-core-utils"
import reqIp from "request-ip"
import { EntityManager } from "typeorm"
import { Logger } from "@medusajs/types"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { Cart, LineItem } from "../../../../models"
@@ -75,18 +81,56 @@ import { FlagRouter } from "../../../../utils/flag-router"
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const entityManager: EntityManager = req.scope.resolve("manager")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const validated = req.validatedBody as StorePostCartReq
const logger: Logger = req.scope.resolve("logger")
const reqContext = {
ip: reqIp.getClientIp(req),
user_agent: req.get("user-agent"),
}
const isWorkflowEnabled = featureFlagRouter.isFeatureEnabled({
workflows: Workflows.CreateCart,
})
if (isWorkflowEnabled) {
const cartWorkflow = createCartWorkflow(req.scope as MedusaContainer)
const input = {
...validated,
publishableApiKeyScopes: req.publishableApiKeyScopes,
context: {
...reqContext,
...validated.context,
},
config: {
retrieveConfig: {
select: defaultStoreCartFields,
relations: defaultStoreCartRelations,
},
},
}
const { result, errors } = await cartWorkflow.run({
input,
context: {
manager: entityManager,
},
throwOnError: false,
})
if (Array.isArray(errors)) {
if (isDefined(errors[0])) {
throw errors[0].error
}
}
return res.status(200).json({ cart: cleanResponseData(result, []) })
}
const lineItemService: LineItemService = req.scope.resolve("lineItemService")
const cartService: CartService = req.scope.resolve("cartService")
const regionService: RegionService = req.scope.resolve("regionService")
const entityManager: EntityManager = req.scope.resolve("manager")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
let regionId!: string
if (isDefined(validated.region_id)) {

View File

@@ -0,0 +1,15 @@
export type AddressDTO = {
id?: string
address_1: string
address_2?: string | null
company?: string | null
country_code: string
city?: string | null
phone?: string | null
postal_code?: string | null
province?: string | null
metadata?: Record<string, unknown> | null
created_at?: string | Date
updated_at?: string | Date
deleted_at?: string | Date | null
}

View File

@@ -0,0 +1 @@
export * from "./common"

View File

@@ -0,0 +1,27 @@
export type CartDTO = {
id?: string
email?: string
billing_address_id?: string
shipping_address_id?: string
region_id?: string
customer_id?: string
payment_id?: string
completed_at?: Date
payment_authorized_at?: Date
idempotency_key?: string
context?: Record<string, unknown>
metadata?: Record<string, unknown>
sales_channel_id?: string | null
shipping_total?: number
discount_total?: number
raw_discount_total?: number
item_tax_total?: number | null
shipping_tax_total?: number | null
tax_total?: number | null
refunded_total?: number
total?: number
subtotal?: number
refundable_amount?: number
gift_card_total?: number
gift_card_tax_total?: number
}

View File

@@ -0,0 +1 @@
export * from "./common"

View File

@@ -1,18 +1,20 @@
export * from "./address"
export * from "./bundles"
export * from "./cache"
export * from "./cart"
export * from "./common"
export * from "./dal"
export * from "./event-bus"
export * from "./feature-flag"
export * from "./inventory"
export * from "./joiner"
export * from "./logger"
export * from "./modules-sdk"
export * from "./product"
export * from "./product-category"
export * from "./sales-channel"
export * from "./search"
export * from "./shared-context"
export * from "./stock-location"
export * from "./transaction-base"
export * from "./dal"
export * from "./logger"
export * from "./feature-flag"
export * from "./sales-channel"
export * from "./workflow"

View File

@@ -0,0 +1,22 @@
import { AddressDTO } from "../../address"
import { WorkflowInputConfig } from "../common"
export interface CreateLineItemInputDTO {
variant_id: string
quantity: number
}
export interface CreateCartWorkflowInputDTO {
config?: WorkflowInputConfig
region_id?: string
country_code?: string
items?: CreateLineItemInputDTO[]
context?: object
sales_channel_id?: string
shipping_address_id?: string
billing_address_id?: string
billing_address?: AddressDTO
shipping_address?: AddressDTO
customer_id?: string
email?: string
}

View File

@@ -0,0 +1 @@
export * from "./create-cart"

View File

@@ -1,2 +1,3 @@
export * as ProductWorkflow from "./product"
export * as CartWorkflow from "./cart"
export * as CommonWorkflow from "./common"
export * as ProductWorkflow from "./product"

View File

@@ -88,10 +88,6 @@ export interface CreateProductInputDTO {
metadata?: Record<string, unknown>
sales_channels?: CreateProductSalesChannelInputDTO[]
listConfig: {
select: string[]
relations: string[]
}
}
export interface CreateProductsWorkflowInputDTO {

View File

@@ -0,0 +1,238 @@
import {
TransactionStepsDefinition,
WorkflowManager,
} from "@medusajs/orchestration"
import { CartWorkflow } from "@medusajs/types"
import { Workflows } from "../../definitions"
import {
AddressHandlers,
CartHandlers,
CommonHandlers,
CustomerHandlers,
RegionHandlers,
SalesChannelHandlers,
} from "../../handlers"
import { aggregateData, exportWorkflow, pipe } from "../../helper"
enum CreateCartActions {
setConfig = "setConfig",
setContext = "setContext",
attachLineItems = "attachLineItems",
findRegion = "findRegion",
findSalesChannel = "findSalesChannel",
createCart = "createCart",
findOrCreateAddresses = "findOrCreateAddresses",
findOrCreateCustomer = "findOrCreateCustomer",
removeCart = "removeCart",
removeAddresses = "removeAddresses",
retrieveCart = "retrieveCart",
}
const workflowAlias = "cart"
const getWorkflowInput = (alias = workflowAlias) => ({
inputAlias: workflowAlias,
invoke: {
from: workflowAlias,
alias,
},
})
const workflowSteps: TransactionStepsDefinition = {
next: [
{
action: CreateCartActions.setConfig,
noCompensation: true,
},
{
action: CreateCartActions.findOrCreateCustomer,
noCompensation: true,
},
{
action: CreateCartActions.findSalesChannel,
noCompensation: true,
},
{
action: CreateCartActions.setContext,
noCompensation: true,
},
{
action: CreateCartActions.findRegion,
noCompensation: true,
next: {
action: CreateCartActions.findOrCreateAddresses,
noCompensation: true,
next: {
action: CreateCartActions.createCart,
next: {
action: CreateCartActions.attachLineItems,
noCompensation: true,
next: {
action: CreateCartActions.retrieveCart,
noCompensation: true,
},
},
},
},
},
],
}
const handlers = new Map([
[
CreateCartActions.setConfig,
{
invoke: pipe(
getWorkflowInput(CommonHandlers.setConfig.aliases.Config),
aggregateData(),
CommonHandlers.setConfig
),
},
],
[
CreateCartActions.findOrCreateCustomer,
{
invoke: pipe(
getWorkflowInput(
CustomerHandlers.findOrCreateCustomer.aliases.Customer
),
CustomerHandlers.findOrCreateCustomer
),
},
],
[
CreateCartActions.findSalesChannel,
{
invoke: pipe(
getWorkflowInput(
SalesChannelHandlers.findSalesChannel.aliases.SalesChannel
),
SalesChannelHandlers.findSalesChannel
),
},
],
[
CreateCartActions.setContext,
{
invoke: pipe(
getWorkflowInput(CommonHandlers.setContext.aliases.Context),
CommonHandlers.setContext
),
},
],
[
CreateCartActions.findRegion,
{
invoke: pipe(
getWorkflowInput(RegionHandlers.findRegion.aliases.Region),
RegionHandlers.findRegion
),
},
],
[
CreateCartActions.findOrCreateAddresses,
{
invoke: pipe(
{
invoke: [
getWorkflowInput(
AddressHandlers.findOrCreateAddresses.aliases.Addresses
).invoke,
{
from: CreateCartActions.findRegion,
alias: AddressHandlers.findOrCreateAddresses.aliases.Region,
},
],
},
AddressHandlers.findOrCreateAddresses
),
},
],
[
CreateCartActions.createCart,
{
invoke: pipe(
{
invoke: [
{
from: CreateCartActions.findRegion,
alias: CartHandlers.createCart.aliases.Region,
},
{
from: CreateCartActions.setContext,
alias: CartHandlers.createCart.aliases.Context,
},
{
from: CreateCartActions.findOrCreateCustomer,
alias: CartHandlers.createCart.aliases.Customer,
},
{
from: CreateCartActions.findOrCreateAddresses,
alias: CartHandlers.createCart.aliases.Addresses,
},
],
},
CartHandlers.createCart
),
compensate: pipe(
{
invoke: [
{
from: CreateCartActions.createCart,
alias: CartHandlers.removeCart.aliases.Cart,
},
],
},
CartHandlers.removeCart
),
},
],
[
CreateCartActions.attachLineItems,
{
invoke: pipe(
{
invoke: [
getWorkflowInput(
CartHandlers.attachLineItemsToCart.aliases.LineItems
).invoke,
{
from: CreateCartActions.createCart,
alias: CartHandlers.attachLineItemsToCart.aliases.Cart,
},
],
},
CartHandlers.attachLineItemsToCart
),
},
],
[
CreateCartActions.retrieveCart,
{
invoke: pipe(
{
invoke: [
{
from: CreateCartActions.setConfig,
alias: CommonHandlers.setConfig.aliases.Config,
},
{
from: CreateCartActions.createCart,
alias: CartHandlers.retrieveCart.aliases.Cart,
},
],
},
CartHandlers.retrieveCart
),
},
],
])
WorkflowManager.register(Workflows.CreateCart, workflowSteps, handlers)
type CreateCartWorkflowOutput = Record<any, any>
export const createCart = exportWorkflow<
CartWorkflow.CreateCartWorkflowInputDTO,
CreateCartWorkflowOutput
>(Workflows.CreateCart, CreateCartActions.retrieveCart)

View File

@@ -0,0 +1 @@
export * from "./create-cart"

View File

@@ -1 +1,2 @@
export * from "./create-products"
export * from "./cart"

View File

@@ -1,5 +1,9 @@
export enum Workflows {
// Product workflows
CreateProducts = "create-products",
// Cart workflows
CreateCart = "create-cart",
}
export enum InputAlias {

View File

@@ -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

View File

@@ -0,0 +1 @@
export * from "./find-or-create-addresses"

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,4 @@
export * from "./attach-line-items-to-cart"
export * from "./create-cart"
export * from "./remove-cart"
export * from "./retrieve-cart"

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
export * from "./set-config"
export * from "./set-context"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
export * from "./find-or-create-customer"

View File

@@ -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"

View File

@@ -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

View File

@@ -0,0 +1 @@
export * from "./find-region"

View File

@@ -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

View File

@@ -0,0 +1 @@
export * from "./find-sales-channel"

View File

@@ -1,3 +1,4 @@
export * from "./aggregate"
export * from "./empty-handler"
export * from "./pipe"
export * from "./workflow-export"