import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; import { Prisma, Shifts } from "@prisma/client"; import { UpsertShiftDto } from "../dtos/upsert-shift.dto"; import { BaseApprovalService } from "src/common/shared/base-approval.service"; import { PrismaService } from "src/prisma/prisma.service"; import { toDateOnly } from "../helpers/shifts-date-time-helpers"; import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; import { ShiftsHelpersService } from "../helpers/shifts.helpers"; import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; @Injectable() export class ShiftsCommandService extends BaseApprovalService { private readonly logger = new Logger(ShiftsCommandService.name); constructor( prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, private readonly typeResolver: BankCodesResolver, private readonly helpersService: ShiftsHelpersService, ) { super(prisma); } //_____________________________________________________________________________________________ // APPROVAL AND DELEGATE METHODS //_____________________________________________________________________________________________ protected get delegate() { return this.prisma.shifts; } protected delegateFor(transaction: Prisma.TransactionClient) { return transaction.shifts; } async updateApproval(id: number, is_approved: boolean): Promise { return this.prisma.$transaction((transaction) => this.updateApprovalWithTransaction(transaction, id, is_approved), ); } //_____________________________________________________________________________________________ // MASTER CRUD METHOD //_____________________________________________________________________________________________ async upsertShifts( email: string, action: UpsertAction, dto: UpsertShiftDto, ): Promise<{ action: UpsertAction; day: DayShiftResponse[]; }> { if (!dto.old_shift && !dto.new_shift) throw new BadRequestException('At least one of old or new shift must be provided'); const date = dto.new_shift?.date ?? dto.old_shift?.date; if (!date) throw new BadRequestException("A date (YYYY-MM-DD) must be provided in old_shift or new_shift"); if (dto.old_shift?.date && dto.new_shift?.date && dto.old_shift.date !== dto.new_shift.date) { throw new BadRequestException('old_shift.date and new_shift.date must be identical'); } const employee_id = await this.emailResolver.findIdByEmail(email);//resolve employee id using email if(action === 'create') { if(!dto.new_shift || dto.old_shift) { throw new BadRequestException(`Only new_shift must be provided for create`); } return this.createShift(employee_id, date, dto); } if(action === 'update'){ if(!dto.old_shift || !dto.new_shift) { throw new BadRequestException(`Both new_shift and old_shift must be provided for update`); } return this.updateShift(employee_id, date, dto); } throw new BadRequestException(`Unknown action: ${action}`); } //_________________________________________________________________ // CREATE //_________________________________________________________________ private async createShift( employee_id: number, date_iso: string, dto: UpsertShiftDto, ): Promise<{action: UpsertAction; day: DayShiftResponse[]}> { return this.prisma.$transaction(async (tx) => { const date_only = toDateOnly(date_iso); const { id: timesheet_id } = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift); const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift'); const day_shifts = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift); await tx.shifts.create({ data: { timesheet_id, date: date_only, start_time: new_norm_shift.start_time, end_time: new_norm_shift.end_time, is_remote: new_norm_shift.is_remote, is_approved: new_norm_shift.is_approved, comment: new_norm_shift.comment ?? null, bank_code_id: new_bank_code_id, }, }); await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only,'create'); const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); return { action: 'create', day: await this.helpersService.mapDay(fresh_shift)}; }); } //_________________________________________________________________ // UPDATE //_________________________________________________________________ private async updateShift( employee_id: number, date_iso: string, dto: UpsertShiftDto, ): Promise<{ action: UpsertAction; day: DayShiftResponse[];}>{ return this.prisma.$transaction(async (tx) => { const date_only = toDateOnly(date_iso); const { id: timesheet_id } = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); const old_norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift, 'new_shift'); const old_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, old_norm_shift.type, 'old_shift'); const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift'); const day_shifts = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); const existing = await this.helpersService.findExactOldShift(tx, { timesheet_id, date_only, norm: old_norm_shift, bank_code_id: old_bank_code_id, }); if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift, existing.id); await tx.shifts.update({ where: { id: existing.id }, data: { start_time: new_norm_shift.start_time, end_time: new_norm_shift.end_time, is_remote: new_norm_shift.is_remote, comment: new_norm_shift.comment ?? null, bank_code_id: new_bank_code_id, }, }); await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only, 'update'); const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); return { action: 'update', day: await this.helpersService.mapDay(fresh_shift)}; }); } //_________________________________________________________________ // DELETE //_________________________________________________________________ async deleteShift( email: string, date_iso: string, dto: UpsertShiftDto, ): Promise<{ day: DayShiftResponse[]; }>{ return this.prisma.$transaction(async (tx) => { const date_only = toDateOnly(date_iso); const employee_id = await this.emailResolver.findIdByEmail(email); const { id: timesheet_id } = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); const norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); const bank_code_id = await this.typeResolver.findByType(norm_shift.type); const existing = await this.helpersService.findExactOldShift(tx, { timesheet_id, date_only, norm: norm_shift, bank_code_id: bank_code_id.id, }); if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); await tx.shifts.delete({ where: { id: existing.id } }); await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only, 'delete'); const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); return { day: await this.helpersService.mapDay(fresh_shift)}; }); } }