Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions backend/src/donations/donations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
68 changes: 52 additions & 16 deletions backend/src/donations/donations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand All @@ -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');
Expand All @@ -210,19 +247,18 @@ 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) {
user.currentIntakeLoad = Math.max(0, user.currentIntakeLoad - donation.quantity);
await transactionalEntityManager.save(user);
}
donation.claimedById = null;
donation.volunteerId = null;
donation.pickedUpAt = null;
}

donation.status = status;
Expand Down
9 changes: 9 additions & 0 deletions backend/src/donations/entities/donation.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down