diff --git a/apps/backend/src/donations/donation.entity.ts b/apps/backend/src/donations/donation.entity.ts index dbb2e3a..658246f 100644 --- a/apps/backend/src/donations/donation.entity.ts +++ b/apps/backend/src/donations/donation.entity.ts @@ -1,16 +1,23 @@ import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; -export enum donationType { - 'one_time', - 'recurring', +export enum DonationType { + ONE_TIME = 'one_time', + RECURRING = 'recurring', } -export enum recurringInterval { - 'weekly', - 'monthly', - 'bimonthly', - 'quarterly', - 'annually', +export enum RecurringInterval { + WEEKLY = 'weekly', + MONTHLY = 'monthly', + BIMONTHLY = 'bimonthly', + QUARTERLY = 'quarterly', + ANNUALLY = 'annually', +} + +export enum DonationStatus { + PENDING = 'pending', + SUCCEEDED = 'succeeded', + FAILED = 'failed', + CANCELLED = 'cancelled', } @Entity() @@ -35,18 +42,24 @@ export class Donation { @Column({ default: false }) isAnonymous: boolean; - @Column() - donationType: donationType; + @Column({ type: 'varchar' }) + donationType: DonationType; - @Column({ nullable: true }) - recurringInterval: recurringInterval; + @Column({ type: 'varchar', nullable: true }) + recurringInterval: RecurringInterval | null; @Column({ nullable: true }) - dedicationMessage: string; + dedicationMessage: string | null; @Column({ default: false }) showDedicationPublicly: boolean; + @Column({ type: 'varchar', default: DonationStatus.PENDING }) + status: DonationStatus; + + @Column({ nullable: true }) + transactionId: string | null; + @Column() createdAt: Date; diff --git a/apps/backend/src/donations/donations.repository.spec.ts b/apps/backend/src/donations/donations.repository.spec.ts new file mode 100644 index 0000000..0a605f3 --- /dev/null +++ b/apps/backend/src/donations/donations.repository.spec.ts @@ -0,0 +1,405 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository, SelectQueryBuilder } from 'typeorm'; +import { DonationsRepository, PaginationFilters } from './donations.repository'; +import { + Donation, + DonationType, + DonationStatus, + RecurringInterval, +} from './donation.entity'; + +describe('DonationsRepository', () => { + let repository: DonationsRepository; + let mockTypeOrmRepo: jest.Mocked>; + let mockQueryBuilder: jest.Mocked>; + + const mockDonation: Donation = { + id: 1, + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + amount: 100.0, + isAnonymous: false, + donationType: DonationType.ONE_TIME, + recurringInterval: null, + dedicationMessage: 'In memory of Jane', + showDedicationPublicly: true, + status: DonationStatus.SUCCEEDED, + transactionId: 'txn_123456', + createdAt: new Date('2024-01-15T10:00:00Z'), + updatedAt: new Date('2024-01-15T10:00:00Z'), + }; + + beforeEach(async () => { + // Create mock query builder with all necessary methods + mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + orWhere: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + getManyAndCount: jest.fn(), + getMany: jest.fn(), + getRawOne: jest.fn(), + } as unknown as jest.Mocked>; + + // Create mock TypeORM repository + mockTypeOrmRepo = { + createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder), + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + } as unknown as jest.Mocked>; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DonationsRepository, + { + provide: getRepositoryToken(Donation), + useValue: mockTypeOrmRepo, + }, + ], + }).compile(); + + repository = module.get(DonationsRepository); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('findPaginated', () => { + it('should return paginated results without filters', async () => { + const mockDonations = [mockDonation, { ...mockDonation, id: 2 }]; + mockQueryBuilder.getManyAndCount.mockResolvedValue([mockDonations, 2]); + + const result = await repository.findPaginated(1, 10); + + expect(mockTypeOrmRepo.createQueryBuilder).toHaveBeenCalledWith( + 'donation', + ); + expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith( + 'donation.createdAt', + 'DESC', + ); + expect(mockQueryBuilder.skip).toHaveBeenCalledWith(0); + expect(mockQueryBuilder.take).toHaveBeenCalledWith(10); + expect(result).toEqual({ + rows: mockDonations, + total: 2, + page: 1, + perPage: 10, + totalPages: 1, + }); + }); + + it('should apply pagination correctly for page 2', async () => { + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 25]); + + await repository.findPaginated(2, 10); + + expect(mockQueryBuilder.skip).toHaveBeenCalledWith(10); + expect(mockQueryBuilder.take).toHaveBeenCalledWith(10); + }); + + it('should calculate total pages correctly', async () => { + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 25]); + + const result = await repository.findPaginated(1, 10); + + expect(result.totalPages).toBe(3); + }); + + it('should filter by donationType', async () => { + const filters: PaginationFilters = { + donationType: DonationType.RECURRING, + }; + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]); + + await repository.findPaginated(1, 10, filters); + + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.donationType = :donationType', + { donationType: DonationType.RECURRING }, + ); + }); + + it('should filter by isAnonymous', async () => { + const filters: PaginationFilters = { isAnonymous: true }; + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]); + + await repository.findPaginated(1, 10, filters); + + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.isAnonymous = :isAnonymous', + { isAnonymous: true }, + ); + }); + + it('should filter by recurringInterval', async () => { + const filters: PaginationFilters = { + recurringInterval: RecurringInterval.MONTHLY, + }; + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]); + + await repository.findPaginated(1, 10, filters); + + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.recurringInterval = :recurringInterval', + { recurringInterval: RecurringInterval.MONTHLY }, + ); + }); + + it('should filter by amount range', async () => { + const filters: PaginationFilters = { minAmount: 50, maxAmount: 200 }; + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]); + + await repository.findPaginated(1, 10, filters); + + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.amount >= :minAmount', + { minAmount: 50 }, + ); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.amount <= :maxAmount', + { maxAmount: 200 }, + ); + }); + + it('should filter by date range', async () => { + const startDate = new Date('2024-01-01'); + const endDate = new Date('2024-12-31'); + const filters: PaginationFilters = { startDate, endDate }; + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]); + + await repository.findPaginated(1, 10, filters); + + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.createdAt >= :startDate', + { startDate }, + ); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.createdAt <= :endDate', + { endDate }, + ); + }); + + it('should apply multiple filters together', async () => { + const filters: PaginationFilters = { + donationType: DonationType.RECURRING, + isAnonymous: false, + minAmount: 100, + }; + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]); + + await repository.findPaginated(1, 10, filters); + + expect(mockQueryBuilder.andWhere).toHaveBeenCalledTimes(3); + }); + + it('should filter by status', async () => { + const filters: PaginationFilters = { status: DonationStatus.SUCCEEDED }; + mockQueryBuilder.getManyAndCount.mockResolvedValue([[], 0]); + + await repository.findPaginated(1, 10, filters); + + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.status = :status', + { status: DonationStatus.SUCCEEDED }, + ); + }); + }); + + describe('searchByDonorNameOrEmail', () => { + it('should search by donor name or email with default limit', async () => { + const mockResults = [mockDonation]; + mockQueryBuilder.getMany.mockResolvedValue(mockResults); + + const result = await repository.searchByDonorNameOrEmail('john'); + + expect(mockTypeOrmRepo.createQueryBuilder).toHaveBeenCalledWith( + 'donation', + ); + expect(mockQueryBuilder.where).toHaveBeenCalled(); + expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith( + 'donation.createdAt', + 'DESC', + ); + expect(mockQueryBuilder.limit).toHaveBeenCalledWith(50); + expect(result).toEqual(mockResults); + }); + + it('should search with custom limit', async () => { + mockQueryBuilder.getMany.mockResolvedValue([]); + + await repository.searchByDonorNameOrEmail('jane', 10); + + expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10); + }); + + it('should handle empty search results', async () => { + mockQueryBuilder.getMany.mockResolvedValue([]); + + const result = await repository.searchByDonorNameOrEmail('nonexistent'); + + expect(result).toEqual([]); + }); + + it('should convert search term to lowercase', async () => { + mockQueryBuilder.getMany.mockResolvedValue([]); + + await repository.searchByDonorNameOrEmail('JOHN'); + + // The where clause should include the lowercase version + expect(mockQueryBuilder.where).toHaveBeenCalled(); + }); + }); + + describe('getTotalsByDateRange', () => { + it('should calculate totals for date range', async () => { + const startDate = new Date('2024-01-01'); + const endDate = new Date('2024-12-31'); + mockQueryBuilder.getRawOne.mockResolvedValue({ + total: '1500.50', + count: '15', + }); + + const result = await repository.getTotalsByDateRange(startDate, endDate); + + expect(mockTypeOrmRepo.createQueryBuilder).toHaveBeenCalledWith( + 'donation', + ); + expect(mockQueryBuilder.select).toHaveBeenCalledWith( + 'SUM(donation.amount)', + 'total', + ); + expect(mockQueryBuilder.addSelect).toHaveBeenCalledWith( + 'COUNT(donation.id)', + 'count', + ); + expect(mockQueryBuilder.where).toHaveBeenCalledWith( + 'donation.createdAt >= :startDate', + { startDate }, + ); + expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( + 'donation.createdAt <= :endDate', + { endDate }, + ); + expect(result).toEqual({ + total: 1500.5, + count: 15, + }); + }); + + it('should return zeros when no donations in range', async () => { + mockQueryBuilder.getRawOne.mockResolvedValue({ + total: null, + count: '0', + }); + + const result = await repository.getTotalsByDateRange( + new Date('2024-01-01'), + new Date('2024-01-02'), + ); + + expect(result).toEqual({ + total: 0, + count: 0, + }); + }); + + it('should handle string numbers from database', async () => { + mockQueryBuilder.getRawOne.mockResolvedValue({ + total: '2500.75', + count: '42', + }); + + const result = await repository.getTotalsByDateRange( + new Date('2024-01-01'), + new Date('2024-12-31'), + ); + + expect(result.total).toBe(2500.75); + expect(result.count).toBe(42); + }); + }); + + describe('findRecentPublic', () => { + it('should return recent public donations with privacy applied', async () => { + const mockDonations = [ + mockDonation, + { ...mockDonation, id: 2, isAnonymous: true }, + ]; + mockQueryBuilder.getMany.mockResolvedValue(mockDonations); + + const result = await repository.findRecentPublic(10); + + expect(mockTypeOrmRepo.createQueryBuilder).toHaveBeenCalledWith( + 'donation', + ); + expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith( + 'donation.createdAt', + 'DESC', + ); + expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10); + expect(result).toHaveLength(2); + + // Check that public DTO format is returned + expect(result[0]).toHaveProperty('id'); + expect(result[0]).toHaveProperty('amount'); + expect(result[0]).toHaveProperty('donorName'); // Should include for non-anonymous + expect(result[0]).not.toHaveProperty('email'); // Should not include sensitive data + expect(result[0]).not.toHaveProperty('firstName'); // Should not include sensitive data + + // Check anonymous donor doesn't have name + expect(result[1]).not.toHaveProperty('donorName'); + }); + + it('should respect limit parameter', async () => { + mockQueryBuilder.getMany.mockResolvedValue([]); + + await repository.findRecentPublic(5); + + expect(mockQueryBuilder.limit).toHaveBeenCalledWith(5); + }); + + it('should return empty array when no donations exist', async () => { + mockQueryBuilder.getMany.mockResolvedValue([]); + + const result = await repository.findRecentPublic(10); + + expect(result).toEqual([]); + }); + }); + + describe('deleteById', () => { + it('should delete donation by id', async () => { + mockTypeOrmRepo.delete.mockResolvedValue({ + affected: 1, + raw: {}, + } as never); + + await repository.deleteById(1); + + expect(mockTypeOrmRepo.delete).toHaveBeenCalledWith(1); + }); + + it('should throw error when donation not found', async () => { + mockTypeOrmRepo.delete.mockResolvedValue({ + affected: 0, + raw: {}, + } as never); + + await expect(repository.deleteById(999)).rejects.toThrow( + 'Donation with ID 999 not found', + ); + }); + }); +}); diff --git a/apps/backend/src/donations/donations.repository.ts b/apps/backend/src/donations/donations.repository.ts new file mode 100644 index 0000000..54b1fdc --- /dev/null +++ b/apps/backend/src/donations/donations.repository.ts @@ -0,0 +1,230 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, Brackets } from 'typeorm'; +import { + Donation, + DonationType, + RecurringInterval, + DonationStatus, +} from './donation.entity'; +import { DonationMappers, Donation as DomainDonation } from './mappers'; +import { PublicDonationDto } from './dtos/public-donation-dto'; + +export interface PaginationFilters { + donationType?: DonationType; + isAnonymous?: boolean; + recurringInterval?: RecurringInterval; + minAmount?: number; + maxAmount?: number; + startDate?: Date; + endDate?: Date; + status?: DonationStatus; +} + +export interface PaginatedResult { + rows: T[]; + total: number; + page: number; + perPage: number; + totalPages: number; +} + +@Injectable() +export class DonationsRepository { + constructor( + @InjectRepository(Donation) + private readonly repository: Repository, + ) {} + + /** + * Find donations with pagination and optional filters + */ + async findPaginated( + page: number, + perPage: number, + filters?: PaginationFilters, + ): Promise> { + const queryBuilder = this.repository.createQueryBuilder('donation'); + + // Apply filters + if (filters) { + if (filters.donationType) { + queryBuilder.andWhere('donation.donationType = :donationType', { + donationType: filters.donationType, + }); + } + + if (filters.isAnonymous !== undefined) { + queryBuilder.andWhere('donation.isAnonymous = :isAnonymous', { + isAnonymous: filters.isAnonymous, + }); + } + + if (filters.recurringInterval) { + queryBuilder.andWhere( + 'donation.recurringInterval = :recurringInterval', + { + recurringInterval: filters.recurringInterval, + }, + ); + } + + if (filters.minAmount !== undefined) { + queryBuilder.andWhere('donation.amount >= :minAmount', { + minAmount: filters.minAmount, + }); + } + + if (filters.maxAmount !== undefined) { + queryBuilder.andWhere('donation.amount <= :maxAmount', { + maxAmount: filters.maxAmount, + }); + } + + if (filters.startDate) { + queryBuilder.andWhere('donation.createdAt >= :startDate', { + startDate: filters.startDate, + }); + } + + if (filters.endDate) { + queryBuilder.andWhere('donation.createdAt <= :endDate', { + endDate: filters.endDate, + }); + } + + if (filters.status) { + queryBuilder.andWhere('donation.status = :status', { + status: filters.status, + }); + } + } + + // Order by most recent first + queryBuilder.orderBy('donation.createdAt', 'DESC'); + + // Apply pagination + const offset = (page - 1) * perPage; + queryBuilder.skip(offset).take(perPage); + + // Execute query + const [rows, total] = await queryBuilder.getManyAndCount(); + + return { + rows, + total, + page, + perPage, + totalPages: Math.ceil(total / perPage), + }; + } + + /** + * Search donations by donor name or email + * Useful for admin search functionality + */ + async searchByDonorNameOrEmail( + query: string, + limit: number = 50, + ): Promise { + const searchTerm = `%${query.toLowerCase()}%`; + + return this.repository + .createQueryBuilder('donation') + .where( + new Brackets((qb) => { + qb.where('LOWER(donation.firstName) LIKE :searchTerm', { searchTerm }) + .orWhere('LOWER(donation.lastName) LIKE :searchTerm', { + searchTerm, + }) + .orWhere('LOWER(donation.email) LIKE :searchTerm', { searchTerm }) + .orWhere( + "LOWER(CONCAT(donation.firstName, ' ', donation.lastName)) LIKE :searchTerm", + { searchTerm }, + ); + }), + ) + .orderBy('donation.createdAt', 'DESC') + .limit(limit) + .getMany(); + } + + /** + * Get aggregated totals for a date range + * Useful for admin dashboards and reporting + */ + async getTotalsByDateRange( + startDate: Date, + endDate: Date, + ): Promise<{ total: number; count: number }> { + const result = await this.repository + .createQueryBuilder('donation') + .select('SUM(donation.amount)', 'total') + .addSelect('COUNT(donation.id)', 'count') + .where('donation.createdAt >= :startDate', { startDate }) + .andWhere('donation.createdAt <= :endDate', { endDate }) + .getRawOne(); + + return { + total: parseFloat(result.total) || 0, + count: parseInt(result.count, 10) || 0, + }; + } + + /** + * Find recent public donations for display on public pages + * Respects privacy settings (anonymous, dedication visibility) + */ + async findRecentPublic(limit: number): Promise { + const donations = await this.repository + .createQueryBuilder('donation') + .orderBy('donation.createdAt', 'DESC') + .limit(limit) + .getMany(); + + // Map to public DTOs using the mapper's privacy logic + return DonationMappers.toPublicDonationDtos( + donations.map((d) => this.mapEntityToDomain(d)), + ); + } + + /** + * Delete a donation by ID (admin-only destructive operation) + */ + async deleteById(id: number): Promise { + const result = await this.repository.delete(id); + + if (result.affected === 0) { + throw new Error(`Donation with ID ${id} not found`); + } + } + + /** + * Map entity to domain model + * This bridges the gap between the entity and the domain model used in mappers + */ + private mapEntityToDomain(entity: Donation): DomainDonation { + return { + id: entity.id, + firstName: entity.firstName, + lastName: entity.lastName, + email: entity.email, + amount: entity.amount, + isAnonymous: entity.isAnonymous, + donationType: entity.donationType as 'one_time' | 'recurring', + recurringInterval: entity.recurringInterval as + | 'weekly' + | 'monthly' + | 'bimonthly' + | 'quarterly' + | 'annually' + | undefined, + dedicationMessage: entity.dedicationMessage ?? undefined, + showDedicationPublicly: entity.showDedicationPublicly, + status: entity.status as 'pending' | 'succeeded' | 'failed' | 'cancelled', + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + transactionId: entity.transactionId ?? undefined, + }; + } +} diff --git a/apps/backend/src/donations/dtos/create-donation-dto.ts b/apps/backend/src/donations/dtos/create-donation-dto.ts index 4a1e935..c906699 100644 --- a/apps/backend/src/donations/dtos/create-donation-dto.ts +++ b/apps/backend/src/donations/dtos/create-donation-dto.ts @@ -9,19 +9,7 @@ import { Min, IsNotEmpty, } from 'class-validator'; - -export enum DonationType { - ONE_TIME = 'one_time', - RECURRING = 'recurring', -} - -export enum RecurringInterval { - WEEKLY = 'weekly', - MONTHLY = 'monthly', - BIMONTHLY = 'bimonthly', - QUARTERLY = 'quarterly', - ANNUALLY = 'annually', -} +import { DonationType, RecurringInterval } from '../donation.entity'; export class CreateDonationDto { @ApiProperty({ diff --git a/apps/backend/src/donations/dtos/donation-response-dto.ts b/apps/backend/src/donations/dtos/donation-response-dto.ts index 16e43c8..1f271da 100644 --- a/apps/backend/src/donations/dtos/donation-response-dto.ts +++ b/apps/backend/src/donations/dtos/donation-response-dto.ts @@ -1,12 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { DonationType, RecurringInterval } from './create-donation-dto'; - -export enum DonationStatus { - PENDING = 'pending', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} +import { + DonationType, + RecurringInterval, + DonationStatus, +} from '../donation.entity'; export class DonationResponseDto { @ApiProperty({ @@ -76,7 +73,7 @@ export class DonationResponseDto { @ApiProperty({ description: 'the current donation status', enum: DonationStatus, - example: DonationStatus.COMPLETED, + example: DonationStatus.SUCCEEDED, }) status: DonationStatus; diff --git a/apps/backend/src/donations/dtos/public-donation-dto.ts b/apps/backend/src/donations/dtos/public-donation-dto.ts index 308c817..f705809 100644 --- a/apps/backend/src/donations/dtos/public-donation-dto.ts +++ b/apps/backend/src/donations/dtos/public-donation-dto.ts @@ -1,6 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { DonationType, RecurringInterval } from './create-donation-dto'; -import { DonationStatus } from './donation-response-dto'; +import { + DonationType, + RecurringInterval, + DonationStatus, +} from '../donation.entity'; export class PublicDonationDto { @ApiProperty({ @@ -54,7 +57,7 @@ export class PublicDonationDto { @ApiProperty({ description: 'the current donation status', enum: DonationStatus, - example: DonationStatus.COMPLETED, + example: DonationStatus.SUCCEEDED, }) status: DonationStatus; diff --git a/apps/backend/src/donations/mappers.spec.ts b/apps/backend/src/donations/mappers.spec.ts index 7e34743..5e52736 100644 --- a/apps/backend/src/donations/mappers.spec.ts +++ b/apps/backend/src/donations/mappers.spec.ts @@ -1,10 +1,10 @@ import { validate } from 'class-validator'; +import { CreateDonationDto } from './dtos/create-donation-dto'; import { - CreateDonationDto, DonationType, RecurringInterval, -} from './dtos/create-donation-dto'; -import { DonationStatus } from './dtos/donation-response-dto'; + DonationStatus, +} from './donation.entity'; import { PublicDonationDto } from './dtos/public-donation-dto'; import { DonationMappers, Donation } from './mappers'; @@ -19,7 +19,7 @@ describe('DonationMappers', () => { donationType: 'one_time', dedicationMessage: 'for the Fenway community', showDedicationPublicly: true, - status: 'completed', + status: 'succeeded', createdAt: new Date('2024-01-15T10:30:00Z'), updatedAt: new Date('2024-01-15T10:35:00Z'), transactionId: 'txn_1234567890', @@ -44,7 +44,7 @@ describe('DonationMappers', () => { donationType: DonationType.ONE_TIME, recurringInterval: undefined, dedicationMessage: 'for the Fenway community', - status: DonationStatus.COMPLETED, + status: DonationStatus.SUCCEEDED, createdAt: new Date('2024-01-15T10:30:00Z'), }; diff --git a/apps/backend/src/donations/mappers.ts b/apps/backend/src/donations/mappers.ts index 2583d1e..7c93b5c 100644 --- a/apps/backend/src/donations/mappers.ts +++ b/apps/backend/src/donations/mappers.ts @@ -1,13 +1,11 @@ +import { CreateDonationDto } from './dtos/create-donation-dto'; +import { DonationResponseDto } from './dtos/donation-response-dto'; +import { PublicDonationDto } from './dtos/public-donation-dto'; import { - CreateDonationDto, DonationType, RecurringInterval, -} from './dtos/create-donation-dto'; -import { - DonationResponseDto, DonationStatus, -} from './dtos/donation-response-dto'; -import { PublicDonationDto } from './dtos/public-donation-dto'; +} from './donation.entity'; export interface CreateDonationRequest { firstName: string; @@ -42,7 +40,7 @@ export interface Donation { | 'annually'; dedicationMessage?: string; showDedicationPublicly: boolean; - status: 'pending' | 'completed' | 'failed' | 'cancelled'; + status: 'pending' | 'succeeded' | 'failed' | 'cancelled'; createdAt: Date; updatedAt: Date; transactionId?: string; diff --git a/apps/backend/src/migrations/1759151447065-add_donations.ts b/apps/backend/src/migrations/1759151447065-add_donations.ts index 448a11f..838e762 100644 --- a/apps/backend/src/migrations/1759151447065-add_donations.ts +++ b/apps/backend/src/migrations/1759151447065-add_donations.ts @@ -5,11 +5,27 @@ export class AddDonations1759151447065 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE TABLE "donations" ("id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, "email" character varying NOT NULL, "amount" numeric(10,2) NOT NULL, "isAnonymous" boolean NOT NULL DEFAULT false, "donationType" integer NOT NULL, "recurringInterval" integer, "dedicationMessage" character varying, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT "PK_c01355d6f6f50fc6d1b4a946abf" PRIMARY KEY ("id"))`, + `CREATE TABLE "donation" ( + "id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, + "firstName" character varying NOT NULL, + "lastName" character varying NOT NULL, + "email" character varying NOT NULL, + "amount" numeric(10,2) NOT NULL, + "isAnonymous" boolean NOT NULL DEFAULT false, + "donationType" character varying NOT NULL, + "recurringInterval" character varying, + "dedicationMessage" character varying, + "showDedicationPublicly" boolean NOT NULL DEFAULT false, + "status" character varying NOT NULL DEFAULT 'pending', + "transactionId" character varying, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_c01355d6f6f50fc6d1b4a946abf" PRIMARY KEY ("id") + )`, ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "donations"`); + await queryRunner.query(`DROP TABLE "donation"`); } } diff --git a/package.json b/package.json index 1e90c44..91112ed 100644 --- a/package.json +++ b/package.json @@ -76,8 +76,8 @@ "@types/node": "^18.14.2", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^5.60.1", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", "@vitejs/plugin-react": "^4.0.0", "@vitest/ui": "^0.32.0", "cypress": "^13.0.0", diff --git a/yarn.lock b/yarn.lock index 87fbb76..d4b8cf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1936,7 +1936,12 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.10.0": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint-community/regexpp@^4.6.1": version "4.10.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== @@ -3811,7 +3816,7 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.14" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.14.tgz#74a97a5573980802f32c8e47b663530ab3b6b7d1" integrity sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw== @@ -3914,7 +3919,7 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.5.tgz#4751153abbf8d6199babb345a52e1eb4167d64af" integrity sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw== -"@types/semver@^7.3.12", "@types/semver@^7.5.0": +"@types/semver@^7.3.12": version "7.5.4" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.4.tgz#0a41252ad431c473158b22f9bfb9a63df7541cff" integrity sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ== @@ -4008,31 +4013,30 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^6.7.0": - version "6.9.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz#d8ce497dc0ed42066e195c8ecc40d45c7b1254f4" - integrity sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg== +"@typescript-eslint/eslint-plugin@^7.0.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz#b16d3cf3ee76bf572fdf511e79c248bdec619ea3" + integrity sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw== dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.9.1" - "@typescript-eslint/type-utils" "6.9.1" - "@typescript-eslint/utils" "6.9.1" - "@typescript-eslint/visitor-keys" "6.9.1" - debug "^4.3.4" + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "7.18.0" + "@typescript-eslint/type-utils" "7.18.0" + "@typescript-eslint/utils" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" graphemer "^1.4.0" - ignore "^5.2.4" + ignore "^5.3.1" natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" + ts-api-utils "^1.3.0" -"@typescript-eslint/parser@^5.60.1": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" - integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== +"@typescript-eslint/parser@^7.0.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.18.0.tgz#83928d0f1b7f4afa974098c64b5ce6f9051f96a0" + integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg== dependencies: - "@typescript-eslint/scope-manager" "5.62.0" - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/scope-manager" "7.18.0" + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/typescript-estree" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -4043,23 +4047,23 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.9.1": - version "6.9.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz#e96afeb9a68ad1cd816dba233351f61e13956b75" - integrity sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg== +"@typescript-eslint/scope-manager@7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz#c928e7a9fc2c0b3ed92ab3112c614d6bd9951c83" + integrity sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA== dependencies: - "@typescript-eslint/types" "6.9.1" - "@typescript-eslint/visitor-keys" "6.9.1" + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" -"@typescript-eslint/type-utils@6.9.1": - version "6.9.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz#efd5db20ed35a74d3c7d8fba51b830ecba09ce32" - integrity sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg== +"@typescript-eslint/type-utils@7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz#2165ffaee00b1fbbdd2d40aa85232dab6998f53b" + integrity sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA== dependencies: - "@typescript-eslint/typescript-estree" "6.9.1" - "@typescript-eslint/utils" "6.9.1" + "@typescript-eslint/typescript-estree" "7.18.0" + "@typescript-eslint/utils" "7.18.0" debug "^4.3.4" - ts-api-utils "^1.0.1" + ts-api-utils "^1.3.0" "@typescript-eslint/type-utils@^5.60.1": version "5.62.0" @@ -4076,10 +4080,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.9.1": - version "6.9.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.9.1.tgz#a6cfc20db0fcedcb2f397ea728ef583e0ee72459" - integrity sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ== +"@typescript-eslint/types@7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9" + integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -4094,18 +4098,19 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.9.1": - version "6.9.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz#8c77910a49a04f0607ba94d78772da07dab275ad" - integrity sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw== +"@typescript-eslint/typescript-estree@7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz#b5868d486c51ce8f312309ba79bdb9f331b37931" + integrity sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA== dependencies: - "@typescript-eslint/types" "6.9.1" - "@typescript-eslint/visitor-keys" "6.9.1" + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/visitor-keys" "7.18.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" "@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.58.0", "@typescript-eslint/utils@^5.60.1": version "5.62.0" @@ -4121,18 +4126,15 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@6.9.1": - version "6.9.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.9.1.tgz#763da41281ef0d16974517b5f0d02d85897a1c1e" - integrity sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA== +"@typescript-eslint/utils@7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.18.0.tgz#bca01cde77f95fc6a8d5b0dbcbfb3d6ca4be451f" + integrity sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.9.1" - "@typescript-eslint/types" "6.9.1" - "@typescript-eslint/typescript-estree" "6.9.1" - semver "^7.5.4" + "@typescript-eslint/scope-manager" "7.18.0" + "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/typescript-estree" "7.18.0" "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" @@ -4142,13 +4144,13 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.9.1": - version "6.9.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz#6753a9225a0ba00459b15d6456b9c2780b66707d" - integrity sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw== +"@typescript-eslint/visitor-keys@7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz#0564629b6124d67607378d0f0332a0495b25e7d7" + integrity sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg== dependencies: - "@typescript-eslint/types" "6.9.1" - eslint-visitor-keys "^3.4.1" + "@typescript-eslint/types" "7.18.0" + eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.2.0": version "1.2.0" @@ -7912,11 +7914,16 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== -ignore@^5.0.4, ignore@^5.1.9, ignore@^5.2.0, ignore@^5.2.4: +ignore@^5.0.4, ignore@^5.1.9, ignore@^5.2.0: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" @@ -11718,6 +11725,11 @@ semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semve dependencies: lru-cache "^6.0.0" +semver@^7.6.0: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + semver@^7.7.2: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" @@ -12487,10 +12499,10 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -ts-api-utils@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" - integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== +ts-api-utils@^1.3.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== ts-jest@^29.1.0: version "29.1.1"