feat(cart): POST /store/carts/:id (#6274)

Depends on:
- #6262 
- #6273
This commit is contained in:
Oli Juhl
2024-02-14 16:03:02 +01:00
committed by GitHub
parent 6500f18b9b
commit 1ed5f918c3
17 changed files with 378 additions and 49 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/types": patch
---
feat(cart): `POST /store/carts/:id`

View File

@@ -0,0 +1,63 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICartModuleService } from "@medusajs/types"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import adminSeeder from "../../../../helpers/admin-seeder"
jest.setTimeout(50000)
const env = { MEDUSA_FF_MEDUSA_V2: true }
describe("POST /store/carts/:id", () => {
let dbConnection
let appContainer
let shutdownServer
let cartModuleService: ICartModuleService
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env } as any)
shutdownServer = await startBootstrapApp({ cwd, env })
appContainer = getContainer()
cartModuleService = appContainer.resolve(ModuleRegistrationName.CART)
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
await shutdownServer()
})
beforeEach(async () => {
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should create a cart", async () => {
const api = useApi() as any
const cart = await cartModuleService.create({
currency_code: "usd",
})
const response = await api.post(`/store/carts/${cart.id}`, {
email: "tony@stark.com",
})
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
currency_code: "usd",
email: "tony@stark.com",
})
)
})
})

View File

@@ -258,6 +258,56 @@ describe("Cart Module Service", () => {
})
)
})
it("should update a cart with selector successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [updatedCart] = await service.update(
{ id: createdCart.id },
{
email: "test@email.com",
}
)
const [cart] = await service.list({ id: [createdCart.id] })
expect(cart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
email: updatedCart.email,
})
)
})
it("should update a cart with id successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const updatedCart = await service.update(
createdCart.id,
{
email: "test@email.com",
}
)
const [cart] = await service.list({ id: [createdCart.id] })
expect(cart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
email: updatedCart.email,
})
)
})
})
describe("delete", () => {

View File

@@ -190,43 +190,74 @@ export default class CartModuleService<
return createdCarts
}
async update(data: CartTypes.UpdateCartDTO[]): Promise<CartTypes.CartDTO[]>
async update(
data: CartTypes.UpdateCartDTO[],
cartId: string,
data: CartTypes.UpdateCartDataDTO,
sharedContext?: Context
): Promise<CartTypes.CartDTO>
async update(
selector: Partial<CartTypes.CartDTO>,
data: CartTypes.UpdateCartDataDTO,
sharedContext?: Context
): Promise<CartTypes.CartDTO[]>
async update(
data: CartTypes.UpdateCartDTO,
sharedContext?: Context
): Promise<CartTypes.CartDTO>
@InjectManager("baseRepository_")
async update(
data: CartTypes.UpdateCartDTO[] | CartTypes.UpdateCartDTO,
dataOrIdOrSelector:
| CartTypes.UpdateCartDTO[]
| string
| Partial<CartTypes.CartDTO>,
data?: CartTypes.UpdateCartDataDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<CartTypes.CartDTO[] | CartTypes.CartDTO> {
const input = Array.isArray(data) ? data : [data]
const carts = await this.update_(input, sharedContext)
const result = await this.update_(dataOrIdOrSelector, data, sharedContext)
const result = await this.list(
{ id: carts.map((p) => p!.id) },
{
relations: ["shipping_address", "billing_address"],
},
sharedContext
)
const serializedResult = await this.baseRepository_.serialize<
CartTypes.CartDTO[]
>(result, {
populate: true,
})
return (Array.isArray(data) ? result : result[0]) as
| CartTypes.CartDTO
| CartTypes.CartDTO[]
return isString(dataOrIdOrSelector) ? serializedResult[0] : serializedResult
}
@InjectTransactionManager("baseRepository_")
protected async update_(
data: CartTypes.UpdateCartDTO[],
dataOrIdOrSelector:
| CartTypes.UpdateCartDTO[]
| string
| Partial<CartTypes.CartDTO>,
data?: CartTypes.UpdateCartDataDTO,
@MedusaContext() sharedContext: Context = {}
) {
return await this.cartService_.update(data, sharedContext)
let toUpdate: CartTypes.UpdateCartDTO[] = []
if (isString(dataOrIdOrSelector)) {
toUpdate = [
{
id: dataOrIdOrSelector,
...data,
},
]
} else if (Array.isArray(dataOrIdOrSelector)) {
toUpdate = dataOrIdOrSelector
} else {
const carts = await this.cartService_.list(
{ ...dataOrIdOrSelector },
{ select: ["id"] },
sharedContext
)
toUpdate = carts.map((cart) => {
return {
...data,
id: cart.id,
}
})
}
const result = await this.cartService_.update(toUpdate, sharedContext)
return result
}
addLineItems(

View File

@@ -14,12 +14,12 @@ export interface CreateCartDTO {
export interface UpdateCartDTO {
id: string
region_id?: string
customer_id?: string
sales_channel_id?: string
email?: string
currency_code?: string
metadata?: Record<string, unknown>
region_id?: string | null
customer_id?: string | null
sales_channel_id?: string | null
email?: string | null
currency_code?: string | null
metadata?: Record<string, unknown> | null
adjustments?: (CreateLineItemAdjustmentDTO | UpdateLineItemAdjustmentDTO)[]
}

View File

@@ -1,2 +1,3 @@
export * from "./create-carts";
export * from "./create-carts"
export * from "./update-carts"

View File

@@ -0,0 +1,61 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
CartDTO,
FilterableCartProps,
ICartModuleService,
UpdateCartDataDTO,
} from "@medusajs/types"
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type UpdateCartsStepInput = {
selector: FilterableCartProps
update: UpdateCartDataDTO
}
export const updateCartsStepId = "update-carts"
export const updateCartsStep = createStep(
updateCartsStepId,
async (data: UpdateCartsStepInput, { container }) => {
const service = container.resolve<ICartModuleService>(
ModuleRegistrationName.CART
)
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
data.update,
])
const prevCarts = await service.list(data.selector, {
select: selects,
relations,
})
const updatedCarts = await service.update(
data.selector as Partial<CartDTO>,
data.update
)
return new StepResponse(updatedCarts, prevCarts)
},
async (previousCarts, { container }) => {
if (!previousCarts?.length) {
return
}
const service = container.resolve<ICartModuleService>(
ModuleRegistrationName.CART
)
const toRestore = previousCarts.map((c) => ({
id: c.id,
region_id: c.region_id,
customer_id: c.customer_id,
sales_channel_id: c.sales_channel_id,
email: c.email,
currency_code: c.currency_code,
metadata: c.metadata,
}))
await service.update(toRestore)
}
)

View File

@@ -1,2 +1,3 @@
export * from "./create-carts";
export * from "./create-carts"
export * from "./update-carts"

View File

@@ -0,0 +1,20 @@
import {
CartDTO,
FilterableCartProps,
UpdateCartDataDTO,
} from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updateCartsStep } from "../steps/update-carts"
type WorkflowInput = {
selector: FilterableCartProps
update: UpdateCartDataDTO
}
export const updateCartsWorkflowId = "update-carts"
export const updateCartsWorkflow = createWorkflow(
updateCartsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<CartDTO[]> => {
return updateCartsStep(input)
}
)

View File

@@ -17,6 +17,7 @@ export const config: MiddlewaresConfig = {
...adminCustomerRoutesMiddlewares,
...adminPromotionRoutesMiddlewares,
...adminCampaignRoutesMiddlewares,
...storeCartRoutesMiddlewares,
...storeCustomerRoutesMiddlewares,
...storeCartRoutesMiddlewares,
...authRoutesMiddlewares,

View File

@@ -1,4 +1,7 @@
import { updateCartsWorkflow } from "@medusajs/core-flows"
import { UpdateCartDataDTO } from "@medusajs/types"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
import { defaultStoreCartRemoteQueryObject } from "../query-config"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
@@ -17,3 +20,23 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
res.json({ cart })
}
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
const updateCartWorkflow = updateCartsWorkflow(req.scope)
const workflowInput = {
selector: { id: req.params.id },
update: req.validatedBody as UpdateCartDataDTO,
}
const { result, errors } = await updateCartWorkflow.run({
input: workflowInput,
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({ cart: result[0] })
}

View File

@@ -1,7 +1,11 @@
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import * as QueryConfig from "./query-config"
import { StoreGetCartsCartParams, StorePostCartReq } from "./validators"
import {
StoreGetCartsCartParams,
StorePostCartReq,
StorePostCartsCartReq,
} from "./validators"
export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
{
@@ -19,4 +23,9 @@ export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/store/carts",
middlewares: [transformBody(StorePostCartReq)],
},
{
method: ["POST"],
matcher: "/store/carts/:id",
middlewares: [transformBody(StorePostCartsCartReq)],
},
]

View File

@@ -1,6 +1,7 @@
import { Type } from "class-transformer"
import {
IsArray,
IsEmail,
IsInt,
IsNotEmpty,
IsObject,
@@ -8,7 +9,8 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { FindParams } from "../../../types/common"
import { AddressPayload, FindParams } from "../../../types/common"
import { IsType } from "../../../utils"
export class StoreGetCartsCartParams extends FindParams {}
@@ -35,6 +37,7 @@ export class StorePostCartReq {
@IsString()
email?: string
// TODO: Remove in favor of using region currencies, as in the core
@IsOptional()
@IsString()
currency_code?: string
@@ -53,3 +56,45 @@ export class StorePostCartReq {
@IsOptional()
metadata?: Record<string, unknown>
}
export class StorePostCartsCartReq {
@IsOptional()
@IsString()
region_id?: string
@IsEmail()
@IsOptional()
email?: string
@IsOptional()
@IsType([AddressPayload, String])
billing_address?: AddressPayload | string
@IsOptional()
@IsType([AddressPayload, String])
shipping_address?: AddressPayload | string
@IsString()
@IsOptional()
customer_id?: string
@IsEmail()
@IsOptional()
sales_channel_id?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
// @IsOptional()
// @IsArray()
// @ValidateNested({ each: true })
// @Type(() => GiftCard)
// gift_cards?: GiftCard[]
// @IsOptional()
// @IsArray()
// @ValidateNested({ each: true })
// @Type(() => Discount)
// discounts?: Discount[]
}

View File

@@ -61,6 +61,8 @@ import {
} from "../types/common"
import { buildQuery, isString, setMetadata } from "../utils"
import { Modules, RemoteLink } from "@medusajs/modules-sdk"
import { RemoteQueryFunction } from "@medusajs/types"
import { AddressRepository } from "../repositories/address"
import { CartRepository } from "../repositories/cart"
import { LineItemRepository } from "../repositories/line-item"
@@ -68,8 +70,6 @@ import { PaymentSessionRepository } from "../repositories/payment-session"
import { ShippingMethodRepository } from "../repositories/shipping-method"
import { PaymentSessionInput } from "../types/payment"
import { validateEmail } from "../utils/is-email"
import { RemoteQueryFunction } from "@medusajs/types"
import { Modules, RemoteLink } from "@medusajs/modules-sdk"
type InjectedDependencies = {
manager: EntityManager

View File

@@ -1,5 +1,6 @@
import type { Customer, User } from "../models"
import type { NextFunction, Request, Response } from "express"
import type { Customer, User } from "../models"
import type { MedusaContainer } from "./global"
export interface MedusaRequest extends Request {

View File

@@ -1,4 +1,4 @@
import { CartLineItemDTO } from "./common"
import { CartDTO, CartLineItemDTO } from "./common"
/** ADDRESS START */
export interface UpsertAddressDTO {
@@ -40,22 +40,25 @@ export interface CreateCartDTO {
items?: CreateLineItemDTO[]
}
export interface UpdateCartDTO {
id: string
region_id?: string
customer_id?: string
sales_channel_id?: string
export interface UpdateCartDataDTO {
region_id?: string | null
customer_id?: string | null
sales_channel_id?: string | null
email?: string
currency_code?: string
email?: string | null
currency_code?: string | null
shipping_address_id?: string
billing_address_id?: string
shipping_address_id?: string | null
billing_address_id?: string | null
billing_address?: CreateAddressDTO | UpdateAddressDTO
shipping_address?: CreateAddressDTO | UpdateAddressDTO
billing_address?: CreateAddressDTO | UpdateAddressDTO | null
shipping_address?: CreateAddressDTO | UpdateAddressDTO | null
metadata?: Record<string, unknown>
metadata?: Record<string, unknown> | null
}
export interface UpdateCartDTO extends UpdateCartDataDTO {
id?: string
}
/** CART END */
@@ -171,6 +174,11 @@ export interface UpdateLineItemWithSelectorDTO {
data: Partial<UpdateLineItemDTO>
}
export interface UpdateCartWithSelectorDTO {
selector: Partial<CartDTO>
data: UpdateCartDTO
}
export interface UpdateLineItemDTO
extends Omit<
CreateLineItemDTO,

View File

@@ -32,6 +32,7 @@ import {
CreateShippingMethodTaxLineDTO,
UpdateAddressDTO,
UpdateCartDTO,
UpdateCartDataDTO,
UpdateLineItemDTO,
UpdateLineItemTaxLineDTO,
UpdateLineItemWithSelectorDTO,
@@ -62,8 +63,17 @@ export interface ICartModuleService extends IModuleService {
create(data: CreateCartDTO[], sharedContext?: Context): Promise<CartDTO[]>
create(data: CreateCartDTO, sharedContext?: Context): Promise<CartDTO>
update(data: UpdateCartDTO[], sharedContext?: Context): Promise<CartDTO[]>
update(data: UpdateCartDTO, sharedContext?: Context): Promise<CartDTO>
update(data: UpdateCartDTO[]): Promise<CartDTO[]>
update(
cartId: string,
data: UpdateCartDataDTO,
sharedContext?: Context
): Promise<CartDTO>
update(
selector: Partial<CartDTO>,
data: UpdateCartDataDTO,
sharedContext?: Context
): Promise<CartDTO[]>
delete(cartIds: string[], sharedContext?: Context): Promise<void>
delete(cartId: string, sharedContext?: Context): Promise<void>