reserves module
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AuthorizationService } from '../authorization/authorization.service';
|
||||
import { ReservePolicy } from '../authorization/policies/reserve.policy';
|
||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||
import { CustomersService } from '../customers/customers.service';
|
||||
import { SalonsService } from '../salons/salons.service';
|
||||
import { ServiceSkill } from '../services/entities/service-skill.entity';
|
||||
import { Service } from '../services/entities/service.entity';
|
||||
import { StylistsService } from '../stylists/stylists.service';
|
||||
import { UserRole } from '../users/enums/user-role.enum';
|
||||
import { CreateReserveDto } from './dto/create-reserve.dto';
|
||||
import { FindReservesQueryDto } from './dto/find-reserves-query.dto';
|
||||
import { UpdateReserveDto } from './dto/update-reserve.dto';
|
||||
import { ReserveItem } from './entities/reserve-item.entity';
|
||||
import { Reserve } from './entities/reserve.entity';
|
||||
import { ReserveStatus } from './enums/reserve-status.enum';
|
||||
|
||||
@Injectable()
|
||||
export class ReservesService {
|
||||
constructor(
|
||||
@InjectRepository(Reserve)
|
||||
private readonly reservesRepository: Repository<Reserve>,
|
||||
@InjectRepository(ReserveItem)
|
||||
private readonly reserveItemsRepository: Repository<ReserveItem>,
|
||||
@InjectRepository(Service)
|
||||
private readonly servicesRepository: Repository<Service>,
|
||||
@InjectRepository(ServiceSkill)
|
||||
private readonly serviceSkillsRepository: Repository<ServiceSkill>,
|
||||
private readonly salonsService: SalonsService,
|
||||
private readonly customersService: CustomersService,
|
||||
private readonly stylistsService: StylistsService,
|
||||
private readonly authorizationService: AuthorizationService,
|
||||
private readonly reservePolicy: ReservePolicy,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
createReserveDto: CreateReserveDto,
|
||||
user: AuthUser,
|
||||
): Promise<Reserve> {
|
||||
const salon = await this.salonsService.findOne(createReserveDto.salonId);
|
||||
const customer = await this.customersService.findOne(
|
||||
createReserveDto.customerId,
|
||||
);
|
||||
const stylist = await this.stylistsService.findOne(
|
||||
createReserveDto.stylistId,
|
||||
);
|
||||
|
||||
if (stylist.salonId !== salon.id) {
|
||||
throw new BadRequestException(
|
||||
'Stylist does not belong to the selected salon',
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.reservePolicy.canCreate(user, { salon, customer })) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
this.validateTimeRange(createReserveDto.startTime, createReserveDto.endTime);
|
||||
this.validateAmounts(
|
||||
createReserveDto.totalAmount,
|
||||
createReserveDto.depositAmount,
|
||||
);
|
||||
await this.validateItems(createReserveDto.items);
|
||||
|
||||
const reserve = this.reservesRepository.create({
|
||||
salonId: createReserveDto.salonId,
|
||||
customerId: createReserveDto.customerId,
|
||||
stylistId: createReserveDto.stylistId,
|
||||
appointmentDate: createReserveDto.appointmentDate,
|
||||
startTime: this.normalizeTime(createReserveDto.startTime),
|
||||
endTime: this.normalizeTime(createReserveDto.endTime),
|
||||
totalAmount: createReserveDto.totalAmount,
|
||||
depositAmount: createReserveDto.depositAmount,
|
||||
remainingAmount:
|
||||
createReserveDto.totalAmount - createReserveDto.depositAmount,
|
||||
items: createReserveDto.items.map((item) =>
|
||||
this.reserveItemsRepository.create(item),
|
||||
),
|
||||
});
|
||||
|
||||
const saved = await this.reservesRepository.save(reserve);
|
||||
return this.findOne(saved.id, user);
|
||||
}
|
||||
|
||||
async findAll(
|
||||
user: AuthUser,
|
||||
query: FindReservesQueryDto,
|
||||
): Promise<Reserve[]> {
|
||||
const qb = this.reservesRepository
|
||||
.createQueryBuilder('reserve')
|
||||
.leftJoinAndSelect('reserve.salon', 'salon')
|
||||
.leftJoinAndSelect('reserve.customer', 'customer')
|
||||
.leftJoinAndSelect('customer.user', 'customerUser')
|
||||
.leftJoinAndSelect('reserve.stylist', 'stylist')
|
||||
.leftJoinAndSelect('stylist.user', 'stylistUser')
|
||||
.leftJoinAndSelect('reserve.items', 'items')
|
||||
.leftJoinAndSelect('items.service', 'service')
|
||||
.leftJoinAndSelect('items.serviceSkill', 'serviceSkill')
|
||||
.orderBy('reserve.appointmentDate', 'ASC')
|
||||
.addOrderBy('reserve.startTime', 'ASC');
|
||||
|
||||
if (query.salonId) {
|
||||
qb.andWhere('reserve.salonId = :salonId', { salonId: query.salonId });
|
||||
}
|
||||
if (query.customerId) {
|
||||
qb.andWhere('reserve.customerId = :customerId', {
|
||||
customerId: query.customerId,
|
||||
});
|
||||
}
|
||||
if (query.stylistId) {
|
||||
qb.andWhere('reserve.stylistId = :stylistId', {
|
||||
stylistId: query.stylistId,
|
||||
});
|
||||
}
|
||||
if (query.status) {
|
||||
qb.andWhere('reserve.status = :status', { status: query.status });
|
||||
}
|
||||
if (query.appointmentDate) {
|
||||
qb.andWhere('reserve.appointmentDate = :appointmentDate', {
|
||||
appointmentDate: query.appointmentDate,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.authorizationService.isAdmin(user)) {
|
||||
if (user.role === UserRole.SALON) {
|
||||
qb.andWhere('salon.ownerId = :ownerId', { ownerId: user.id });
|
||||
} else if (user.role === UserRole.CUSTOMER) {
|
||||
qb.andWhere('customer.userId = :userId', { userId: user.id });
|
||||
} else {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: string, user: AuthUser): Promise<Reserve> {
|
||||
const reserve = await this.reservesRepository.findOne({
|
||||
where: { id },
|
||||
relations: this.defaultRelations,
|
||||
});
|
||||
if (!reserve) {
|
||||
throw new NotFoundException(`Reserve #${id} not found`);
|
||||
}
|
||||
if (!this.reservePolicy.canRead(user, reserve)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return reserve;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
updateReserveDto: UpdateReserveDto,
|
||||
user: AuthUser,
|
||||
): Promise<Reserve> {
|
||||
const reserve = await this.findOne(id, user);
|
||||
if (!this.reservePolicy.canUpdate(user, reserve)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const {
|
||||
appointmentDate,
|
||||
startTime,
|
||||
endTime,
|
||||
status,
|
||||
totalAmount,
|
||||
depositAmount,
|
||||
cancellationReason,
|
||||
} = updateReserveDto;
|
||||
|
||||
if (appointmentDate !== undefined) {
|
||||
reserve.appointmentDate = appointmentDate;
|
||||
}
|
||||
if (startTime !== undefined) {
|
||||
reserve.startTime = this.normalizeTime(startTime);
|
||||
}
|
||||
if (endTime !== undefined) {
|
||||
reserve.endTime = this.normalizeTime(endTime);
|
||||
}
|
||||
|
||||
this.validateTimeRange(reserve.startTime, reserve.endTime);
|
||||
|
||||
if (totalAmount !== undefined) {
|
||||
reserve.totalAmount = totalAmount;
|
||||
}
|
||||
if (depositAmount !== undefined) {
|
||||
reserve.depositAmount = depositAmount;
|
||||
}
|
||||
if (totalAmount !== undefined || depositAmount !== undefined) {
|
||||
this.validateAmounts(reserve.totalAmount, reserve.depositAmount);
|
||||
reserve.remainingAmount = reserve.totalAmount - reserve.depositAmount;
|
||||
}
|
||||
|
||||
if (status !== undefined) {
|
||||
reserve.status = status;
|
||||
if (status === ReserveStatus.CANCELLED) {
|
||||
reserve.cancellationReason = cancellationReason ?? null;
|
||||
} else if (cancellationReason !== undefined) {
|
||||
reserve.cancellationReason = cancellationReason;
|
||||
}
|
||||
} else if (cancellationReason !== undefined) {
|
||||
reserve.cancellationReason = cancellationReason;
|
||||
}
|
||||
|
||||
await this.reservesRepository.save(reserve);
|
||||
return this.findOne(id, user);
|
||||
}
|
||||
|
||||
private readonly defaultRelations = {
|
||||
salon: true,
|
||||
customer: { user: true },
|
||||
stylist: { user: true },
|
||||
items: { service: true, serviceSkill: true },
|
||||
} as const;
|
||||
|
||||
private validateTimeRange(startTime: string, endTime: string): void {
|
||||
if (startTime >= endTime) {
|
||||
throw new BadRequestException('endTime must be after startTime');
|
||||
}
|
||||
}
|
||||
|
||||
private validateAmounts(totalAmount: number, depositAmount: number): void {
|
||||
if (depositAmount > totalAmount) {
|
||||
throw new BadRequestException(
|
||||
'depositAmount cannot exceed totalAmount',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeTime(time: string): string {
|
||||
return time.length === 5 ? `${time}:00` : time;
|
||||
}
|
||||
|
||||
private async validateItems(
|
||||
items: CreateReserveDto['items'],
|
||||
): Promise<void> {
|
||||
for (const item of items) {
|
||||
const serviceExists = await this.servicesRepository.existsBy({
|
||||
id: item.serviceId,
|
||||
});
|
||||
if (!serviceExists) {
|
||||
throw new BadRequestException(`Service #${item.serviceId} not found`);
|
||||
}
|
||||
|
||||
const skill = await this.serviceSkillsRepository.findOne({
|
||||
where: { id: item.serviceSkillId, serviceId: item.serviceId },
|
||||
});
|
||||
if (!skill) {
|
||||
throw new BadRequestException(
|
||||
`Service skill #${item.serviceSkillId} not found for service #${item.serviceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user