diff --git a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts index 40c87afeb2..3b55fb9047 100644 --- a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts +++ b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts @@ -418,6 +418,91 @@ describe("Customer Module Service", () => { const remainingCustomers = await service.list({ last_name: "Doe" }) expect(remainingCustomers.length).toBe(0) }) + + it("should cascade relationship when deleting customer", async () => { + // Creating a customer and a group + const customer = await service.create({ + first_name: "John", + last_name: "Doe", + }) + const group = await service.createCustomerGroup({ name: "VIP" }) + + // Adding the customer to the groups + await service.addCustomerToGroup({ + customer_id: customer.id, + customer_group_id: group.id, + }) + + await service.delete(customer.id) + + const res = await service.listCustomerGroupRelations({ + customer_id: customer.id, + customer_group_id: group.id, + }) + expect(res.length).toBe(0) + }) + }) + + describe("deleteCustomerGroup", () => { + it("should delete a single customer group", async () => { + const [group] = await service.createCustomerGroup([{ name: "VIP" }]) + await service.deleteCustomerGroup(group.id) + + await expect( + service.retrieveCustomerGroup(group.id) + ).rejects.toThrowError(`CustomerGroup with id: ${group.id} was not found`) + }) + + it("should delete multiple customer groups by IDs", async () => { + const groups = await service.createCustomerGroup([ + { name: "VIP" }, + { name: "Regular" }, + ]) + + const groupIds = groups.map((group) => group.id) + await service.deleteCustomerGroup(groupIds) + + for (const group of groups) { + await expect( + service.retrieveCustomerGroup(group.id) + ).rejects.toThrowError( + `CustomerGroup with id: ${group.id} was not found` + ) + } + }) + + it("should delete customer groups using a selector", async () => { + await service.createCustomerGroup([{ name: "VIP" }, { name: "Regular" }]) + + const selector = { name: "VIP" } + await service.deleteCustomerGroup(selector) + + const remainingGroups = await service.listCustomerGroups({ name: "VIP" }) + expect(remainingGroups.length).toBe(0) + }) + + it("should cascade relationship when deleting customer group", async () => { + // Creating a customer and a group + const customer = await service.create({ + first_name: "John", + last_name: "Doe", + }) + const group = await service.createCustomerGroup({ name: "VIP" }) + + // Adding the customer to the groups + await service.addCustomerToGroup({ + customer_id: customer.id, + customer_group_id: group.id, + }) + + await service.deleteCustomerGroup(group.id) + + const res = await service.listCustomerGroupRelations({ + customer_id: customer.id, + customer_group_id: group.id, + }) + expect(res.length).toBe(0) + }) }) describe("removeCustomerFromGroup", () => { @@ -496,4 +581,172 @@ describe("Customer Module Service", () => { } }) }) + + describe("softDelete", () => { + it("should soft delete a single customer", async () => { + const [customer] = await service.create([ + { first_name: "John", last_name: "Doe" }, + ]) + await service.softDelete([customer.id]) + + const res = await service.list({ id: customer.id }) + expect(res.length).toBe(0) + + const deletedCustomer = await service.retrieve(customer.id, { + withDeleted: true, + }) + + expect(deletedCustomer.deleted_at).not.toBeNull() + }) + + it("should soft delete multiple customers", async () => { + const customers = await service.create([ + { first_name: "John", last_name: "Doe" }, + { first_name: "Jane", last_name: "Smith" }, + ]) + const customerIds = customers.map((customer) => customer.id) + await service.softDelete(customerIds) + + const res = await service.list({ id: customerIds }) + expect(res.length).toBe(0) + + const deletedCustomers = await service.list( + { id: customerIds }, + { withDeleted: true } + ) + expect(deletedCustomers.length).toBe(2) + }) + + it("should remove customer in group relation", async () => { + // Creating a customer and a group + const customer = await service.create({ + first_name: "John", + last_name: "Doe", + }) + const group = await service.createCustomerGroup({ name: "VIP" }) + + // Adding the customer to the group + await service.addCustomerToGroup({ + customer_id: customer.id, + customer_group_id: group.id, + }) + + await service.softDelete([customer.id]) + + const resGroup = await service.retrieveCustomerGroup(group.id, { + relations: ["customers"], + }) + expect(resGroup.customers?.length).toBe(0) + }) + }) + + describe("restore", () => { + it("should restore a single customer", async () => { + const [customer] = await service.create([ + { first_name: "John", last_name: "Doe" }, + ]) + await service.softDelete([customer.id]) + + const res = await service.list({ id: customer.id }) + expect(res.length).toBe(0) + + await service.restore([customer.id]) + + const restoredCustomer = await service.retrieve(customer.id, { + withDeleted: true, + }) + expect(restoredCustomer.deleted_at).toBeNull() + }) + + it("should restore multiple customers", async () => { + const customers = await service.create([ + { first_name: "John", last_name: "Doe" }, + { first_name: "Jane", last_name: "Smith" }, + ]) + const customerIds = customers.map((customer) => customer.id) + await service.softDelete(customerIds) + + const res = await service.list({ id: customerIds }) + expect(res.length).toBe(0) + + await service.restore(customerIds) + + const restoredCustomers = await service.list( + { id: customerIds }, + { withDeleted: true } + ) + expect(restoredCustomers.length).toBe(2) + }) + }) + + describe("softDeleteCustomerGroup", () => { + it("should soft delete a single customer group", async () => { + const [group] = await service.createCustomerGroup([{ name: "VIP" }]) + await service.softDeleteCustomerGroup([group.id]) + + const res = await service.listCustomerGroups({ id: group.id }) + expect(res.length).toBe(0) + + const deletedGroup = await service.retrieveCustomerGroup(group.id, { + withDeleted: true, + }) + + expect(deletedGroup.deleted_at).not.toBeNull() + }) + + it("should soft delete multiple customer groups", async () => { + const groups = await service.createCustomerGroup([ + { name: "VIP" }, + { name: "Regular" }, + ]) + const groupIds = groups.map((group) => group.id) + await service.softDeleteCustomerGroup(groupIds) + + const res = await service.listCustomerGroups({ id: groupIds }) + expect(res.length).toBe(0) + + const deletedGroups = await service.listCustomerGroups( + { id: groupIds }, + { withDeleted: true } + ) + expect(deletedGroups.length).toBe(2) + }) + }) + + describe("restoreCustomerGroup", () => { + it("should restore a single customer group", async () => { + const [group] = await service.createCustomerGroup([{ name: "VIP" }]) + await service.softDeleteCustomerGroup([group.id]) + + const res = await service.listCustomerGroups({ id: group.id }) + expect(res.length).toBe(0) + + await service.restoreCustomerGroup([group.id]) + + const restoredGroup = await service.retrieveCustomerGroup(group.id, { + withDeleted: true, + }) + expect(restoredGroup.deleted_at).toBeNull() + }) + + it("should restore multiple customer groups", async () => { + const groups = await service.createCustomerGroup([ + { name: "VIP" }, + { name: "Regular" }, + ]) + const groupIds = groups.map((group) => group.id) + await service.softDeleteCustomerGroup(groupIds) + + const res = await service.listCustomerGroups({ id: groupIds }) + expect(res.length).toBe(0) + + await service.restoreCustomerGroup(groupIds) + + const restoredGroups = await service.listCustomerGroups( + { id: groupIds }, + { withDeleted: true } + ) + expect(restoredGroups.length).toBe(2) + }) + }) }) diff --git a/packages/customer/src/joiner-config.ts b/packages/customer/src/joiner-config.ts index 4fed5e99df..a4c13525dc 100644 --- a/packages/customer/src/joiner-config.ts +++ b/packages/customer/src/joiner-config.ts @@ -1,10 +1,11 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" -import { Customer } from "@models" +import { Customer, CustomerGroup } from "@models" export const LinkableKeys = { customer_id: Customer.name, + customer_group_id: CustomerGroup.name, } const entityLinkableKeysMap: MapToConfig = {} diff --git a/packages/customer/src/models/customer-group-customer.ts b/packages/customer/src/models/customer-group-customer.ts index 35e3fbfa02..94602e6693 100644 --- a/packages/customer/src/models/customer-group-customer.ts +++ b/packages/customer/src/models/customer-group-customer.ts @@ -1,6 +1,7 @@ import { DAL } from "@medusajs/types" import { generateEntityId } from "@medusajs/utils" import { + Cascade, BeforeCreate, ManyToOne, Entity, @@ -32,6 +33,7 @@ export default class CustomerGroupCustomer { fieldName: "customer_id", index: "IDX_customer_group_customer_customer_id", nullable: true, + cascade: [Cascade.REMOVE], }) customer: Customer | null @@ -40,6 +42,7 @@ export default class CustomerGroupCustomer { fieldName: "customer_group_id", index: "IDX_customer_group_customer_group_id", nullable: true, + cascade: [Cascade.REMOVE], }) customer_group: CustomerGroup | null diff --git a/packages/customer/src/models/customer-group.ts b/packages/customer/src/models/customer-group.ts index 7d219b6df3..c6db5c3486 100644 --- a/packages/customer/src/models/customer-group.ts +++ b/packages/customer/src/models/customer-group.ts @@ -1,5 +1,5 @@ import { DAL } from "@medusajs/types" -import { generateEntityId } from "@medusajs/utils" +import { DALUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Entity, @@ -9,13 +9,15 @@ import { Property, ManyToMany, Collection, + Filter, } from "@mikro-orm/core" import Customer from "./customer" import CustomerGroupCustomer from "./customer-group-customer" -type OptionalGroupProps = DAL.EntityDateColumns // TODO: To be revisited when more clear +type OptionalGroupProps = DAL.SoftDeletableEntityDateColumns // TODO: To be revisited when more clear @Entity({ tableName: "customer_group" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class CustomerGroup { [OptionalProps]: OptionalGroupProps @@ -52,6 +54,9 @@ export default class CustomerGroup { }) updated_at: Date + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "cusgroup") diff --git a/packages/customer/src/models/customer.ts b/packages/customer/src/models/customer.ts index 31bad011cb..3a2a4c6a4a 100644 --- a/packages/customer/src/models/customer.ts +++ b/packages/customer/src/models/customer.ts @@ -1,10 +1,11 @@ import { DAL } from "@medusajs/types" -import { generateEntityId } from "@medusajs/utils" +import { DALUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Cascade, Collection, Entity, + Filter, Index, ManyToMany, ManyToOne, @@ -23,9 +24,10 @@ type OptionalCustomerProps = | "addresses" | "default_shipping_address" | "default_billing_address" - | DAL.EntityDateColumns + | DAL.SoftDeletableEntityDateColumns @Entity({ tableName: "customer" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class Customer { [OptionalProps]?: OptionalCustomerProps diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts index ccaad8af8e..d9d399de91 100644 --- a/packages/customer/src/services/customer-module.ts +++ b/packages/customer/src/services/customer-module.ts @@ -6,16 +6,23 @@ import { InternalModuleDeclaration, ModuleJoinerConfig, CustomerTypes, + SoftDeleteReturn, + RestoreReturn, } from "@medusajs/types" import { InjectManager, InjectTransactionManager, MedusaContext, + mapObjectTo, isString, isObject, } from "@medusajs/utils" -import { joinerConfig } from "../joiner-config" +import { + entityNameToLinkableKeysMap, + LinkableKeys, + joinerConfig, +} from "../joiner-config" import * as services from "../services" type InjectedDependencies = { @@ -264,6 +271,23 @@ export default class CustomerModuleService implements ICustomerModuleService { return Array.isArray(dataOrArrayOfData) ? serialized : serialized[0] } + @InjectManager("baseRepository_") + async retrieveCustomerGroup( + groupId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ) { + const group = await this.customerGroupService_.retrieve( + groupId, + config, + sharedContext + ) + return await this.baseRepository_.serialize( + group, + { populate: true } + ) + } + async updateCustomerGroup( groupId: string, data: Partial, @@ -331,6 +355,39 @@ export default class CustomerModuleService implements ICustomerModuleService { >(groups, { populate: true }) } + deleteCustomerGroup(groupId: string, sharedContext?: Context): Promise + deleteCustomerGroup( + groupIds: string[], + sharedContext?: Context + ): Promise + deleteCustomerGroup( + selector: CustomerTypes.FilterableCustomerGroupProps, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async deleteCustomerGroup( + groupIdOrSelector: + | string + | string[] + | CustomerTypes.FilterableCustomerGroupProps, + @MedusaContext() sharedContext: Context = {} + ) { + let toDelete = Array.isArray(groupIdOrSelector) + ? groupIdOrSelector + : [groupIdOrSelector as string] + if (isObject(groupIdOrSelector)) { + const ids = await this.customerGroupService_.list( + groupIdOrSelector, + { select: ["id"] }, + sharedContext + ) + toDelete = ids.map(({ id }) => id) + } + + return await this.customerGroupService_.delete(toDelete, sharedContext) + } + async addCustomerToGroup( groupCustomerPair: CustomerTypes.GroupCustomerPair, sharedContext?: Context @@ -382,6 +439,25 @@ export default class CustomerModuleService implements ICustomerModuleService { ) } + @InjectManager("baseRepository_") + async listCustomerGroupRelations( + filters?: CustomerTypes.FilterableCustomerGroupCustomerProps, + config?: FindConfig, + @MedusaContext() sharedContext: Context = {} + ) { + const groupCustomers = await this.customerGroupCustomerService_.list( + filters, + config, + sharedContext + ) + + return await this.baseRepository_.serialize< + CustomerTypes.CustomerGroupCustomerDTO[] + >(groupCustomers, { + populate: true, + }) + } + @InjectManager("baseRepository_") async listCustomerGroups( filters: CustomerTypes.FilterableCustomerGroupProps = {}, @@ -423,4 +499,90 @@ export default class CustomerModuleService implements ICustomerModuleService { count, ] } + + @InjectTransactionManager("baseRepository_") + async softDeleteCustomerGroup< + TReturnableLinkableKeys extends string = string + >( + groupIds: string[], + config: SoftDeleteReturn = {}, + @MedusaContext() sharedContext: Context = {} + ) { + const [_, cascadedEntitiesMap] = + await this.customerGroupService_.softDelete(groupIds, sharedContext) + return config.returnLinkableKeys + ? mapObjectTo>( + cascadedEntitiesMap, + entityNameToLinkableKeysMap, + { + pick: config.returnLinkableKeys, + } + ) + : void 0 + } + + @InjectTransactionManager("baseRepository_") + async restoreCustomerGroup( + groupIds: string[], + config: RestoreReturn = {}, + @MedusaContext() sharedContext: Context = {} + ) { + const [_, cascadedEntitiesMap] = await this.customerGroupService_.restore( + groupIds, + sharedContext + ) + return config.returnLinkableKeys + ? mapObjectTo>( + cascadedEntitiesMap, + entityNameToLinkableKeysMap, + { + pick: config.returnLinkableKeys, + } + ) + : void 0 + } + + @InjectTransactionManager("baseRepository_") + async softDelete( + customerIds: string[], + config: SoftDeleteReturn = {}, + @MedusaContext() sharedContext: Context = {} + ) { + const [_, cascadedEntitiesMap] = await this.customerService_.softDelete( + customerIds, + sharedContext + ) + + return config.returnLinkableKeys + ? mapObjectTo>( + cascadedEntitiesMap, + entityNameToLinkableKeysMap, + { + pick: config.returnLinkableKeys, + } + ) + : void 0 + } + + @InjectTransactionManager("baseRepository_") + async restore( + customerIds: string[], + config: RestoreReturn = {}, + @MedusaContext() sharedContext: Context = {} + ) { + const [_, cascadedEntitiesMap] = await this.customerService_.restore( + customerIds, + sharedContext + ) + + return config.returnLinkableKeys + ? mapObjectTo>( + cascadedEntitiesMap, + entityNameToLinkableKeysMap, + { + pick: config.returnLinkableKeys, + } + ) + : void 0 + } } diff --git a/packages/types/src/customer/common.ts b/packages/types/src/customer/common.ts index 5aea291312..a4ebbb69c4 100644 --- a/packages/types/src/customer/common.ts +++ b/packages/types/src/customer/common.ts @@ -2,27 +2,28 @@ import { BaseFilterable } from "../dal" import { OperatorMap } from "../dal/utils" import { AddressDTO } from "../address" -export interface CustomerGroupDTO { - id: string - name: string - customers?: Partial[] - metadata?: Record - created_by?: string | null - deleted_at?: Date | string | null - created_at?: Date | string - updated_at?: Date | string -} - export interface FilterableCustomerGroupProps extends BaseFilterable { id?: string | string[] - name?: OperatorMap + name?: string | OperatorMap customers?: FilterableCustomerProps | string | string[] created_by?: string | string[] | null created_at?: OperatorMap updated_at?: OperatorMap } +export interface FilterableCustomerGroupCustomerProps + extends BaseFilterable { + id?: string | string[] + customer_id?: string | string[] + customer_group_id?: string | string[] + customer?: FilterableCustomerProps | string | string[] + group?: FilterableCustomerGroupProps | string | string[] + created_by?: string | string[] | null + created_at?: OperatorMap + updated_at?: OperatorMap +} + export interface FilterableCustomerProps extends BaseFilterable { id?: string | string[] @@ -38,6 +39,28 @@ export interface FilterableCustomerProps updated_at?: OperatorMap } +export interface CustomerGroupDTO { + id: string + name: string + customers?: Partial[] + metadata?: Record + created_by?: string | null + deleted_at?: Date | string | null + created_at?: Date | string + updated_at?: Date | string +} + +export interface CustomerGroupCustomerDTO { + id: string + customer_id: string + customer_group_id: string + customer?: Partial + group?: Partial + created_by?: string | null + created_at?: Date | string + updated_at?: Date | string +} + export interface CustomerDTO { id: string email: string @@ -52,6 +75,7 @@ export interface CustomerDTO { phone?: string | null groups?: { id: string }[] metadata?: Record + created_by?: string | null deleted_at?: Date | string | null created_at?: Date | string updated_at?: Date | string diff --git a/packages/types/src/customer/service.ts b/packages/types/src/customer/service.ts index f39ef3e8ba..cbbb873dcd 100644 --- a/packages/types/src/customer/service.ts +++ b/packages/types/src/customer/service.ts @@ -1,9 +1,12 @@ import { FindConfig } from "../common" +import { RestoreReturn, SoftDeleteReturn } from "../dal" import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { CustomerDTO, CustomerGroupDTO, + CustomerGroupCustomerDTO, + FilterableCustomerGroupCustomerProps, FilterableCustomerProps, FilterableCustomerGroupProps, GroupCustomerPair, @@ -21,7 +24,6 @@ export interface ICustomerModuleService extends IModuleService { data: CreateCustomerDTO[], sharedContext?: Context ): Promise - create(data: CreateCustomerDTO, sharedContext?: Context): Promise update( @@ -57,6 +59,12 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise + retrieveCustomerGroup( + groupId: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + updateCustomerGroup( groupId: string, data: Partial, @@ -73,6 +81,16 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise + deleteCustomerGroup(groupId: string, sharedContext?: Context): Promise + deleteCustomerGroup( + groupIds: string[], + sharedContext?: Context + ): Promise + deleteCustomerGroup( + selector: FilterableCustomerGroupProps, + sharedContext?: Context + ): Promise + addCustomerToGroup( groupCustomerPair: GroupCustomerPair, sharedContext?: Context @@ -84,14 +102,20 @@ export interface ICustomerModuleService extends IModuleService { ): Promise<{ id: string }[]> removeCustomerFromGroup( - groupCustomerPair: { customer_id: string; customer_group_id: string }, + groupCustomerPair: GroupCustomerPair, sharedContext?: Context ): Promise removeCustomerFromGroup( - groupCustomerPairs: { customer_id: string; customer_group_id: string }[], + groupCustomerPairs: GroupCustomerPair[], sharedContext?: Context ): Promise + listCustomerGroupRelations( + filters?: FilterableCustomerGroupCustomerProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + list( filters?: FilterableCustomerProps, config?: FindConfig, @@ -115,4 +139,28 @@ export interface ICustomerModuleService extends IModuleService { config?: FindConfig, sharedContext?: Context ): Promise<[CustomerGroupDTO[], number]> + + softDelete( + customerIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restore( + customerIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + + softDeleteCustomerGroup( + groupIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restoreCustomerGroup( + groupIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> }