Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions apps/backend/src/config/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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 { UpdateManufacturerEntity1768680807820 } from '../migrations/1768680807820-UpdateManufacturerEntity';
import { AddUserPoolId1769189327767 } from '../migrations/1769189327767-AddUserPoolId';
import { UpdateOrderEntity1769990652833 } from '../migrations/1769990652833-UpdateOrderEntity';
Expand Down Expand Up @@ -57,6 +58,7 @@ const schemaMigrations = [
RemoveUnusedStatuses1764816885341,
PopulateDummyData1768501812134,
RemovePantryFromOrders1769316004958,
AddDonationRecurrenceFields1770080947285,
UpdateManufacturerEntity1768680807820,
AddUserPoolId1769189327767,
UpdateOrderEntity1769990652833,
Expand Down
44 changes: 19 additions & 25 deletions apps/backend/src/donations/donations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
import { ApiBody } from '@nestjs/swagger';
import { Donation } from './donations.entity';
import { DonationService } from './donations.service';
import { DonationStatus } from './types';
import { DonationStatus, RecurrenceEnum } from './types';
import { CreateDonationDto } from './dtos/create-donation.dto';

@Controller('donations')
export class DonationsController {
Expand Down Expand Up @@ -52,36 +53,29 @@ export class DonationsController {
example: DonationStatus.AVAILABLE,
},
totalItems: { type: 'integer', example: 100 },
totalOz: { type: 'integer', example: 500 },
totalEstimatedValue: { type: 'integer', example: 1000 },
totalOz: { type: 'number', example: 100.5 },
totalEstimatedValue: { type: 'number', example: 100.5 },
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
43 changes: 33 additions & 10 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;
@Column({ name: 'total_oz', type: 'numeric', nullable: true })
totalOz?: number;

@Column({ name: 'total_estimated_value', type: 'int', nullable: true })
totalEstimatedValue: number;
@Column({ name: 'total_estimated_value', type: 'numeric', nullable: true })
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;
}
32 changes: 15 additions & 17 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,31 +39,28 @@ export class DonationService {
return this.repo.count();
}

async create(
foodManufacturerId: number,
dateDonated: Date,
status: DonationStatus,
totalItems: number,
totalOz: number,
totalEstimatedValue: number,
) {
validateId(foodManufacturerId, 'Food Manufacturer');
async create(donationData: CreateDonationDto): Promise<Donation> {
validateId(donationData.foodManufacturerId, 'Food Manufacturer');
const manufacturer = await this.manufacturerRepo.findOne({
where: { foodManufacturerId },
where: { foodManufacturerId: donationData.foodManufacturerId },
});

if (!manufacturer) {
throw new NotFoundException(
`Food Manufacturer ${foodManufacturerId} not found`,
`Food Manufacturer ${donationData.foodManufacturerId} not found`,
);
}
const donation = this.repo.create({
foodManufacturer: manufacturer,
dateDonated,
status,
totalItems,
totalOz,
totalEstimatedValue,
dateDonated: donationData.dateDonated,
status: donationData.status,
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
64 changes: 64 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,64 @@
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;

@Type(() => Date)
@IsDate()
@IsNotEmpty()
dateDonated!: Date;

@IsNotEmpty()
@IsEnum(DonationStatus)
status!: DonationStatus;

@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',
}
5 changes: 1 addition & 4 deletions apps/backend/src/foodRequests/request.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,7 @@ describe('RequestsController', () => {
};

// Mock Photos
const mockStream = new Readable();
mockStream._read = () => {
// no-op
};
const mockStream = {} as Readable;

const photos: Express.Multer.File[] = [
{
Expand Down
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;
`);
}
}
2 changes: 1 addition & 1 deletion apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export class ApiClient {
userId: number,
body: { role: string },
): Promise<void> {
return this.axiosInstance.put(`/api/users/${userId}/role`, body);
await this.axiosInstance.put(`/api/users/${userId}/role`, body);
}

public async getOrderFoodRequest(requestId: number): Promise<FoodRequest> {
Expand Down
Loading