diff --git a/backend/src/donations/donations.controller.ts b/backend/src/donations/donations.controller.ts index 1619d74e..6ef42490 100644 --- a/backend/src/donations/donations.controller.ts +++ b/backend/src/donations/donations.controller.ts @@ -120,26 +120,49 @@ export class DonationsController { return this.donationsService.updateStatus(id, updateDto.status, req.user.userId); } + @Patch(':id/pickup') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Confirm food donation pickup (Volunteer only)' }) + @ApiResponse({ + status: 200, + description: 'Donation picked up successfully' + }) + @ApiResponse({ + status: 400, + description: 'Invalid state or unauthorized' + }) + @ApiResponse({ + status: 404, + description: 'Donation not found' + }) + pickup( + @Param('id') id: string, + @Req() req: any, + ) { + return this.donationsService.pickup(id, req.user.userId); + } + @Patch(':id/deliver') @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOperation({ summary: 'Mark a food donation as delivered' }) + @ApiOperation({ summary: 'Confirm food donation delivery (Volunteer only)' }) @ApiResponse({ status: 200, - description: 'Donation marked as delivered successfully' + description: 'Donation delivered successfully' }) @ApiResponse({ status: 400, - description: 'Donation already delivered or mismatch' + description: 'Invalid state or unauthorized' }) @ApiResponse({ status: 404, description: 'Donation not found' }) - markAsDelivered( + deliver( @Param('id') id: string, @Req() req: any, ) { - return this.donationsService.markAsDelivered(id, req.user.userId); + return this.donationsService.deliver(id, req.user.userId); } } \ No newline at end of file diff --git a/backend/src/donations/donations.service.ts b/backend/src/donations/donations.service.ts index 0b820a02..e95fe85c 100644 --- a/backend/src/donations/donations.service.ts +++ b/backend/src/donations/donations.service.ts @@ -152,34 +152,60 @@ export class DonationsService { }); } - async markAsDelivered(id: string, userId: string) { + async pickup(id: string, userId: string) { return await this.donationsRepository.manager.transaction(async transactionalEntityManager => { const donation = await transactionalEntityManager.findOne(Donation, { where: { id } }); + const user = await transactionalEntityManager.findOne(User, { where: { id: userId } }); if (!donation) { throw new NotFoundException('Donation not found'); } - - if (donation.claimedById !== userId) { - throw new BadRequestException('You can only mark your claimed donations as delivered'); + if (!user) { + throw new NotFoundException('User not found'); } - - if (donation.status === DonationStatus.DELIVERED) { - throw new BadRequestException('Donation already marked as delivered'); + if (user.role !== UserRole.VOLUNTEER) { + throw new BadRequestException('Only volunteers can confirm pickups'); + } + if (donation.status !== DonationStatus.CLAIMED) { + throw new BadRequestException('Donation must be claimed before pickup'); } + donation.status = DonationStatus.PICKED_UP; + donation.volunteerId = userId; + donation.pickedUpAt = new Date(); + + return await transactionalEntityManager.save(donation); + }); + } + + async deliver(id: string, userId: string) { + return await this.donationsRepository.manager.transaction(async transactionalEntityManager => { + const donation = await transactionalEntityManager.findOne(Donation, { where: { id } }); const user = await transactionalEntityManager.findOne(User, { where: { id: userId } }); + + if (!donation) { + throw new NotFoundException('Donation not found'); + } if (!user) { throw new NotFoundException('User not found'); } + if (user.role !== UserRole.VOLUNTEER) { + throw new BadRequestException('Only volunteers can confirm deliveries'); + } + if (donation.status !== DonationStatus.PICKED_UP) { + throw new BadRequestException('Donation must be picked up before delivery'); + } - // Update status to DELIVERED donation.status = DonationStatus.DELIVERED; + donation.deliveredAt = new Date(); - // Decrement current intake load - user.currentIntakeLoad = Math.max(0, user.currentIntakeLoad - donation.quantity); + // Also finalize the NGO's intake load + const ngo = await transactionalEntityManager.findOne(User, { where: { id: donation.claimedById } }); + if (ngo) { + ngo.currentIntakeLoad = Math.max(0, ngo.currentIntakeLoad - donation.quantity); + await transactionalEntityManager.save(ngo); + } - await transactionalEntityManager.save(user); return await transactionalEntityManager.save(donation); }); } @@ -199,7 +225,18 @@ export class DonationsService { const oldStatus = donation.status; - // If setting to DELIVERED, use the existing logic (which also decrements load) + // Prevent invalid state transitions + if (status === DonationStatus.PICKED_UP && oldStatus !== DonationStatus.CLAIMED) { + throw new BadRequestException('Donation must be CLAIMED before it can be PICKED_UP'); + } + if (status === DonationStatus.DELIVERED && oldStatus !== DonationStatus.PICKED_UP) { + throw new BadRequestException('Donation must be PICKED_UP before it can be DELIVERED'); + } + if (status === DonationStatus.CLAIMED && oldStatus !== DonationStatus.AVAILABLE) { + throw new BadRequestException('Donation must be AVAILABLE before it can be CLAIMED'); + } + + // If setting to DELIVERED, decrement NGO intake load if (status === DonationStatus.DELIVERED) { if (oldStatus === DonationStatus.DELIVERED) { throw new BadRequestException('Donation already marked as delivered'); @@ -210,12 +247,9 @@ export class DonationsService { user.currentIntakeLoad = Math.max(0, user.currentIntakeLoad - donation.quantity); await transactionalEntityManager.save(user); } - - donation.status = DonationStatus.DELIVERED; - return await transactionalEntityManager.save(donation); } - // If reversing a claim (CLAIMED/PICKED_UP -> AVAILABLE), decrement load + // If reversing a claim (CLAIMED/PICKED_UP -> AVAILABLE), decrement load and clear tracking fields if (status === DonationStatus.AVAILABLE && (oldStatus === DonationStatus.CLAIMED || oldStatus === DonationStatus.PICKED_UP)) { const user = await transactionalEntityManager.findOne(User, { where: { id: donation.claimedById } }); if (user) { @@ -223,6 +257,8 @@ export class DonationsService { await transactionalEntityManager.save(user); } donation.claimedById = null; + donation.volunteerId = null; + donation.pickedUpAt = null; } donation.status = status; diff --git a/backend/src/donations/entities/donation.entity.ts b/backend/src/donations/entities/donation.entity.ts index ff2042e1..acd1ae91 100644 --- a/backend/src/donations/entities/donation.entity.ts +++ b/backend/src/donations/entities/donation.entity.ts @@ -64,6 +64,15 @@ export class Donation { @Column({ nullable: true }) claimedById: string; + @Column({ nullable: true }) + volunteerId: string; + + @Column({ type: 'timestamp', nullable: true }) + pickedUpAt: Date; + + @Column({ type: 'timestamp', nullable: true }) + deliveredAt: Date; + @Column({ type: 'timestamp', nullable: true }) expiryTime: Date;