diff --git a/docker-compose.yml b/docker-compose.yml index 642e5d4..febd6a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: volumes: - .:/usr/src/app - ./node_modules:/usr/src/app/node_modules - restart: unless-stopped + restart: always depends_on: - crowdfunding_database diff --git a/src/features/campaigns/campaigns.module.ts b/src/features/campaigns/campaigns.module.ts index 483d7dc..a433ecc 100644 --- a/src/features/campaigns/campaigns.module.ts +++ b/src/features/campaigns/campaigns.module.ts @@ -21,9 +21,15 @@ import { CampaignPledge, CampaignPledgeSchema, } from './schemas/campaign-pledge.schema'; +import { + CampaignClaim, + CampaignClaimSchema, +} from './schemas/campaign-claim.schema'; import { UsersRepository } from '../users/repositories/users/mongo/users.repository'; import { UsersModule } from '../users/users.module'; import { User, UserSchema } from '../users/schemas/user.schema'; +import { CampaignClaimService } from './services/campaign-claim/campaign-claim.service'; +import { CampaignClaimMongoRepository } from './repositories/mongo/campaign-claim/campaign-claim.repository'; @Module({ imports: [ @@ -40,6 +46,10 @@ import { User, UserSchema } from '../users/schemas/user.schema'; name: CampaignPledge.name, schema: CampaignPledgeSchema, }, + { + name: CampaignClaim.name, + schema: CampaignClaimSchema, + }, { name: User.name, schema: UserSchema, @@ -58,8 +68,10 @@ import { User, UserSchema } from '../users/schemas/user.schema'; CampaignLaunchMongoRepository, CampaignPledgeService, CampaignPledgeMongoRepository, + CampaignClaimMongoRepository, UsersRepository, + CampaignClaimService, ], - exports: [CampaignLaunchService, CampaignPledgeService], + exports: [CampaignLaunchService, CampaignPledgeService, CampaignClaimService], }) export class CampaignsModule {} diff --git a/src/features/campaigns/repositories/mongo/campaign-claim/campaign-claim.repository.spec.ts b/src/features/campaigns/repositories/mongo/campaign-claim/campaign-claim.repository.spec.ts new file mode 100644 index 0000000..4720393 --- /dev/null +++ b/src/features/campaigns/repositories/mongo/campaign-claim/campaign-claim.repository.spec.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { getModelToken } from '@nestjs/mongoose'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Model, ObjectId } from 'mongoose'; + +import { CampaignClaim } from 'src/features/campaigns/schemas/campaign-claim.schema'; +import { campaignClaimMock } from 'src/features/campaigns/tests/mocks'; +import { CampaignClaimMongoRepository } from './campaign-claim.repository'; + +describe('Campaign Claim Mongo Repository', () => { + let campaignClaimRepository: CampaignClaimMongoRepository; + let campaignClaimModel: Model; + + const campaignId = '634f3292a486274ca2f3d47f' as unknown as ObjectId; + const userId = '634f3292a486274ca2f3d47a' as unknown as ObjectId; + const tokenId = '634f3292a486274ca2f3d47b' as unknown as ObjectId; + const amount = '1'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CampaignClaimMongoRepository, + { + provide: getModelToken(CampaignClaim.name), + useValue: { + find: jest.fn(), + findOne: jest.fn(), + sort: jest.fn(), + skip: jest.fn(), + limit: jest.fn(), + lean: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }, + ], + }).compile(); + + campaignClaimRepository = module.get( + CampaignClaimMongoRepository, + ); + campaignClaimModel = module.get>( + getModelToken(CampaignClaim.name), + ); + }); + + it('should be defined', () => { + expect(campaignClaimRepository).toBeDefined(); + }); + + describe('create method', () => { + it('should call create method without errors', async () => { + jest + .spyOn(campaignClaimModel, 'create') + .mockReturnValue(campaignClaimMock as any); + + const response = await campaignClaimRepository.create({ + campaignId, + userId, + tokenId, + amount, + }); + + expect(response).toStrictEqual(campaignClaimMock); + }); + }); +}); diff --git a/src/features/campaigns/repositories/mongo/campaign-claim/campaign-claim.repository.ts b/src/features/campaigns/repositories/mongo/campaign-claim/campaign-claim.repository.ts new file mode 100644 index 0000000..3fba2aa --- /dev/null +++ b/src/features/campaigns/repositories/mongo/campaign-claim/campaign-claim.repository.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, ObjectId } from 'mongoose'; + +import { + CampaignClaim, + CampaignClaimDocument, +} from '../../../schemas/campaign-claim.schema'; + +@Injectable() +export class CampaignClaimMongoRepository { + constructor( + @InjectModel(CampaignClaim.name) + private campaignClaimModel: Model, + ) {} + + // TODO: possible to use an abstract class to define the interface? + async create({ + campaignId, + userId, + tokenId, + amount, + }: { + userId: ObjectId; + campaignId: ObjectId; + tokenId: ObjectId; + amount: string; + }) { + return await this.campaignClaimModel.create({ + campaign: campaignId, + user: userId, + token: tokenId, + amount, + date: new Date(), + }); + } +} diff --git a/src/features/campaigns/repositories/mongo/campaigns.repository.ts b/src/features/campaigns/repositories/mongo/campaigns.repository.ts index 4776e34..de8434e 100644 --- a/src/features/campaigns/repositories/mongo/campaigns.repository.ts +++ b/src/features/campaigns/repositories/mongo/campaigns.repository.ts @@ -151,7 +151,7 @@ export class CampaignsMongoRepository { updateCampaignDto: UpdateCampaignDto; }) { const existingCampaign = await this.campaignModel - .findOne({ _id: id }) + .findOne({ onchainId: id }) .exec(); if (!existingCampaign) { throw new NotFoundException(); diff --git a/src/features/campaigns/schemas/claim.schema.ts b/src/features/campaigns/schemas/campaign-claim.schema.ts similarity index 83% rename from src/features/campaigns/schemas/claim.schema.ts rename to src/features/campaigns/schemas/campaign-claim.schema.ts index 84767de..e4f06c7 100644 --- a/src/features/campaigns/schemas/claim.schema.ts +++ b/src/features/campaigns/schemas/campaign-claim.schema.ts @@ -5,10 +5,10 @@ import { User } from 'src/features/users/schemas/user.schema'; import { Campaign } from 'src/features/campaigns/schemas/campaign.schema'; import { Token } from 'src/features/tokens/schemas/token.schema'; -export type ClaimDocument = Claim & Document; +export type CampaignClaimDocument = CampaignClaim & Document; @Schema({ timestamps: { createdAt: 'created', updatedAt: 'updated' } }) -export class Claim { +export class CampaignClaim { @Prop({ index: true, type: MongooseSchema.Types.ObjectId, @@ -35,4 +35,4 @@ export class Claim { date: Date; } -export const ClaimSchema = SchemaFactory.createForClass(Claim); +export const CampaignClaimSchema = SchemaFactory.createForClass(CampaignClaim); diff --git a/src/features/campaigns/services/campaign-claim/campaign-claim.service.spec.ts b/src/features/campaigns/services/campaign-claim/campaign-claim.service.spec.ts new file mode 100644 index 0000000..ecd38f4 --- /dev/null +++ b/src/features/campaigns/services/campaign-claim/campaign-claim.service.spec.ts @@ -0,0 +1,160 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Test, TestingModule } from '@nestjs/testing'; + +import { + campaignClaimArgumentMock, + campaignClaimMock, + campaignPledgeArgumentMock, + mongoClaimedCampaingStatus, + tokenMock, + userMock, +} from '../../tests/mocks'; +import { campaignMock } from 'src/features/users/tests/mocks'; +import { CampaignsService } from 'src/features/campaigns/services/campaigns.service'; +import { TokensService } from 'src/features/tokens/services/tokens.service'; +import { UsersService } from 'src/features/users/services/users.service'; +import { CampaignStatusService } from 'src/features/campaign-statuses/services/campaign-statuses.service'; +import { CampaignClaimService } from 'src/features/campaigns/services/campaign-claim/campaign-claim.service'; +import { CampaignsMongoRepository } from 'src/features/campaigns/repositories/mongo/campaigns.repository'; +import { CampaignClaimMongoRepository } from 'src/features/campaigns/repositories/mongo/campaign-claim/campaign-claim.repository'; +import { UserCampaignsRepository } from 'src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository'; + +describe('CampaignClaimService', () => { + let campaignClaimService: CampaignClaimService; + let campaignService: CampaignsService; + let usersService: UsersService; + let tokensService: TokensService; + let campaignStatusService: CampaignStatusService; + let campaignClaimMongoRepository: CampaignClaimMongoRepository; + let campaignMongoRepository: CampaignsMongoRepository; + let userCampaignsMongoRepository: UserCampaignsRepository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CampaignClaimService, + { + provide: CampaignsService, + useValue: { + findOne: jest.fn(), + }, + }, + { + provide: CampaignStatusService, + useValue: { + getStatusByCode: jest.fn(), + }, + }, + { + provide: UsersService, + useValue: { + findUserByAddress: jest.fn(), + }, + }, + { + provide: TokensService, + useValue: { + getByDefault: jest.fn(), + }, + }, + { + provide: CampaignClaimMongoRepository, + useValue: { + create: jest.fn(), + }, + }, + { + provide: CampaignsMongoRepository, + useValue: { + update: jest.fn(), + }, + }, + { + provide: UserCampaignsRepository, + useValue: { + updateUserCampaignByEvent: jest.fn(), + }, + }, + ], + }).compile(); + + campaignClaimService = + module.get(CampaignClaimService); + campaignService = module.get(CampaignsService); + usersService = module.get(UsersService); + tokensService = module.get(TokensService); + campaignStatusService = module.get( + CampaignStatusService, + ); + campaignClaimMongoRepository = module.get( + CampaignClaimMongoRepository, + ); + campaignMongoRepository = module.get( + CampaignsMongoRepository, + ); + userCampaignsMongoRepository = module.get( + UserCampaignsRepository, + ); + }); + + it('should be defined', () => { + expect(campaignClaimService).toBeDefined(); + }); + + describe('create method', () => { + it('should call create method and fails because arguments are not right', async () => { + await expect(() => + campaignClaimService.create('WrongArgument'), + ).rejects.toThrow(); + }); + + it('should call create method and fails because there is no associated campaign', async () => { + jest.spyOn(campaignService, 'findOne').mockResolvedValue(null as any); + jest + .spyOn(usersService, 'findUserByAddress') + .mockResolvedValue(userMock as any); + jest + .spyOn(tokensService, 'getByDefault') + .mockResolvedValue(tokenMock as any); + + await expect(() => + campaignClaimService.create(campaignClaimArgumentMock), + ).rejects.toThrow(); + + expect(campaignService.findOne).toBeCalledTimes(1); + expect(usersService.findUserByAddress).toBeCalledTimes(1); + expect(tokensService.getByDefault).toBeCalledTimes(1); + }); + + it('should call create method without errors', async () => { + jest + .spyOn(campaignService, 'findOne') + .mockResolvedValue({ campaign: campaignMock } as any); + jest + .spyOn(usersService, 'findUserByAddress') + .mockResolvedValue(userMock as any); + jest + .spyOn(tokensService, 'getByDefault') + .mockResolvedValue(tokenMock as any); + jest + .spyOn(campaignClaimMongoRepository, 'create') + .mockResolvedValue(campaignClaimMock as any); + jest + .spyOn(campaignStatusService, 'getStatusByCode') + .mockResolvedValue(mongoClaimedCampaingStatus as any); + jest.spyOn(campaignMongoRepository, 'update').mockResolvedValue(null); + jest + .spyOn(userCampaignsMongoRepository, 'updateUserCampaignByEvent') + .mockResolvedValue(null); + + await expect(() => + campaignClaimService.create(campaignPledgeArgumentMock), + ).not.toThrow(); + + expect(campaignService.findOne).toBeCalledTimes(1); + expect(usersService.findUserByAddress).toBeCalledTimes(1); + expect(tokensService.getByDefault).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/features/campaigns/services/campaign-claim/campaign-claim.service.ts b/src/features/campaigns/services/campaign-claim/campaign-claim.service.ts new file mode 100644 index 0000000..c91ac84 --- /dev/null +++ b/src/features/campaigns/services/campaign-claim/campaign-claim.service.ts @@ -0,0 +1,66 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { CampaignsService } from 'src/features/campaigns/services/campaigns.service'; +import { TokensService } from 'src/features/tokens/services/tokens.service'; +import { UsersService } from 'src/features/users/services/users.service'; +import { CampaignEventService } from '../common/campaign-event.service'; +import { CampaignStatusService } from 'src/features/campaign-statuses/services/campaign-statuses.service'; +import { CampaignClaimMongoRepository } from 'src/features/campaigns/repositories/mongo/campaign-claim/campaign-claim.repository'; +import { CampaignsMongoRepository } from 'src/features/campaigns/repositories/mongo/campaigns.repository'; +import { UserCampaignsRepository } from 'src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository'; +import { CrowdfundingEvent } from 'src/features/events/types'; + +@Injectable() +export class CampaignClaimService extends CampaignEventService { + private readonly logger = new Logger(CampaignClaimService.name); + private readonly claimStatusCode = 'claimed'; + + constructor( + readonly campaignService: CampaignsService, + readonly usersService: UsersService, + readonly tokensService: TokensService, + private readonly campaignClaimMongoRepository: CampaignClaimMongoRepository, + private readonly campaignMongoRepository: CampaignsMongoRepository, + private readonly userCampaignsMongoRepository: UserCampaignsRepository, + private readonly campaignStatusService: CampaignStatusService, + ) { + super(campaignService, usersService, tokensService); + } + + async create(eventData: unknown) { + if (!Array.isArray(eventData)) { + throw new Error('Event data is corrupted'); + } + + const [onchainId, userAddress, amount] = eventData; + const { campaign, user, token } = await this.getMetadata({ + onchainId, + userAddress, + }); + + const savedClaim = await this.campaignClaimMongoRepository.create({ + campaignId: campaign._id, + userId: user._id, + tokenId: token._id, + amount, + }); + + const { _id: claimStatusId } = + await this.campaignStatusService.getStatusByCode(this.claimStatusCode); + + await this.campaignMongoRepository.update({ + id: parseInt(onchainId).toString(), + updateCampaignDto: { + status: claimStatusId, + }, + }); + + await this.userCampaignsMongoRepository.updateUserCampaignByEvent({ + campaign, + user, + token, + event: savedClaim, + eventType: CrowdfundingEvent.Claim, + }); + } +} diff --git a/src/features/campaigns/services/campaign-pledge/campaign-pledge.service.spec.ts b/src/features/campaigns/services/campaign-pledge/campaign-pledge.service.spec.ts index 82df7d4..e4150c0 100644 --- a/src/features/campaigns/services/campaign-pledge/campaign-pledge.service.spec.ts +++ b/src/features/campaigns/services/campaign-pledge/campaign-pledge.service.spec.ts @@ -64,7 +64,7 @@ describe('CampaignPledgeService', () => { { provide: UserCampaignsRepository, useValue: { - updateUserCampaignByPledge: jest.fn(), + updateUserCampaignByEvent: jest.fn(), }, }, ], @@ -111,7 +111,7 @@ describe('CampaignPledgeService', () => { .spyOn(campaignMongoRepository, 'updateTokenAmount') .mockResolvedValue(null); jest - .spyOn(userCampaignsMongoRepository, 'updateUserCampaignByPledge') + .spyOn(userCampaignsMongoRepository, 'updateUserCampaignByEvent') .mockResolvedValue(null); await expect(() => diff --git a/src/features/campaigns/services/campaign-pledge/campaign-pledge.service.ts b/src/features/campaigns/services/campaign-pledge/campaign-pledge.service.ts index f170a8c..d2fa8d5 100644 --- a/src/features/campaigns/services/campaign-pledge/campaign-pledge.service.ts +++ b/src/features/campaigns/services/campaign-pledge/campaign-pledge.service.ts @@ -10,6 +10,7 @@ import { UserDocument } from 'src/features/users/schemas/user.schema'; import { CampaignDocument } from '../../schemas/campaign.schema'; import { UserCampaignsRepository } from 'src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository'; import { movementTypeEnum } from '../../constants'; +import { CrowdfundingEvent } from 'src/features/events/types'; @Injectable() export class CampaignPledgeService { @@ -49,14 +50,16 @@ export class CampaignPledgeService { action: movementTypeEnum.INCREASE, }); - await this.userCampaignsMongoRepository.updateUserCampaignByPledge({ + await this.userCampaignsMongoRepository.updateUserCampaignByEvent({ campaign, user, token, - pledge: savedPledge, + event: savedPledge, + eventType: CrowdfundingEvent.Pledge, }); } + // TODO: Use abstract class CampaignEventService private async getMetadata({ onchainId, userAddress, diff --git a/src/features/campaigns/services/common/campaign-event.service.ts b/src/features/campaigns/services/common/campaign-event.service.ts new file mode 100644 index 0000000..b2bf352 --- /dev/null +++ b/src/features/campaigns/services/common/campaign-event.service.ts @@ -0,0 +1,43 @@ +import { TokenDocument } from 'src/features/tokens/schemas/token.schema'; +import { UserDocument } from 'src/features/users/schemas/user.schema'; +import { CampaignDocument } from '../../schemas/campaign.schema'; +import { CampaignsService } from 'src/features/campaigns/services/campaigns.service'; +import { TokensService } from 'src/features/tokens/services/tokens.service'; +import { UsersService } from 'src/features/users/services/users.service'; + +export abstract class CampaignEventService { + constructor( + readonly campaignService: CampaignsService, + readonly usersService: UsersService, + readonly tokensService: TokensService, + ) {} + + async getMetadata({ + onchainId, + userAddress, + }: { + onchainId: string; + userAddress: string; + }): Promise<{ + campaign: CampaignDocument; + user: UserDocument; + token: TokenDocument; + }> { + const promises = [ + this.campaignService.findOne(onchainId), + this.usersService.findUserByAddress(userAddress), + this.tokensService.getByDefault(), + ]; + + const promiseResults = await Promise.all(promises); + if (promiseResults.some((result) => !result)) { + throw new Error('No metadata associated'); + } + + const { campaign } = promiseResults[0] as { campaign: CampaignDocument }; + const user = promiseResults[1] as UserDocument; + const token = promiseResults[2] as TokenDocument; + + return { campaign, user, token }; + } +} diff --git a/src/features/campaigns/tests/mocks/index.ts b/src/features/campaigns/tests/mocks/index.ts index 1eceab0..56d390b 100644 --- a/src/features/campaigns/tests/mocks/index.ts +++ b/src/features/campaigns/tests/mocks/index.ts @@ -164,6 +164,7 @@ export const mongoLaunchedCampaign = { }; export const campaignPledgeArgumentMock = [onchainId, userAddress, amount]; +export const campaignClaimArgumentMock = [onchainId, userAddress, amount]; export const userMock = { username: 'rcargnelutti', @@ -240,6 +241,18 @@ export const campaignPledgeMock = { __v: 0, }; +export const campaignClaimMock = { + _id: '639b969369bed1e35eeca7aa', + campaign: '638e605ab710197625b571ba', + user: '634dd92c34361cf5a21fb96a', + token: '63611e69143b8def9c4843da', + amount: '2000000000000000000', + date: '2023-01-15T21:50:11.066+0000', + created: '2023-01-12T21:50:11.072+0000', + updated: '2023-11-12T21:50:11.072+0000', + __v: 0, +}; + export const campaignLaunchEventDto = { creator: launchEventData[2], goal: launchEventData[1], @@ -257,3 +270,12 @@ export const findCampaignToLaunchData = { export const requestWithUser = { user: { ...userMock }, } as unknown as Request; + +export const mongoClaimedCampaingStatus = { + _id: '73611e68143b8def9c4843de', + name: 'Claimed', + code: 'claimed', + created: '2022-10-18T23:11:14.611Z', + updated: '2022-10-18T23:11:14.611Z', + __v: 0, +}; diff --git a/src/features/events/services/events.service.ts b/src/features/events/services/events.service.ts index 52d782d..6323438 100644 --- a/src/features/events/services/events.service.ts +++ b/src/features/events/services/events.service.ts @@ -11,6 +11,7 @@ import { contractsToHandle, eventsToHandle } from 'src/common/contracts'; import { EventsMongoRepository } from '../repositories/mongo/events.repository'; import { CrowdfundingEvent } from '../types'; import { CampaignLaunchService } from 'src/features/campaigns/services/campaign-launch.service'; +import { CampaignClaimService } from 'src/features/campaigns/services/campaign-claim/campaign-claim.service'; import { CampaignPledgeService } from 'src/features/campaigns/services/campaign-pledge/campaign-pledge.service'; @Injectable() @@ -25,6 +26,7 @@ export class EventsService private eventsMongoRepository: EventsMongoRepository, private campaignLaunchService: CampaignLaunchService, private campaignPledgeService: CampaignPledgeService, + private campaignClaimService: CampaignClaimService, ) {} onApplicationShutdown() { @@ -78,6 +80,9 @@ export class EventsService case CrowdfundingEvent.Pledge: await this.campaignPledgeService.create(data); break; + case CrowdfundingEvent.Claim: + await this.campaignClaimService.create(data); + break; } this.storeRawEvent(data, event); //FIXME could process repetead events. storeRawEvent does not distinguish. diff --git a/src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository.spec.ts b/src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository.spec.ts index 0c802af..e169a9a 100644 --- a/src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository.spec.ts +++ b/src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository.spec.ts @@ -8,11 +8,13 @@ import { UserCampaignsRepository } from './user-campaigns.repository'; import { UserCampaign } from 'src/features/users/schemas/user-campaign.schema'; import { campaignMock, + claimMock, pledgeMock, tokenMock, userCampaignMock, userMock, } from 'src/features/users/tests/mocks'; +import { CrowdfundingEvent } from 'src/features/events/types'; describe('UserCampaignsRepository', () => { let userCampaignsRepository: UserCampaignsRepository; @@ -41,34 +43,75 @@ describe('UserCampaignsRepository', () => { ); }); - describe('updateUserCampaignByPledge method', () => { - it('should call updateUserCampaignByPledge usersCampaignRepository method without errors for an non existing document', async () => { + describe('updateUserCampaignByEvent method when pledging', () => { + it('should call updateUserCampaignByEvent usersCampaignRepository method without errors for an non existing document', async () => { jest.spyOn(userCampaignModel, 'findOne').mockReturnValue(null); jest.spyOn(userCampaignModel, 'create').mockReturnValue({ + pledges: [], + totalPledged: '0', save: jest.fn(), } as any); await expect(() => - userCampaignsRepository.updateUserCampaignByPledge({ + userCampaignsRepository.updateUserCampaignByEvent({ campaign: campaignMock, user: userMock, token: tokenMock, - pledge: pledgeMock, + eventType: CrowdfundingEvent.Pledge, + event: pledgeMock, }), ).not.toThrow(); }); - it('should call updateUserCampaignByPledge usersCampaignRepository method without errors for an existing document', async () => { + it('should call updateUserCampaignByEvent usersCampaignRepository method without errors for an existing document', async () => { jest .spyOn(userCampaignModel, 'findOne') .mockReturnValue(userCampaignMock as any); await expect(() => - userCampaignsRepository.updateUserCampaignByPledge({ + userCampaignsRepository.updateUserCampaignByEvent({ campaign: campaignMock, user: userMock, token: tokenMock, - pledge: pledgeMock, + eventType: CrowdfundingEvent.Pledge, + event: pledgeMock, + }), + ).not.toThrow(); + }); + }); + + describe('updateUserCampaignByEvent method when pledging', () => { + it('should call updateUserCampaignByEvent usersCampaignRepository method without errors for an non existing document', async () => { + jest.spyOn(userCampaignModel, 'findOne').mockReturnValue(null); + jest.spyOn(userCampaignModel, 'create').mockReturnValue({ + claims: [], + totalClaimed: '0', + save: jest.fn(), + } as any); + + await expect(() => + userCampaignsRepository.updateUserCampaignByEvent({ + campaign: campaignMock, + user: userMock, + token: tokenMock, + eventType: CrowdfundingEvent.Claim, + event: claimMock, + }), + ).not.toThrow(); + }); + + it('should call updateUserCampaignByEvent usersCampaignRepository method without errors for an existing document', async () => { + jest + .spyOn(userCampaignModel, 'findOne') + .mockReturnValue(userCampaignMock as any); + + await expect(() => + userCampaignsRepository.updateUserCampaignByEvent({ + campaign: campaignMock, + user: userMock, + token: tokenMock, + eventType: CrowdfundingEvent.Claim, + event: claimMock, }), ).not.toThrow(); }); diff --git a/src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository.ts b/src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository.ts index 3ab46b7..9dfca25 100644 --- a/src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository.ts +++ b/src/features/users/repositories/user-campaigns/mongo/user-campaigns.repository.ts @@ -3,6 +3,7 @@ import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { formatEther, parseEther } from 'ethers/lib/utils'; +import { CrowdfundingEvent } from 'src/features/events/types/index'; import { CampaignPledgeDocument } from 'src/features/campaigns/schemas/campaign-pledge.schema'; import { CampaignDocument } from 'src/features/campaigns/schemas/campaign.schema'; import { TokenDocument } from 'src/features/tokens/schemas/token.schema'; @@ -19,49 +20,72 @@ export class UserCampaignsRepository { private userCampaignModel: Model, ) {} - // TODO: Consider to refactor this method once we add new events handlers - async updateUserCampaignByPledge({ + async updateUserCampaignByEvent({ campaign, user, token, - pledge, + eventType, + event, }: { campaign: CampaignDocument; user: UserDocument; token: TokenDocument; - pledge: CampaignPledgeDocument; + eventType: CrowdfundingEvent; + event: CampaignPledgeDocument | CampaignPledgeDocument; }): Promise { - const existingUserCampaign = await this.userCampaignModel.findOne({ + let associatedUserCampaign = await this.userCampaignModel.findOne({ campaign: campaign._id, user: user._id, token: token._id, }); - if (existingUserCampaign) { - existingUserCampaign.pledges.push(pledge._id); - existingUserCampaign.totalPledged = formatEther( - parseEther(existingUserCampaign.totalPledged).add( - parseEther(pledge.amount), - ), - ); - - await existingUserCampaign.save(); - } else { - const newUserCampaign = await this.userCampaignModel.create({ + if (!associatedUserCampaign) { + associatedUserCampaign = await this.userCampaignModel.create({ campaign: campaign._id, user: user._id, token: token._id, - totalPledged: formatEther(pledge.amount), + totalPledged: 0, totalUnpledged: 0, totalClaimed: 0, totalRefunded: 0, - pledges: [pledge._id], + pledges: [], unpledges: [], claims: [], refunds: [], }); + } + + const userCampignToSave = this.getUserCampaignUpdated({ + event, + eventType, + userCampaign: associatedUserCampaign, + }); - await newUserCampaign.save(); + await userCampignToSave.save(); + } + + private getUserCampaignUpdated({ + userCampaign, + eventType, + event, + }: { + userCampaign: UserCampaignDocument; + eventType: CrowdfundingEvent; + event: CampaignPledgeDocument | CampaignPledgeDocument; + }): UserCampaignDocument { + switch (eventType) { + case CrowdfundingEvent.Pledge: + userCampaign.pledges.push(event._id); + userCampaign.totalPledged = formatEther( + parseEther(userCampaign.totalPledged).add(parseEther(event.amount)), + ); + break; + case CrowdfundingEvent.Claim: + userCampaign.claims.push(event._id); + userCampaign.totalClaimed = formatEther(parseEther(event.amount)); + break; } + + return userCampaign; } } diff --git a/src/features/users/schemas/user-campaign.schema.ts b/src/features/users/schemas/user-campaign.schema.ts index ef2c307..c84f3c0 100644 --- a/src/features/users/schemas/user-campaign.schema.ts +++ b/src/features/users/schemas/user-campaign.schema.ts @@ -2,7 +2,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, Schema as MongooseSchema } from 'mongoose'; import { Campaign } from 'src/features/campaigns/schemas/campaign.schema'; -import { Claim } from 'src/features/campaigns/schemas/claim.schema'; +import { CampaignClaim } from 'src/features/campaigns/schemas/campaign-claim.schema'; import { CampaignPledge } from 'src/features/campaigns/schemas/campaign-pledge.schema'; import { Refund } from 'src/features/campaigns/schemas/refund.schema'; import { Unpledge } from 'src/features/campaigns/schemas/unpledge.schema'; @@ -58,10 +58,10 @@ export class UserCampaign { unpledges: Unpledge[]; @Prop({ - type: [{ type: MongooseSchema.Types.ObjectId, ref: 'Claim' }], + type: [{ type: MongooseSchema.Types.ObjectId, ref: 'CampaignClaim' }], default: [], }) - claims: Claim[]; + claims: CampaignClaim[]; @Prop({ type: [{ type: MongooseSchema.Types.ObjectId, ref: 'Refund' }], diff --git a/src/features/users/tests/mocks/index.ts b/src/features/users/tests/mocks/index.ts index b4e13be..f184592 100644 --- a/src/features/users/tests/mocks/index.ts +++ b/src/features/users/tests/mocks/index.ts @@ -1,3 +1,4 @@ +import { CampaignClaimDocument } from './../../../campaigns/schemas/campaign-claim.schema'; import { CampaignPledgeDocument } from 'src/features/campaigns/schemas/campaign-pledge.schema'; import { CampaignDocument } from 'src/features/campaigns/schemas/campaign.schema'; import { TokenDocument } from 'src/features/tokens/schemas/token.schema'; @@ -41,10 +42,17 @@ export const pledgeMock = { amount: '1', } as CampaignPledgeDocument; +export const claimMock = { + _id: '634f3292a486274ca2f3d47f', + amount: '1', +} as CampaignClaimDocument; + export const userCampaignMock = { _id: '634f3292a486274ca2f3d47f', - totalPledged: '1', + claims: [claimMock._id], pledges: [pledgeMock._id], + totalPledged: '1', + totalClaimed: '1', save: jest.fn(), } as unknown as UserCampaign;