-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 그룹 포트폴리오 활성화 구현 * refactor: 임포트 순서 수정
- Loading branch information
Showing
11 changed files
with
302 additions
and
1 deletion.
There are no files selected for viewing
6 changes: 6 additions & 0 deletions
6
src/app/application/group/command/enablePortfolio/EnablePortfolioCommand.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export class EnablePortfolioCommand { | ||
constructor( | ||
readonly groupId: string, | ||
readonly requesterUserId: string, | ||
) {} | ||
} |
98 changes: 98 additions & 0 deletions
98
src/app/application/group/command/enablePortfolio/EnablePortfolioCommandHandler.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Test } from '@nestjs/testing'; | ||
import { advanceTo, clear } from 'jest-date-mock'; | ||
|
||
import { EnablePortfolioCommand } from '@sight/app/application/group/command/enablePortfolio/EnablePortfolioCommand'; | ||
import { EnablePortfolioCommandHandler } from '@sight/app/application/group/command/enablePortfolio/EnablePortfolioCommandHandler'; | ||
|
||
import { Group } from '@sight/app/domain/group/model/Group'; | ||
import { | ||
GroupRepository, | ||
IGroupRepository, | ||
} from '@sight/app/domain/group/IGroupRepository'; | ||
|
||
import { DomainFixture } from '@sight/__test__/fixtures'; | ||
import { Message } from '@sight/constant/message'; | ||
|
||
describe('EnablePortfolioCommandHandler', () => { | ||
let handler: EnablePortfolioCommandHandler; | ||
let groupRepository: jest.Mocked<IGroupRepository>; | ||
|
||
beforeAll(async () => { | ||
advanceTo(new Date()); | ||
|
||
const testModule = await Test.createTestingModule({ | ||
providers: [ | ||
EnablePortfolioCommandHandler, | ||
{ provide: GroupRepository, useValue: {} }, | ||
], | ||
}).compile(); | ||
|
||
handler = testModule.get(EnablePortfolioCommandHandler); | ||
groupRepository = testModule.get(GroupRepository); | ||
}); | ||
|
||
afterAll(() => { | ||
clear(); | ||
}); | ||
|
||
describe('handle', () => { | ||
let group: Group; | ||
|
||
const groupId = 'groupId'; | ||
const groupAdminUserId = 'groupAdminUserId'; | ||
|
||
beforeEach(() => { | ||
group = DomainFixture.generateGroup({ | ||
adminUserId: groupAdminUserId, | ||
hasPortfolio: false, | ||
}); | ||
|
||
groupRepository.findById = jest.fn().mockResolvedValue(group); | ||
|
||
groupRepository.save = jest.fn(); | ||
}); | ||
|
||
test('그룹이 존재하지 않는다면 예외를 발생시켜야 한다', async () => { | ||
groupRepository.findById = jest.fn().mockResolvedValue(null); | ||
|
||
await expect( | ||
handler.execute(new EnablePortfolioCommand(groupId, groupAdminUserId)), | ||
).rejects.toThrowError(Message.GROUP_NOT_FOUND); | ||
}); | ||
|
||
test('요청자가 그룹장이 아니라면 예외를 발생시켜야 한다', async () => { | ||
const otherUserId = 'otherUserId'; | ||
|
||
await expect( | ||
handler.execute(new EnablePortfolioCommand(groupId, otherUserId)), | ||
).rejects.toThrowError(Message.ONLY_GROUP_ADMIN_CAN_EDIT_GROUP); | ||
}); | ||
|
||
test('그룹이 고객센터 그룹이라면 예외를 발생시켜야 한다', async () => { | ||
jest.spyOn(group, 'isCustomerServiceGroup').mockReturnValue(true); | ||
|
||
await expect( | ||
handler.execute(new EnablePortfolioCommand(groupId, groupAdminUserId)), | ||
).rejects.toThrowError(Message.CANNOT_MODIFY_CUSTOMER_SERVICE_GROUP); | ||
}); | ||
|
||
test('그룹의 포트폴리오를 활성화 시켜야 한다', async () => { | ||
jest.spyOn(group, 'enablePortfolio'); | ||
|
||
await handler.execute( | ||
new EnablePortfolioCommand(groupId, groupAdminUserId), | ||
); | ||
|
||
expect(group.enablePortfolio).toBeCalled(); | ||
}); | ||
|
||
test('그룹을 저장해야 한다', async () => { | ||
await handler.execute( | ||
new EnablePortfolioCommand(groupId, groupAdminUserId), | ||
); | ||
|
||
expect(groupRepository.save).toBeCalledWith(group); | ||
expect(groupRepository.save).toBeCalledTimes(1); | ||
}); | ||
}); | ||
}); |
51 changes: 51 additions & 0 deletions
51
src/app/application/group/command/enablePortfolio/EnablePortfolioCommandHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; | ||
import { | ||
ForbiddenException, | ||
Inject, | ||
NotFoundException, | ||
UnprocessableEntityException, | ||
} from '@nestjs/common'; | ||
|
||
import { Transactional } from '@sight/core/persistence/transaction/Transactional'; | ||
|
||
import { EnablePortfolioCommand } from '@sight/app/application/group/command/enablePortfolio/EnablePortfolioCommand'; | ||
|
||
import { | ||
GroupRepository, | ||
IGroupRepository, | ||
} from '@sight/app/domain/group/IGroupRepository'; | ||
|
||
import { Message } from '@sight/constant/message'; | ||
|
||
@CommandHandler(EnablePortfolioCommand) | ||
export class EnablePortfolioCommandHandler | ||
implements ICommandHandler<EnablePortfolioCommand> | ||
{ | ||
constructor( | ||
@Inject(GroupRepository) | ||
private readonly groupRepository: IGroupRepository, | ||
) {} | ||
|
||
@Transactional() | ||
async execute(command: EnablePortfolioCommand): Promise<void> { | ||
const { groupId, requesterUserId } = command; | ||
|
||
const group = await this.groupRepository.findById(groupId); | ||
if (!group) { | ||
throw new NotFoundException(Message.GROUP_NOT_FOUND); | ||
} | ||
|
||
if (group.adminUserId !== requesterUserId) { | ||
throw new ForbiddenException(Message.ONLY_GROUP_ADMIN_CAN_EDIT_GROUP); | ||
} | ||
|
||
if (group.isCustomerServiceGroup()) { | ||
throw new UnprocessableEntityException( | ||
Message.CANNOT_MODIFY_CUSTOMER_SERVICE_GROUP, | ||
); | ||
} | ||
|
||
group.enablePortfolio(); | ||
await this.groupRepository.save(group); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
src/app/application/group/eventHandler/GroupPortfolioEnabledHandler.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// TODO: 핸들러의 구현 방향성이 잡히고, 리팩토링이 완료되면 테스트 작성 예정 | ||
describe('GroupPortfolioEnabledHandler', () => { | ||
test.todo('그룹이 존재하지 않으면 아무것도 하지 않아야 한다'); | ||
|
||
test.todo('모든 그룹 멤버들에게 포인트를 지급해야 한다'); | ||
|
||
test.todo('그룹 로그를 생성해야 한다'); | ||
|
||
test.todo('메시지를 전송해야 한다'); | ||
}); |
89 changes: 89 additions & 0 deletions
89
src/app/application/group/eventHandler/GroupPortfolioEnabledHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { Inject } from '@nestjs/common'; | ||
import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; | ||
|
||
import { Transactional } from '@sight/core/persistence/transaction/Transactional'; | ||
|
||
import { GroupPortfolioEnabled } from '@sight/app/domain/group/event/GroupPortfolioEnabled'; | ||
import { GroupLogFactory } from '@sight/app/domain/group/GroupLogFactory'; | ||
import { SlackMessageCategory } from '@sight/app/domain/message/model/constant'; | ||
import { | ||
ISlackSender, | ||
SlackSender, | ||
} from '@sight/app/domain/adapter/ISlackSender'; | ||
import { | ||
GroupLogRepository, | ||
IGroupLogRepository, | ||
} from '@sight/app/domain/group/IGroupLogRepository'; | ||
import { | ||
GroupMemberRepository, | ||
IGroupMemberRepository, | ||
} from '@sight/app/domain/group/IGroupMemberRepository'; | ||
import { | ||
GroupRepository, | ||
IGroupRepository, | ||
} from '@sight/app/domain/group/IGroupRepository'; | ||
import { | ||
IUserRepository, | ||
UserRepository, | ||
} from '@sight/app/domain/user/IUserRepository'; | ||
|
||
import { Point } from '@sight/constant/point'; | ||
|
||
@EventsHandler(GroupPortfolioEnabled) | ||
export class GroupPortfolioEnabledHandler | ||
implements IEventHandler<GroupPortfolioEnabled> | ||
{ | ||
constructor( | ||
@Inject(GroupRepository) | ||
private readonly groupRepository: IGroupRepository, | ||
@Inject(GroupMemberRepository) | ||
private readonly groupMemberRepository: IGroupMemberRepository, | ||
@Inject(GroupLogFactory) | ||
private readonly groupLogFactory: GroupLogFactory, | ||
@Inject(GroupLogRepository) | ||
private readonly groupLogRepository: IGroupLogRepository, | ||
@Inject(UserRepository) | ||
private readonly userRepository: IUserRepository, | ||
@Inject(SlackSender) | ||
private readonly slackSender: ISlackSender, | ||
) {} | ||
|
||
@Transactional() | ||
async handle(event: GroupPortfolioEnabled): Promise<void> { | ||
const { groupId } = event; | ||
|
||
const group = await this.groupRepository.findById(groupId); | ||
if (!group) { | ||
return; | ||
} | ||
|
||
const groupMembers = | ||
await this.groupMemberRepository.findByGroupId(groupId); | ||
const userIds = groupMembers.map((groupMember) => groupMember.userId); | ||
const users = await this.userRepository.findByIds(userIds); | ||
|
||
users.forEach((user) => | ||
user.grantPoint( | ||
Point.GROUP_ENABLED_PORTFOLIO, | ||
`<u>${group.title}</u> 그룹의 포트폴리오가 발행되었습니다.`, | ||
), | ||
); | ||
await this.userRepository.save(...users); | ||
|
||
// TODO: 그룹 로거를 만들어서 사용하도록 수정 | ||
const groupLog = this.groupLogFactory.create({ | ||
id: this.groupLogRepository.nextId(), | ||
groupId, | ||
userId: '', // TODO: 요청자 정보에 접근할 수 있을 때 수정 | ||
message: '포트폴리오가 발행 중입니다.', | ||
}); | ||
await this.groupLogRepository.save(groupLog); | ||
|
||
this.slackSender.send({ | ||
category: SlackMessageCategory.GROUP_ACTIVITY, | ||
message: `<a href="/group/'${groupId}'"><u>${group.title}</u></a> 그룹의 <a href="/folio/'${groupId}'" target="_blank">포트폴리오</a>가 발행 중입니다.`, | ||
sourceUserId: null, | ||
targetUserId: '', // TODO: 요청자 정보에 접근할 수 있을 때 수정 | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export class GroupPortfolioEnabled { | ||
constructor(readonly groupId: string) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters