From 1ed5f918c31794a70aca4a4e4cd83cf456593baa Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:03:02 +0100 Subject: [PATCH] feat(cart): `POST /store/carts/:id` (#6274) Depends on: - #6262 - #6273 --- .changeset/rude-shirts-act.md | 5 ++ .../__tests__/cart/store/update-cart.ts | 63 ++++++++++++++++ .../services/cart-module/index.spec.ts | 50 +++++++++++++ packages/cart/src/services/cart-module.ts | 71 +++++++++++++------ packages/cart/src/types/cart.ts | 12 ++-- .../src/definition/cart/steps/index.ts | 3 +- .../src/definition/cart/steps/update-carts.ts | 61 ++++++++++++++++ .../src/definition/cart/workflows/index.ts | 3 +- .../definition/cart/workflows/update-carts.ts | 20 ++++++ packages/medusa/src/api-v2/middlewares.ts | 1 + .../src/api-v2/store/carts/[id]/route.ts | 23 ++++++ .../src/api-v2/store/carts/middlewares.ts | 11 ++- .../src/api-v2/store/carts/validators.ts | 47 +++++++++++- packages/medusa/src/services/cart.ts | 4 +- packages/medusa/src/types/routing.ts | 3 +- packages/types/src/cart/mutations.ts | 34 +++++---- packages/types/src/cart/service.ts | 14 +++- 17 files changed, 377 insertions(+), 48 deletions(-) create mode 100644 .changeset/rude-shirts-act.md create mode 100644 integration-tests/plugins/__tests__/cart/store/update-cart.ts create mode 100644 packages/core-flows/src/definition/cart/steps/update-carts.ts create mode 100644 packages/core-flows/src/definition/cart/workflows/update-carts.ts diff --git a/.changeset/rude-shirts-act.md b/.changeset/rude-shirts-act.md new file mode 100644 index 0000000000000..c9ef061c28b1e --- /dev/null +++ b/.changeset/rude-shirts-act.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +feat(cart): `POST /store/carts/:id` diff --git a/integration-tests/plugins/__tests__/cart/store/update-cart.ts b/integration-tests/plugins/__tests__/cart/store/update-cart.ts new file mode 100644 index 0000000000000..dbbdc2b42a0bc --- /dev/null +++ b/integration-tests/plugins/__tests__/cart/store/update-cart.ts @@ -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", + }) + ) + }) +}) diff --git a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts index 3148d14b2d597..b2c2052293e37 100644 --- a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts +++ b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts @@ -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", () => { diff --git a/packages/cart/src/services/cart-module.ts b/packages/cart/src/services/cart-module.ts index deffd092d6120..099613e1a3282 100644 --- a/packages/cart/src/services/cart-module.ts +++ b/packages/cart/src/services/cart-module.ts @@ -190,43 +190,74 @@ export default class CartModuleService< return createdCarts } + async update(data: CartTypes.UpdateCartDTO[]): Promise async update( - data: CartTypes.UpdateCartDTO[], + cartId: string, + data: CartTypes.UpdateCartDataDTO, sharedContext?: Context - ): Promise - + ): Promise async update( - data: CartTypes.UpdateCartDTO, + selector: Partial, + data: CartTypes.UpdateCartDataDTO, sharedContext?: Context - ): Promise + ): Promise @InjectManager("baseRepository_") async update( - data: CartTypes.UpdateCartDTO[] | CartTypes.UpdateCartDTO, + dataOrIdOrSelector: + | CartTypes.UpdateCartDTO[] + | string + | Partial, + data?: CartTypes.UpdateCartDataDTO, @MedusaContext() sharedContext: Context = {} ): Promise { - 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, + 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( diff --git a/packages/cart/src/types/cart.ts b/packages/cart/src/types/cart.ts index 0b0df6ac1cba0..46ffcf8358d28 100644 --- a/packages/cart/src/types/cart.ts +++ b/packages/cart/src/types/cart.ts @@ -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 + region_id?: string | null + customer_id?: string | null + sales_channel_id?: string | null + email?: string | null + currency_code?: string | null + metadata?: Record | null adjustments?: (CreateLineItemAdjustmentDTO | UpdateLineItemAdjustmentDTO)[] } diff --git a/packages/core-flows/src/definition/cart/steps/index.ts b/packages/core-flows/src/definition/cart/steps/index.ts index 3d037035be361..92c4628406e3a 100644 --- a/packages/core-flows/src/definition/cart/steps/index.ts +++ b/packages/core-flows/src/definition/cart/steps/index.ts @@ -1,2 +1,3 @@ -export * from "./create-carts"; +export * from "./create-carts" +export * from "./update-carts" diff --git a/packages/core-flows/src/definition/cart/steps/update-carts.ts b/packages/core-flows/src/definition/cart/steps/update-carts.ts new file mode 100644 index 0000000000000..978e6d94a66c9 --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/update-carts.ts @@ -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( + 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, + data.update + ) + + return new StepResponse(updatedCarts, prevCarts) + }, + async (previousCarts, { container }) => { + if (!previousCarts?.length) { + return + } + + const service = container.resolve( + 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) + } +) diff --git a/packages/core-flows/src/definition/cart/workflows/index.ts b/packages/core-flows/src/definition/cart/workflows/index.ts index 3d037035be361..92c4628406e3a 100644 --- a/packages/core-flows/src/definition/cart/workflows/index.ts +++ b/packages/core-flows/src/definition/cart/workflows/index.ts @@ -1,2 +1,3 @@ -export * from "./create-carts"; +export * from "./create-carts" +export * from "./update-carts" diff --git a/packages/core-flows/src/definition/cart/workflows/update-carts.ts b/packages/core-flows/src/definition/cart/workflows/update-carts.ts new file mode 100644 index 0000000000000..b7f5b07a36520 --- /dev/null +++ b/packages/core-flows/src/definition/cart/workflows/update-carts.ts @@ -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): WorkflowData => { + return updateCartsStep(input) + } +) diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 067b63873dca3..0add798844a52 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -17,6 +17,7 @@ export const config: MiddlewaresConfig = { ...adminCustomerRoutesMiddlewares, ...adminPromotionRoutesMiddlewares, ...adminCampaignRoutesMiddlewares, + ...storeCartRoutesMiddlewares, ...storeCustomerRoutesMiddlewares, ...storeCartRoutesMiddlewares, ...authRoutesMiddlewares, diff --git a/packages/medusa/src/api-v2/store/carts/[id]/route.ts b/packages/medusa/src/api-v2/store/carts/[id]/route.ts index c4c6071c08de2..42d78120cac9e 100644 --- a/packages/medusa/src/api-v2/store/carts/[id]/route.ts +++ b/packages/medusa/src/api-v2/store/carts/[id]/route.ts @@ -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] }) +} diff --git a/packages/medusa/src/api-v2/store/carts/middlewares.ts b/packages/medusa/src/api-v2/store/carts/middlewares.ts index 769d818202daf..e811b40b9e45c 100644 --- a/packages/medusa/src/api-v2/store/carts/middlewares.ts +++ b/packages/medusa/src/api-v2/store/carts/middlewares.ts @@ -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)], + }, ] diff --git a/packages/medusa/src/api-v2/store/carts/validators.ts b/packages/medusa/src/api-v2/store/carts/validators.ts index 88a841ea76fff..f0dda2d5fd97f 100644 --- a/packages/medusa/src/api-v2/store/carts/validators.ts +++ b/packages/medusa/src/api-v2/store/carts/validators.ts @@ -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 } + +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 + + // @IsOptional() + // @IsArray() + // @ValidateNested({ each: true }) + // @Type(() => GiftCard) + // gift_cards?: GiftCard[] + + // @IsOptional() + // @IsArray() + // @ValidateNested({ each: true }) + // @Type(() => Discount) + // discounts?: Discount[] +} diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index eb98af721a5fa..1879c87421bd5 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -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 diff --git a/packages/medusa/src/types/routing.ts b/packages/medusa/src/types/routing.ts index c1a7027837505..47f1dcfb719c7 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -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 { diff --git a/packages/types/src/cart/mutations.ts b/packages/types/src/cart/mutations.ts index dc4ea41016531..92a7fe0c71f5e 100644 --- a/packages/types/src/cart/mutations.ts +++ b/packages/types/src/cart/mutations.ts @@ -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 + metadata?: Record | null +} + +export interface UpdateCartDTO extends UpdateCartDataDTO { + id?: string } /** CART END */ @@ -171,6 +174,11 @@ export interface UpdateLineItemWithSelectorDTO { data: Partial } +export interface UpdateCartWithSelectorDTO { + selector: Partial + data: UpdateCartDTO +} + export interface UpdateLineItemDTO extends Omit< CreateLineItemDTO, diff --git a/packages/types/src/cart/service.ts b/packages/types/src/cart/service.ts index 2af985f422c9c..8e00c2a06ad3f 100644 --- a/packages/types/src/cart/service.ts +++ b/packages/types/src/cart/service.ts @@ -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 create(data: CreateCartDTO, sharedContext?: Context): Promise - update(data: UpdateCartDTO[], sharedContext?: Context): Promise - update(data: UpdateCartDTO, sharedContext?: Context): Promise + update(data: UpdateCartDTO[]): Promise + update( + cartId: string, + data: UpdateCartDataDTO, + sharedContext?: Context + ): Promise + update( + selector: Partial, + data: UpdateCartDataDTO, + sharedContext?: Context + ): Promise delete(cartIds: string[], sharedContext?: Context): Promise delete(cartId: string, sharedContext?: Context): Promise