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
4 changes: 4 additions & 0 deletions apps/backend/src/config/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-R
import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields';
import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData';
import { RemovePantryFromOrders1769316004958 } from '../migrations/1769316004958-RemovePantryFromOrders';
import { AddDonationRecurrenceFields1770080947285 } from '../migrations/1770080947285-AddDonationRecurrenceFields';
import { AddFoodRescueToDonationItems1770679339809 } from '../migrations/1770679339809-AddFoodRescueToDonationItems';
import { UpdateManufacturerEntity1768680807820 } from '../migrations/1768680807820-UpdateManufacturerEntity';

const schemaMigrations = [
Expand Down Expand Up @@ -55,6 +57,8 @@ const schemaMigrations = [
RemoveUnusedStatuses1764816885341,
PopulateDummyData1768501812134,
RemovePantryFromOrders1769316004958,
AddDonationRecurrenceFields1770080947285,
AddFoodRescueToDonationItems1770679339809,
UpdateManufacturerEntity1768680807820,
];

Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/donationItems/donationItems.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@ export class DonationItem {

@OneToMany(() => Allocation, (allocation) => allocation.item)
allocations: Allocation[];

@Column({ name: 'food_rescue', type: 'boolean', default: false })
foodRescue: boolean;
}
57 changes: 24 additions & 33 deletions apps/backend/src/donations/donations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
Param,
NotFoundException,
ParseIntPipe,
BadRequestException,
} from '@nestjs/common';
import { ApiBody } from '@nestjs/swagger';
import { Donation } from './donations.entity';
import { DonationService } from './donations.service';
import { DonationStatus } from './types';
import { RecurrenceEnum } from './types';
import { CreateDonationDto } from './dtos/create-donation.dto';

@Controller('donations')
export class DonationsController {
Expand All @@ -28,6 +28,13 @@ export class DonationsController {
return this.donationService.getNumberOfDonations();
}

@Get('/donations/:foodManufacturerId')
async getDonationsByFoodManufacturer(
@Param('foodManufacturerId', ParseIntPipe) foodManufacturerId: number,
): Promise<Donation[]> {
return this.donationService.getByFoodManufacturer(foodManufacturerId);
}

@Get('/:donationId')
async getDonation(
@Param('donationId', ParseIntPipe) donationId: number,
Expand All @@ -42,46 +49,30 @@ export class DonationsController {
type: 'object',
properties: {
foodManufacturerId: { type: 'integer', example: 1 },
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE FOR PR Reviewer: We deleted this since it does not make sense to have either of these fields here in creating a donation. The status will always be the same, as will the date donated.

dateDonated: {
type: 'string',
format: 'date-time',
},
status: {
type: 'string',
enum: Object.values(DonationStatus),
example: DonationStatus.AVAILABLE,
},
totalItems: { type: 'integer', example: 100 },
totalOz: { type: 'integer', example: 500 },
totalEstimatedValue: { type: 'integer', example: 1000 },
recurrence: {
type: 'string',
enum: Object.values(RecurrenceEnum),
example: RecurrenceEnum.NONE,
},
recurrenceFreq: { type: 'integer', example: 1, nullable: true },
nextDonationDates: {
type: 'array',
items: { type: 'string', format: 'date-time' },
example: ['2024-07-01T00:00:00Z', '2024-08-01T00:00:00Z'],
nullable: true,
},
occurrencesRemaining: { type: 'integer', example: 2, nullable: true },
},
},
})
async createDonation(
@Body()
body: {
foodManufacturerId: number;
dateDonated: Date;
status: DonationStatus;
totalItems: number;
totalOz: number;
totalEstimatedValue: number;
},
body: CreateDonationDto,
): Promise<Donation> {
if (
body.status &&
!Object.values(DonationStatus).includes(body.status as DonationStatus)
) {
throw new BadRequestException('Invalid status');
}
return this.donationService.create(
body.foodManufacturerId,
body.dateDonated,
body.status ?? DonationStatus.AVAILABLE,
body.totalItems,
body.totalOz,
body.totalEstimatedValue,
);
return this.donationService.create(body);
}

@Patch('/:donationId/fulfill')
Expand Down
39 changes: 31 additions & 8 deletions apps/backend/src/donations/donations.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@ import {
JoinColumn,
ManyToOne,
} from 'typeorm';
import { DonationStatus, RecurrenceEnum } from './types';
import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';
import { DonationStatus } from './types';

@Entity('donations')
export class Donation {
@PrimaryGeneratedColumn({ name: 'donation_id' })
donationId: number;
donationId!: number;

@ManyToOne(() => FoodManufacturer, (manufacturer) => manufacturer.donations, {
nullable: false,
})
@JoinColumn({ name: 'food_manufacturer_id' })
foodManufacturer: FoodManufacturer;
foodManufacturer!: FoodManufacturer;

@CreateDateColumn({
name: 'date_donated',
type: 'timestamp',
default: () => 'NOW()',
})
dateDonated: Date;
dateDonated!: Date;

@Column({
name: 'status',
Expand All @@ -34,14 +34,37 @@ export class Donation {
enumName: 'donations_status_enum',
default: DonationStatus.AVAILABLE,
})
status: DonationStatus;
status!: DonationStatus;

@Column({ name: 'total_items', type: 'int', nullable: true })
totalItems: number;
totalItems?: number;

@Column({ name: 'total_oz', type: 'int', nullable: true })
totalOz: number;
totalOz?: number;

@Column({ name: 'total_estimated_value', type: 'int', nullable: true })
totalEstimatedValue: number;
totalEstimatedValue?: number;

@Column({
name: 'recurrence',
type: 'enum',
enum: RecurrenceEnum,
enumName: 'donation_recurrence_enum',
default: RecurrenceEnum.NONE,
})
recurrence!: RecurrenceEnum;

@Column({ name: 'recurrence_freq', type: 'int', nullable: true })
recurrenceFreq?: number;

@Column({
name: 'next_donation_dates',
type: 'timestamptz',
array: true,
nullable: true,
})
nextDonationDates?: Date[];

@Column({ name: 'occurrences_remaining', type: 'int', nullable: true })
occurrencesRemaining?: number;
}
45 changes: 31 additions & 14 deletions apps/backend/src/donations/donations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Donation } from './donations.entity';
import { validateId } from '../utils/validation.utils';
import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';
import { DonationStatus } from './types';
import { CreateDonationDto } from './dtos/create-donation.dto';
import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';

@Injectable()
export class DonationService {
Expand Down Expand Up @@ -38,15 +39,9 @@ export class DonationService {
return this.repo.count();
}

async create(
foodManufacturerId: number,
dateDonated: Date,
status: DonationStatus,
totalItems: number,
totalOz: number,
totalEstimatedValue: number,
) {
async getByFoodManufacturer(foodManufacturerId: number): Promise<Donation[]> {
validateId(foodManufacturerId, 'Food Manufacturer');

const manufacturer = await this.manufacturerRepo.findOne({
where: { foodManufacturerId },
});
Expand All @@ -56,13 +51,35 @@ export class DonationService {
`Food Manufacturer ${foodManufacturerId} not found`,
);
}

return this.repo.find({
where: { foodManufacturer: { foodManufacturerId } },
relations: ['foodManufacturer'],
});
}

async create(donationData: CreateDonationDto): Promise<Donation> {
validateId(donationData.foodManufacturerId, 'Food Manufacturer');
const manufacturer = await this.manufacturerRepo.findOne({
where: { foodManufacturerId: donationData.foodManufacturerId },
});

if (!manufacturer) {
throw new NotFoundException(
`Food Manufacturer ${donationData.foodManufacturerId} not found`,
);
}
const donation = this.repo.create({
foodManufacturer: manufacturer,
dateDonated,
status,
totalItems,
totalOz,
totalEstimatedValue,
dateDonated: new Date(),
status: DonationStatus.AVAILABLE,
totalItems: donationData.totalItems,
totalOz: donationData.totalOz,
totalEstimatedValue: donationData.totalEstimatedValue,
recurrence: donationData.recurrence,
recurrenceFreq: donationData.recurrenceFreq,
nextDonationDates: donationData.nextDonationDates,
occurrencesRemaining: donationData.occurrencesRemaining,
});

return this.repo.save(donation);
Expand Down
55 changes: 55 additions & 0 deletions apps/backend/src/donations/dtos/create-donation.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
ArrayNotEmpty,
IsArray,
IsDate,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
Min,
ValidateIf,
} from 'class-validator';
import { DonationStatus, RecurrenceEnum } from '../types';
import { Type } from 'class-transformer';

export class CreateDonationDto {
@IsNumber()
@Min(1)
foodManufacturerId!: number;

@IsNumber()
@Min(1)
@IsOptional()
totalItems?: number;

@IsNumber({ maxDecimalPlaces: 2 })
@Min(0.01)
@IsOptional()
totalOz?: number;

@IsNumber({ maxDecimalPlaces: 2 })
@Min(0.01)
@IsOptional()
totalEstimatedValue?: number;

@IsNotEmpty()
@IsEnum(RecurrenceEnum)
recurrence!: RecurrenceEnum;

@IsNumber()
@ValidateIf((o) => o.recurrence !== RecurrenceEnum.NONE)
@Min(1)
recurrenceFreq?: number;

@Type(() => Date)
@IsArray()
@ArrayNotEmpty()
@IsDate({ each: true })
@ValidateIf((o) => o.recurrence !== RecurrenceEnum.NONE)
nextDonationDates?: Date[];

@IsNumber()
@ValidateIf((o) => o.recurrence !== RecurrenceEnum.NONE)
@Min(1)
occurrencesRemaining?: number;
}
7 changes: 7 additions & 0 deletions apps/backend/src/donations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ export enum DonationStatus {
FULFILLED = 'fulfilled',
MATCHING = 'matching',
}

export enum RecurrenceEnum {
NONE = 'none',
WEEKLY = 'weekly',
MONTHLY = 'monthly',
YEARLY = 'yearly',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddDonationRecurrenceFields1770080947285
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TYPE donation_recurrence_enum AS ENUM (
'none',
'weekly',
'monthly',
'yearly'
);
`);

await queryRunner.query(`
ALTER TABLE donations
ADD COLUMN recurrence donation_recurrence_enum NOT NULL DEFAULT 'none',
ADD COLUMN recurrence_freq INTEGER,
ADD COLUMN next_donation_dates TIMESTAMP WITH TIME ZONE[],
ADD COLUMN occurrences_remaining INTEGER;
`);

await queryRunner.query(`
ALTER TABLE donations
ADD CONSTRAINT recurrence_fields_not_null CHECK (
(recurrence = 'none'
AND recurrence_freq IS NULL
AND next_donation_dates IS NULL
AND occurrences_remaining IS NULL)
OR
(recurrence != 'none'
AND recurrence_freq IS NOT NULL
AND next_donation_dates IS NOT NULL
AND occurrences_remaining IS NOT NULL)
);
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE donations
DROP CONSTRAINT recurrence_fields_not_null,
DROP COLUMN recurrence,
DROP COLUMN recurrence_freq,
DROP COLUMN next_donation_dates,
DROP COLUMN occurrences_remaining;
DROP TYPE donation_recurrence_enum;
`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddFoodRescueToDonationItems1770679339809
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE donation_items
ADD COLUMN food_rescue boolean NOT NULL DEFAULT false
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE donation_items
DROP COLUMN food_rescue
`);
}
}
Loading