diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 2ee1aa1a9f93..a5c123295be1 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -1255,6 +1255,95 @@ export interface OrderTransactionDTO { updated_at: Date | string } +export interface OrderTransactionDTO { + /** + * The ID of the transaction + */ + id: string + /** + * The ID of the associated order + */ + order_id: string + /** + * The associated order + * + * @expandable + */ + order: OrderDTO + /** + * The amount of the transaction + */ + amount: BigNumberValue + /** + * The raw amount of the transaction + */ + raw_amount: BigNumberRawValue + /** + * The currency code of the transaction + */ + currency_code: string + /** + * The reference of the transaction + */ + reference: string + /** + * The ID of the reference + */ + reference_id: string + /** + * The metadata of the transaction + */ + metadata: Record | null + /** + * When the transaction was created + */ + created_at: Date | string + /** + * When the transaction was updated + */ + updated_at: Date | string +} + +export interface OrderReturnReasonDTO { + /** + * The ID of the return reason + */ + id: string + /** + * The unique value of the return reason + */ + value: string + /** + * The label of the return reason + */ + label: string + /** + * The description of the return reason + */ + description?: string + /** + * The parent return reason ID + */ + parent_return_reason_id?: string + + parent_return_reason?: OrderReturnReasonDTO + + return_reason_children?: OrderReturnReasonDTO[] + + /** + * The metadata of the return reason + */ + metadata: Record | null + /** + * When the return reason was created + */ + created_at: Date | string + /** + * When the return reason was updated + */ + updated_at: Date | string +} + export interface FilterableOrderProps extends BaseFilterable { id?: string | string[] diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index 61ef62c21c1e..15c0a0d63034 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -401,3 +401,22 @@ export interface CreateOrderReturnDTO { } /** ORDER bundled action flows */ + +export interface CreateOrderTransactionDTO { + order_id: string + description?: string + reference_type?: string + reference_id?: string + internal_note?: string + created_by?: string + amount: BigNumberInput + metadata?: Record | null +} + +export interface CreateOrderReturnReasonDTO { + value: string + label: string + description?: string + parent_return_reason_id?: string + metadata?: Record | null +} diff --git a/packages/core/types/src/order/service.ts b/packages/core/types/src/order/service.ts index c4200e308e9b..53a86377e088 100644 --- a/packages/core/types/src/order/service.ts +++ b/packages/core/types/src/order/service.ts @@ -19,9 +19,11 @@ import { OrderLineItemAdjustmentDTO, OrderLineItemDTO, OrderLineItemTaxLineDTO, + OrderReturnReasonDTO, OrderShippingMethodAdjustmentDTO, OrderShippingMethodDTO, OrderShippingMethodTaxLineDTO, + OrderTransactionDTO, } from "./common" import { CancelOrderChangeDTO, @@ -35,9 +37,11 @@ import { CreateOrderLineItemForOrderDTO, CreateOrderLineItemTaxLineDTO, CreateOrderReturnDTO, + CreateOrderReturnReasonDTO, CreateOrderShippingMethodAdjustmentDTO, CreateOrderShippingMethodDTO, CreateOrderShippingMethodTaxLineDTO, + CreateOrderTransactionDTO, DeclineOrderChangeDTO, RegisterOrderFulfillmentDTO, RegisterOrderShipmentDTO, @@ -1389,6 +1393,36 @@ export interface IOrderModuleService extends IModuleService { revertLastVersion(orderId: string, sharedContext?: Context): Promise + addTransaction( + transactionData: CreateOrderTransactionDTO, + sharedContext?: Context + ): Promise + + addTransaction( + transactionData: CreateOrderTransactionDTO[], + sharedContext?: Context + ): Promise + + deleteTransaction( + transactionIds: string[], + sharedContext?: Context + ): Promise + + createReturnReason( + returnReasonData: CreateOrderReturnReasonDTO, + sharedContext?: Context + ): Promise + + createReturnReason( + returnReasonData: CreateOrderReturnReasonDTO[], + sharedContext?: Context + ): Promise + + deleteReturnReason( + returnReasonIds: string[], + sharedContext?: Context + ): Promise + // Bundled flows registerFulfillment( data: RegisterOrderFulfillmentDTO, diff --git a/packages/medusa/src/api-v2/store/return/middlewares.ts b/packages/medusa/src/api-v2/store/return/middlewares.ts new file mode 100644 index 000000000000..299a12319abb --- /dev/null +++ b/packages/medusa/src/api-v2/store/return/middlewares.ts @@ -0,0 +1,19 @@ +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { validateAndTransformBody } from "../../utils/validate-body" +import { validateAndTransformQuery } from "../../utils/validate-query" +import * as QueryConfig from "./query-config" +import { ReturnsParams, StorePostReturnsReqSchema } from "./validators" + +export const storeRegionRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["POST"], + matcher: "/store/returns/create-return", + middlewares: [ + validateAndTransformBody(StorePostReturnsReqSchema), + validateAndTransformQuery( + ReturnsParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api-v2/store/return/query-config.ts b/packages/medusa/src/api-v2/store/return/query-config.ts new file mode 100644 index 000000000000..f700e109e125 --- /dev/null +++ b/packages/medusa/src/api-v2/store/return/query-config.ts @@ -0,0 +1,13 @@ +export const defaultReturnFields = [ + "id", + "order_id", + "created_at", + "updated_at", + "deleted_at", + "metadata", +] + +export const retrieveTransformQueryConfig = { + defaults: defaultReturnFields, + isList: false, +} diff --git a/packages/medusa/src/api-v2/store/return/route.ts b/packages/medusa/src/api-v2/store/return/route.ts new file mode 100644 index 000000000000..745f2eafd7cb --- /dev/null +++ b/packages/medusa/src/api-v2/store/return/route.ts @@ -0,0 +1,17 @@ +import { CreateOrderReturnDTO } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const input = [req.validatedBody as CreateOrderReturnDTO] + + const { result, errors } = await createReturnsWorkflow(req.scope).run({ + input: { products: input }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ return: result }) +} diff --git a/packages/medusa/src/api-v2/store/return/validators.ts b/packages/medusa/src/api-v2/store/return/validators.ts new file mode 100644 index 000000000000..343110ff161c --- /dev/null +++ b/packages/medusa/src/api-v2/store/return/validators.ts @@ -0,0 +1,32 @@ +import { z } from "zod" +import { createFindParams, createSelectParams } from "../../utils/validators" + +export type ReturnParamsType = z.infer +export const ReturnParams = createSelectParams() + +export type ReturnsParamsType = z.infer +export const ReturnsParams = createFindParams().merge( + z.object({ + id: z.union([z.string(), z.array(z.string())]).optional(), + order_id: z.union([z.string(), z.array(z.string())]).optional(), + $and: z.lazy(() => ReturnsParams.array()).optional(), + $or: z.lazy(() => ReturnsParams.array()).optional(), + }) +) + +const ReturnShippingSchema = z.object({ + option_id: z.string(), +}) + +const ItemSchema = z.object({ + item_id: z.string(), + quantity: z.number().min(1), + reason_id: z.string().optional(), + note: z.string().optional(), +}) + +export const StorePostReturnsReqSchema = z.object({ + order_id: z.string(), + items: z.array(ItemSchema), + return_shipping: ReturnShippingSchema.optional(), +}) diff --git a/packages/modules/order/integration-tests/__tests__/returns.ts b/packages/modules/order/integration-tests/__tests__/returns.ts new file mode 100644 index 000000000000..03d86e0f36b7 --- /dev/null +++ b/packages/modules/order/integration-tests/__tests__/returns.ts @@ -0,0 +1,96 @@ +import { Modules } from "@medusajs/modules-sdk" +import { IOrderModuleService } from "@medusajs/types" +import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils" + +jest.setTimeout(100000) + +moduleIntegrationTestRunner({ + moduleName: Modules.ORDER, + testSuite: ({ service }: SuiteOptions) => { + describe("Order Module Service - Returns", () => { + it("should create return reasons", async function () { + const reason = await service.createReturnReason({ + value: "test", + label: "label test", + description: "description test", + }) + + expect(reason).toEqual({ + id: expect.any(String), + value: "test", + label: "label test", + description: "description test", + return_reason_children: [], + metadata: null, + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }) + }) + + it("should create return reasons with parent", async function () { + const reason = await service.createReturnReason({ + value: "test", + label: "label test", + description: "description test", + }) + + const reason2 = await service.createReturnReason({ + value: "test 2.0", + label: "child", + parent_return_reason_id: reason.id, + }) + const reason3 = await service.createReturnReason({ + value: "test 3.0", + label: "child 3", + parent_return_reason_id: reason.id, + }) + + const getChild = await service.retrieveReturnReason(reason2.id, { + relations: ["parent_return_reason"], + }) + expect(getChild).toEqual( + expect.objectContaining({ + id: reason2.id, + value: "test 2.0", + label: "child", + parent_return_reason_id: reason.id, + parent_return_reason: expect.objectContaining({ + id: reason.id, + value: "test", + label: "label test", + description: "description test", + parent_return_reason_id: null, + }), + }) + ) + + const getParent = await service.retrieveReturnReason(reason.id, { + relations: ["return_reason_children"], + }) + expect(getParent).toEqual( + expect.objectContaining({ + id: reason.id, + value: "test", + label: "label test", + description: "description test", + return_reason_children: [ + expect.objectContaining({ + id: reason2.id, + value: "test 2.0", + label: "child", + parent_return_reason_id: reason.id, + }), + expect.objectContaining({ + id: reason3.id, + value: "test 3.0", + label: "child 3", + parent_return_reason_id: reason.id, + }), + ], + }) + ) + }) + }) + }, +}) diff --git a/packages/modules/order/src/migrations/Migration20240219102530.ts b/packages/modules/order/src/migrations/Migration20240219102530.ts index fe23b9e2d46c..f6a5a0c851fd 100644 --- a/packages/modules/order/src/migrations/Migration20240219102530.ts +++ b/packages/modules/order/src/migrations/Migration20240219102530.ts @@ -466,6 +466,28 @@ export class Migration20240219102530 extends Migration { CREATE INDEX IF NOT EXISTS "IDX_order_transaction_reference_id" ON "order_transaction" ( reference_id ); + + CREATE TABLE IF NOT EXISTS "return_reason" + ( + id character varying NOT NULL, + value character varying NOT NULL, + label character varying NOT NULL, + description character varying, + metadata JSONB NULL, + parent_return_reason_id character varying, + created_at timestamp with time zone NOT NULL DEFAULT now(), + updated_at timestamp with time zone NOT NULL DEFAULT now(), + deleted_at timestamp with time zone, + CONSTRAINT "return_reason_pkey" PRIMARY KEY (id), + CONSTRAINT "return_reason_parent_return_reason_id_foreign" FOREIGN KEY (parent_return_reason_id) + REFERENCES "return_reason" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + ); + + CREATE UNIQUE INDEX IF NOT EXISTS "IDX_return_reason_value" ON "return_reason" USING btree (value ASC NULLS LAST) + WHERE deleted_at IS NOT NULL; + ALTER TABLE if exists "order" ADD CONSTRAINT "order_shipping_address_id_foreign" FOREIGN KEY ("shipping_address_id") REFERENCES "order_address" ("id") ON diff --git a/packages/modules/order/src/models/index.ts b/packages/modules/order/src/models/index.ts index 72fb64f390ac..e0d0688638f5 100644 --- a/packages/modules/order/src/models/index.ts +++ b/packages/modules/order/src/models/index.ts @@ -8,6 +8,7 @@ export { default as OrderChangeAction } from "./order-change-action" export { default as OrderItem } from "./order-item" export { default as OrderShippingMethod } from "./order-shipping-method" export { default as OrderSummary } from "./order-summary" +export { default as ReturnReason } from "./return-reason" export { default as ShippingMethod } from "./shipping-method" export { default as ShippingMethodAdjustment } from "./shipping-method-adjustment" export { default as ShippingMethodTaxLine } from "./shipping-method-tax-line" diff --git a/packages/modules/order/src/models/return-reason.ts b/packages/modules/order/src/models/return-reason.ts new file mode 100644 index 000000000000..68e668f4221d --- /dev/null +++ b/packages/modules/order/src/models/return-reason.ts @@ -0,0 +1,105 @@ +import { DAL } from "@medusajs/types" +import { + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" +import { + BeforeCreate, + Cascade, + Entity, + ManyToOne, + OnInit, + OneToMany, + OptionalProps, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +const DeletedAtIndex = createPsqlIndexStatementHelper({ + tableName: "return_reason", + columns: "deleted_at", + where: "deleted_at IS NOT NULL", +}) + +const ValueIndex = createPsqlIndexStatementHelper({ + tableName: "return_reason", + columns: "value", + where: "deleted_at IS NOT NULL", +}) + +const ParentIndex = createPsqlIndexStatementHelper({ + tableName: "return_reason", + columns: "parent_return_reason_id", + where: "deleted_at IS NOT NULL", +}) + +type OptionalOrderProps = "parent_return_reason" | DAL.EntityDateColumns + +@Entity({ tableName: "return_reason" }) +export default class ReturnReason { + [OptionalProps]?: OptionalOrderProps + + @PrimaryKey({ columnType: "text" }) + id: string + + @Property({ columnType: "text" }) + @ValueIndex.MikroORMIndex() + value: string + + @Property({ columnType: "text" }) + label: string + + @Property({ columnType: "text", nullable: true }) + description: string | null = null + + @Property({ columnType: "text", nullable: true }) + @ParentIndex.MikroORMIndex() + parent_return_reason_id?: string | null + + @ManyToOne({ + entity: () => ReturnReason, + fieldName: "parent_return_reason_id", + nullable: true, + cascade: [Cascade.PERSIST], + }) + parent_return_reason?: ReturnReason | null + + @OneToMany( + () => ReturnReason, + (return_reason) => return_reason.parent_return_reason, + { cascade: [Cascade.PERSIST] } + ) + return_reason_children: ReturnReason[] + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true }) + @DeletedAtIndex.MikroORMIndex() + deleted_at: Date | null = null + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "rr") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "rr") + } +} diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 090419d97791..5492c178b299 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -37,6 +37,7 @@ import { OrderItem, OrderShippingMethod, OrderSummary, + ReturnReason, ShippingMethod, ShippingMethodAdjustment, ShippingMethodTaxLine, @@ -75,6 +76,7 @@ type InjectedDependencies = { orderItemService: ModulesSdkTypes.InternalModuleService orderSummaryService: ModulesSdkTypes.InternalModuleService orderShippingMethodService: ModulesSdkTypes.InternalModuleService + returnReasonService: ModulesSdkTypes.InternalModuleService } const generateMethodForModels = [ @@ -91,6 +93,7 @@ const generateMethodForModels = [ OrderItem, OrderSummary, OrderShippingMethod, + ReturnReason, ] export default class OrderModuleService< @@ -107,7 +110,8 @@ export default class OrderModuleService< TOrderChangeAction extends OrderChangeAction = OrderChangeAction, TOrderItem extends OrderItem = OrderItem, TOrderSummary extends OrderSummary = OrderSummary, - TOrderShippingMethod extends OrderShippingMethod = OrderShippingMethod + TOrderShippingMethod extends OrderShippingMethod = OrderShippingMethod, + TReturnReason extends ReturnReason = ReturnReason > extends ModulesSdkUtils.abstractModuleServiceFactory< InjectedDependencies, @@ -128,6 +132,7 @@ export default class OrderModuleService< OrderItem: { dto: OrderTypes.OrderItemDTO } OrderSummary: { dto: OrderTypes.OrderSummaryDTO } OrderShippingMethod: { dto: OrderShippingMethod } + ReturnReason: { dto: OrderTypes.OrderReturnReasonDTO } } >(Order, generateMethodForModels, entityNameToLinkableKeysMap) implements IOrderModuleService @@ -147,6 +152,7 @@ export default class OrderModuleService< protected orderItemService_: ModulesSdkTypes.InternalModuleService protected orderSummaryService_: ModulesSdkTypes.InternalModuleService protected orderShippingMethodService_: ModulesSdkTypes.InternalModuleService + protected returnReasonService_: ModulesSdkTypes.InternalModuleService constructor( { @@ -165,6 +171,7 @@ export default class OrderModuleService< orderItemService, orderSummaryService, orderShippingMethodService, + returnReasonService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { @@ -186,6 +193,7 @@ export default class OrderModuleService< this.orderItemService_ = orderItemService this.orderSummaryService_ = orderSummaryService this.orderShippingMethodService_ = orderShippingMethodService + this.returnReasonService_ = returnReasonService } __joinerConfig(): ModuleJoinerConfig { @@ -2261,4 +2269,88 @@ export default class OrderModuleService< await this.confirmOrderChange(change[0].id, sharedContext) } + + public async addTransaction( + transactionData: OrderTypes.CreateOrderTransactionDTO, + sharedContext?: Context + ): Promise + + public async addTransaction( + transactionData: OrderTypes.CreateOrderTransactionDTO[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + public async addTransaction( + transactionData: + | OrderTypes.CreateOrderTransactionDTO + | OrderTypes.CreateOrderTransactionDTO[], + sharedContext?: Context + ): Promise { + const data = Array.isArray(transactionData) + ? transactionData + : [transactionData] + + const created = await this.transactionService_.create(data, sharedContext) + + return await this.baseRepository_.serialize( + !Array.isArray(transactionData) ? created[0] : created, + { + populate: true, + } + ) + } + + @InjectTransactionManager("baseRepository_") + async deleteTransaction( + transactionIds: string[], + sharedContext?: Context | undefined + ): Promise { + return await this.transactionService_.delete( + { id: transactionIds }, + sharedContext + ) + } + + public async createReturnReason( + transactionData: OrderTypes.CreateOrderReturnReasonDTO, + sharedContext?: Context + ): Promise + + public async createReturnReason( + transactionData: OrderTypes.CreateOrderReturnReasonDTO[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + public async createReturnReason( + returnReasonData: + | OrderTypes.CreateOrderReturnReasonDTO + | OrderTypes.CreateOrderReturnReasonDTO[], + sharedContext?: Context + ): Promise { + const data = Array.isArray(returnReasonData) + ? returnReasonData + : [returnReasonData] + + const created = await this.returnReasonService_.create(data, sharedContext) + + return await this.baseRepository_.serialize( + !Array.isArray(returnReasonData) ? created[0] : created, + { + populate: true, + } + ) + } + + @InjectTransactionManager("baseRepository_") + async deleteReturnReason( + returnReasonIds: string[], + sharedContext?: Context | undefined + ): Promise { + return await this.returnReasonService_.delete( + { id: returnReasonIds }, + sharedContext + ) + } }