diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index 82384673..9bb60358 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -27,6 +27,7 @@ import { RemoveMultipleVolunteerTypes1764811878152 } from '../migrations/1764811 import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-RemoveUnusedStatuses'; import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields'; import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData'; +import { AddDonationRecurrenceFields1770080947285 } from '../migrations/1770080947285-AddDonationRecurrenceFields'; const config = { type: 'postgres', @@ -67,6 +68,7 @@ const config = { RemoveMultipleVolunteerTypes1764811878152, RemoveUnusedStatuses1764816885341, PopulateDummyData1768501812134, + AddDonationRecurrenceFields1770080947285, ], }; diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 6bcd2a7e..0dd247c2 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -12,7 +12,7 @@ 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'; @Controller('donations') export class DonationsController { @@ -54,6 +54,19 @@ export class DonationsController { 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, + }, + occurences: { type: 'integer', example: 2, nullable: true }, }, }, }) @@ -66,6 +79,10 @@ export class DonationsController { totalItems: number; totalOz: number; totalEstimatedValue: number; + recurrence: RecurrenceEnum; + recurrenceFreq?: number; + nextDonationDates?: Date[]; + occurences?: number; }, ): Promise { if ( @@ -74,6 +91,14 @@ export class DonationsController { ) { throw new BadRequestException('Invalid status'); } + // If we got a recurrence, we should have all of these values + // The next donation dates should be a list of dates we will get from the frontend accordingly + if ( + body.recurrence != RecurrenceEnum.NONE && + (!body.recurrenceFreq || !body.nextDonationDates || !body.occurences) + ) { + throw new BadRequestException('recurrence details are incomplete'); + } return this.donationService.create( body.foodManufacturerId, body.dateDonated, @@ -81,6 +106,10 @@ export class DonationsController { body.totalItems, body.totalOz, body.totalEstimatedValue, + body.recurrence, + body.recurrenceFreq ?? 0, + body.nextDonationDates ?? null, + body.occurences ?? null, ); } diff --git a/apps/backend/src/donations/donations.entity.ts b/apps/backend/src/donations/donations.entity.ts index 1c40a7c0..304869cf 100644 --- a/apps/backend/src/donations/donations.entity.ts +++ b/apps/backend/src/donations/donations.entity.ts @@ -7,7 +7,7 @@ import { ManyToOne, } from 'typeorm'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; -import { DonationStatus } from './types'; +import { DonationStatus, RecurrenceEnum } from './types'; @Entity('donations') export class Donation { @@ -44,4 +44,27 @@ export class Donation { @Column({ name: 'total_estimated_value', type: 'int', 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: 'occurences', type: 'int', nullable: true }) + occurencesRemaining: number; } diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 6afaaee4..6ed31ed2 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -4,7 +4,7 @@ import { Repository } from 'typeorm'; import { Donation } from './donations.entity'; import { validateId } from '../utils/validation.utils'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; -import { DonationStatus } from './types'; +import { DonationStatus, RecurrenceEnum } from './types'; @Injectable() export class DonationService { @@ -45,7 +45,11 @@ export class DonationService { totalItems: number, totalOz: number, totalEstimatedValue: number, - ) { + recurrence: RecurrenceEnum, + recurrenceFreq: number, + nextDonationDates: Date[] | null, + occurences: number | null, + ): Promise { validateId(foodManufacturerId, 'Food Manufacturer'); const manufacturer = await this.manufacturerRepo.findOne({ where: { foodManufacturerId }, @@ -63,6 +67,10 @@ export class DonationService { totalItems, totalOz, totalEstimatedValue, + recurrence, + recurrenceFreq, + nextDonationDates, + occurences, }); return this.repo.save(donation); diff --git a/apps/backend/src/donations/types.ts b/apps/backend/src/donations/types.ts index 16387987..cb63fda3 100644 --- a/apps/backend/src/donations/types.ts +++ b/apps/backend/src/donations/types.ts @@ -3,3 +3,10 @@ export enum DonationStatus { FULFILLED = 'fulfilled', MATCHING = 'matching', } + +export enum RecurrenceEnum { + NONE = 'none', + WEEKLY = 'weekly', + MONTHLY = 'monthly', + YEARLY = 'yearly', +} diff --git a/apps/backend/src/migrations/1770080947285-AddDonationRecurrenceFields.ts b/apps/backend/src/migrations/1770080947285-AddDonationRecurrenceFields.ts new file mode 100644 index 00000000..8678852b --- /dev/null +++ b/apps/backend/src/migrations/1770080947285-AddDonationRecurrenceFields.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDonationRecurrenceFields1770080947285 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + 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 occurences 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 occurences IS NULL) + OR + (recurrence != 'none' + AND recurrence_freq IS NOT NULL + AND next_donation_dates IS NOT NULL + AND occurences IS NOT NULL) + ); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + 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 occurences; + + DROP TYPE donation_recurrence_enum; + `); + } +} diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index eb98e3a2..46540c55 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -103,6 +103,13 @@ export enum DonationStatus { MATCHING = 'matching', } +export enum RecurrenceEnum { + NONE = 'none', + WEEKLY = 'weekly', + MONTHLY = 'monthly', + YEARLY = 'yearly', +} + export interface Donation { donationId: number; dateDonated: string;