Skip to content

Commit

Permalink
feat: implement chamas service
Browse files Browse the repository at this point in the history
  • Loading branch information
okjodom committed Feb 11, 2025
1 parent 49e60d1 commit 2094344
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 20 deletions.
4 changes: 2 additions & 2 deletions apps/api/src/chamas/chamas.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export class ChamasController {
@Get('find/:id')
@ApiOperation({ summary: 'Find existing Chama by ID' })
@ApiParam({ name: 'chamaId', description: 'Chama ID' })
async findChama(@Param('id') id: string) {
return this.chamasService.findChama({ chamaId: id });
async findChama(@Param('chamaId') chamaId: string) {
return this.chamasService.findChama({ chamaId });
}

@Get('filter')
Expand Down
190 changes: 172 additions & 18 deletions libs/common/src/chamas/chamas.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Injectable, Logger } from '@nestjs/common';
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import {
CreateChamaDto,
FilterChamasDto,
Expand All @@ -7,17 +12,36 @@ import {
JoinChamaDto,
UpdateChamaDto,
} from '../dto';
import { type Chama } from '../types';
import { toChama } from '../database';
import { ChamaMember, type Chama } from '../types';
import { UsersService } from '../users';
import { ChamasRepository } from './chamas.repository';

interface ChamaFilter {
createdBy?: string;
members?: { $elemMatch: { memberId: string } };
}

interface ResolvedMembers {
registered: ChamaMember[];
nonRegistered: ChamaMember[];
}

interface ChamaResponse {
// created or updated chama
chama: Chama;
// list of members to be invited
memberInvites: ChamaMember[];
}

export interface IChamasService {
createChama(req: CreateChamaDto): Promise<Chama>;
createChama(req: CreateChamaDto): Promise<ChamaResponse>;

updateChama(req: UpdateChamaDto): Promise<Chama>;
updateChama(req: UpdateChamaDto): Promise<ChamaResponse>;

joinChama(req: JoinChamaDto): Promise<Chama>;
inviteMembers(req: InviteMembersDto): Promise<ChamaResponse>;

inviteMembers(req: InviteMembersDto): Promise<Chama>;
joinChama(req: JoinChamaDto): Promise<Chama>;

findChama(req: FindChamaDto): Promise<Chama>;

Expand All @@ -28,31 +52,161 @@ export interface IChamasService {
export class ChamasService implements IChamasService {
private readonly logger = new Logger(ChamasService.name);

constructor(private readonly chamas: ChamasRepository) {
constructor(
private readonly chamas: ChamasRepository,
private readonly users: UsersService,
) {
this.logger.debug('ChamasService initialized');
}

createChama(req: CreateChamaDto): Promise<Chama> {
throw new Error('Method not implemented.');
async createChama({
name,
description,
members,
createdBy,
}: CreateChamaDto): Promise<ChamaResponse> {
const { registered, nonRegistered } = await this.resolveMembers(members);

if (!registered.find((member) => member.userId === createdBy)) {
throw new BadRequestException('Failed to create chama', {
cause: new Error('Invalid chama creator'),
description:
'Seems the proposed chama creator, is not yet a registered member',
});
}

const cd = await this.chamas.create({
name,
description,
members: registered,
createdBy,
});

return {
chama: toChama(cd),
memberInvites: nonRegistered,
};
}

updateChama(req: UpdateChamaDto): Promise<Chama> {
throw new Error('Method not implemented.');
private async resolveMembers(
proposed: ChamaMember[],
): Promise<ResolvedMembers> {
if (!proposed?.length) {
return { registered: [], nonRegistered: [] };
}

const uniqueMembers = [
...new Map(proposed.map((member) => [member.userId, member])).values(),
];

try {
const userIds = uniqueMembers.map((member) => member.userId);
const existingUsers = await this.users.findUsersById(new Set(userIds));

const existingUserIds = new Set(existingUsers.map((user) => user.id));

return uniqueMembers.reduce<ResolvedMembers>(
(acc, member) => {
if (existingUserIds.has(member.userId)) {
acc.registered.push(member);
} else {
acc.nonRegistered.push(member);
}
return acc;
},
{ registered: [], nonRegistered: [] },
);
} catch (error) {
this.logger.error('Failed to resolve members', {
error,
proposedMembers: uniqueMembers,
});
throw new InternalServerErrorException('Failed to verify members');
}
}

joinChama(req: JoinChamaDto): Promise<Chama> {
throw new Error('Method not implemented.');
private deduplicateMembers(current: ChamaMember[], updates: ChamaMember[]) {
return [...current, ...updates].filter(
(member, index, self) =>
index === self.findIndex((m) => m.userId === member.userId),
);
}

inviteMembers(req: InviteMembersDto): Promise<Chama> {
throw new Error('Method not implemented.');
async updateChama({
chamaId,
updates,
}: UpdateChamaDto): Promise<ChamaResponse> {
const cd = await this.chamas.findOne({ _id: chamaId });

const hunk: Partial<Chama> = {};

if (updates.name) {
hunk.name = updates.name;
}

if (updates.description) {
hunk.description = updates.description;
}

let memberInvites = [];
if (updates.members) {
const { registered, nonRegistered } = await this.resolveMembers(
updates.members,
);
hunk.members = this.deduplicateMembers(cd.members, registered);
memberInvites = nonRegistered;
}

const updatedChama = await this.chamas.findOneAndUpdate(
{ _id: chamaId },
hunk,
);

return {
chama: toChama(updatedChama),
memberInvites,
};
}

findChama(req: FindChamaDto): Promise<Chama> {
throw new Error('Method not implemented.');
async joinChama({ chamaId, memberInfo }: JoinChamaDto): Promise<Chama> {
const cd = await this.chamas.findOne({ _id: chamaId });
const hunk: Partial<Chama> = {
members: this.deduplicateMembers(cd.members, [memberInfo]),
};

const updatedChama = await this.chamas.findOneAndUpdate(
{ _id: chamaId },
hunk,
);

return toChama(updatedChama);
}

filterChamas(req: FilterChamasDto): Promise<Chama[]> {
inviteMembers(req: InviteMembersDto): Promise<ChamaResponse> {
throw new Error('Method not implemented.');
}

async findChama({ chamaId }: FindChamaDto): Promise<Chama> {
const cd = await this.chamas.findOne({ _id: chamaId });
return toChama(cd);
}

async filterChamas({
createdBy,
memberId,
}: FilterChamasDto): Promise<Chama[]> {
const filter: ChamaFilter = {};

if (createdBy) {
filter.createdBy = createdBy;
}

if (memberId) {
filter.members = { $elemMatch: { memberId: memberId } };
}

const cds = await this.chamas.find(filter);

return cds.map(toChama);
}
}
4 changes: 4 additions & 0 deletions libs/common/src/dto/chama.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export class CreateChamaDto implements CreateChamaRequest {
@ApiProperty({ example: 'Kenya Bitcoiners' })
name: string;

@IsChamaName()
@ApiProperty({ example: 'Kenya Bitcoiners' })
description?: string;

@IsMembers(1, 100)
@ApiProperty({
type: [ChamaMemberDto],
Expand Down
12 changes: 12 additions & 0 deletions libs/common/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ export class UsersService implements IUsersService {
return toUser(ud);
}

async findUsersById(ids: Set<string>): Promise<User[]> {
if (!ids.size) return [];

const uds = await this.users.find({
where: {
id: { in: [...ids] },
},
});

return uds.map(toUser);
}

async verifyUser({
otp,
phone,
Expand Down

0 comments on commit 2094344

Please sign in to comment.