Skip to content

Commit 38fd3aa

Browse files
Merge pull request #11 from ChangePlusPlusVandy/setup/dev-setup
Setup/dev setup
2 parents 3c36800 + 3b32703 commit 38fd3aa

File tree

3 files changed

+177
-42
lines changed

3 files changed

+177
-42
lines changed

backend/__tests__/controllers/organizationController.test.ts

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { OrganizationRole, OrganizationStatus, PrismaClient } from '@prisma/client';
1+
import {
2+
OrganizationRole,
3+
OrganizationStatus,
4+
TennesseeRegion,
5+
OrganizationSize,
6+
PrismaClient,
7+
} from '@prisma/client';
28
import { mockDeep, mockReset } from 'jest-mock-extended';
39
import admin from 'firebase-admin';
410

@@ -18,6 +24,17 @@ jest.mock('@prisma/client', () => ({
1824
PENDING: 'PENDING',
1925
SUSPENDED: 'SUSPENDED',
2026
},
27+
TennesseeRegion: {
28+
EAST: 'EAST',
29+
MIDDLE: 'MIDDLE',
30+
WEST: 'WEST',
31+
},
32+
OrganizationSize: {
33+
SMALL: 'SMALL',
34+
MEDIUM: 'MEDIUM',
35+
LARGE: 'LARGE',
36+
EXTRA_LARGE: 'EXTRA_LARGE',
37+
},
2138
}));
2239

2340
jest.mock('firebase-admin', () => ({
@@ -67,12 +84,20 @@ const mockOrganization = {
6784
city: 'Nashville',
6885
state: 'TN',
6986
zipCode: '37201',
70-
phoneNumber: '615-555-0123',
71-
contactPerson: 'Jane Smith',
72-
contactTitle: 'Executive Director',
87+
primaryContactName: 'Jane Smith',
88+
primaryContactEmail: 'jane.smith@nonprofitorg.org',
89+
primaryContactPhone: '615-555-0123',
90+
secondaryContactName: 'John Doe',
91+
secondaryContactEmail: 'john.doe@nonprofitorg.org',
92+
region: 'MIDDLE' as TennesseeRegion,
93+
organizationType: 'Senior Services',
94+
membershipActive: true,
95+
membershipDate: new Date('2024-01-01'),
96+
membershipRenewalDate: new Date('2025-01-01'),
97+
organizationSize: 'MEDIUM' as OrganizationSize,
7398
role: 'MEMBER' as OrganizationRole,
7499
status: 'ACTIVE' as OrganizationStatus,
75-
tags: ['Nashville', 'Senior Services', 'Healthcare'],
100+
tags: ['Senior Services', 'Healthcare', 'Mental Health'],
76101
};
77102

78103
const mockAdminOrg = {
@@ -119,11 +144,18 @@ describe('OrganizationController', () => {
119144
email: 'neworg@nonprofit.org',
120145
password: 'securePassword123',
121146
name: 'New Community Services',
122-
contactPerson: 'John Smith',
123-
contactTitle: 'Executive Director',
147+
primaryContactName: 'John Smith',
148+
primaryContactEmail: 'john@neworg.org',
149+
primaryContactPhone: '901-555-0100',
150+
secondaryContactName: 'Sarah Johnson',
151+
secondaryContactEmail: 'sarah@neworg.org',
124152
city: 'Memphis',
125153
state: 'TN',
126-
tags: ['Community', 'Services'],
154+
region: 'WEST',
155+
organizationType: 'Community Services',
156+
membershipActive: true,
157+
organizationSize: 'SMALL',
158+
tags: ['Community Services', 'Education'],
127159
},
128160
});
129161
const res = createMockResponse();
@@ -132,7 +164,7 @@ describe('OrganizationController', () => {
132164
...mockOrganization,
133165
email: 'neworg@nonprofit.org',
134166
name: 'New Community Services',
135-
contactPerson: 'John Smith',
167+
primaryContactName: 'John Smith',
136168
status: 'PENDING',
137169
});
138170
await registerOrganization(req, res);
@@ -151,16 +183,24 @@ describe('OrganizationController', () => {
151183
data: {
152184
email: 'neworg@nonprofit.org',
153185
name: 'New Community Services',
154-
contactPerson: 'John Smith',
155-
contactTitle: 'Executive Director',
186+
primaryContactName: 'John Smith',
187+
primaryContactEmail: 'john@neworg.org',
188+
primaryContactPhone: '901-555-0100',
189+
secondaryContactName: 'Sarah Johnson',
190+
secondaryContactEmail: 'sarah@neworg.org',
156191
description: undefined,
157192
website: undefined,
158193
address: undefined,
159194
city: 'Memphis',
160195
state: 'TN',
161196
zipCode: undefined,
162-
phoneNumber: undefined,
163-
tags: ['Community', 'Services'],
197+
region: 'WEST',
198+
organizationType: 'Community Services',
199+
membershipActive: true,
200+
membershipDate: null,
201+
membershipRenewalDate: null,
202+
organizationSize: 'SMALL',
203+
tags: ['Community Services', 'Education'],
164204
firebaseUid: 'firebase-uid-123',
165205
role: 'MEMBER',
166206
status: 'PENDING',
@@ -185,7 +225,8 @@ describe('OrganizationController', () => {
185225
await registerOrganization(req, res);
186226
expect(res.status).toHaveBeenCalledWith(400);
187227
expect(res.json).toHaveBeenCalledWith({
188-
error: 'Email, password, name, and contact person are required',
228+
error:
229+
'Email, password, name, and primary contact information (name, email, phone) are required',
189230
});
190231
});
191232
it('should prevent duplicate registration - POST /api/organizations/register', async () => {
@@ -194,7 +235,9 @@ describe('OrganizationController', () => {
194235
email: 'existing@nonprofit.org',
195236
password: 'password123',
196237
name: 'Existing Organization',
197-
contactPerson: 'Jane Doe',
238+
primaryContactName: 'Jane Doe',
239+
primaryContactEmail: 'jane@existing.org',
240+
primaryContactPhone: '615-555-9999',
198241
},
199242
});
200243
const res = createMockResponse();
@@ -285,15 +328,19 @@ describe('OrganizationController', () => {
285328
body: {
286329
name: 'Updated Senior Services',
287330
description: 'Updated description',
288-
contactPerson: 'John Doe',
289-
contactTitle: 'New Director',
331+
primaryContactName: 'John Doe',
332+
primaryContactEmail: 'john.doe@updated.org',
333+
primaryContactPhone: '615-555-9999',
334+
region: 'EAST',
335+
organizationType: 'Healthcare',
336+
membershipActive: true,
290337
},
291338
});
292339
const res = createMockResponse();
293340
const updatedOrg = {
294341
...mockOrganization,
295342
name: 'Updated Senior Services',
296-
contactPerson: 'John Doe',
343+
primaryContactName: 'John Doe',
297344
};
298345
prismaMock.organization.update.mockResolvedValue(updatedOrg);
299346
await updateOrganization(req, res);
@@ -302,8 +349,12 @@ describe('OrganizationController', () => {
302349
data: expect.objectContaining({
303350
name: 'Updated Senior Services',
304351
description: 'Updated description',
305-
contactPerson: 'John Doe',
306-
contactTitle: 'New Director',
352+
primaryContactName: 'John Doe',
353+
primaryContactEmail: 'john.doe@updated.org',
354+
primaryContactPhone: '615-555-9999',
355+
region: 'EAST',
356+
organizationType: 'Healthcare',
357+
membershipActive: true,
307358
}),
308359
});
309360
expect(res.json).toHaveBeenCalledWith(updatedOrg);

backend/controllers/organizationController.ts

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { Request, Response } from 'express';
2-
import { PrismaClient, OrganizationRole, OrganizationStatus } from '@prisma/client';
2+
import {
3+
PrismaClient,
4+
OrganizationRole,
5+
OrganizationStatus,
6+
TennesseeRegion,
7+
OrganizationSize,
8+
} from '@prisma/client';
39
import { AuthenticatedRequest } from '../types/index.js';
410
import admin from 'firebase-admin';
511

@@ -12,13 +18,15 @@ const prisma = new PrismaClient();
1218
*/
1319
export const getAllOrganizations = async (req: AuthenticatedRequest, res: Response) => {
1420
try {
15-
const { search, status, role, city, state, tags } = req.query;
21+
const { search, status, role, city, state, tags, region, membershipActive, organizationType } =
22+
req.query;
1623
const where: any = {};
1724
if (search) {
1825
where.OR = [
1926
{ name: { contains: search as string, mode: 'insensitive' } },
2027
{ email: { contains: search as string, mode: 'insensitive' } },
21-
{ contactPerson: { contains: search as string, mode: 'insensitive' } },
28+
{ primaryContactName: { contains: search as string, mode: 'insensitive' } },
29+
{ primaryContactEmail: { contains: search as string, mode: 'insensitive' } },
2230
];
2331
}
2432
if (status) {
@@ -33,6 +41,15 @@ export const getAllOrganizations = async (req: AuthenticatedRequest, res: Respon
3341
if (state) {
3442
where.state = { contains: state as string, mode: 'insensitive' };
3543
}
44+
if (region) {
45+
where.region = region as TennesseeRegion;
46+
}
47+
if (membershipActive !== undefined) {
48+
where.membershipActive = membershipActive === 'true';
49+
}
50+
if (organizationType) {
51+
where.organizationType = { contains: organizationType as string, mode: 'insensitive' };
52+
}
3653
if (tags) {
3754
const tagArray = (tags as string).split(',').map(tag => tag.trim());
3855
where.tags = {
@@ -63,22 +80,40 @@ export const registerOrganization = async (req: Request, res: Response) => {
6380
email,
6481
password,
6582
name,
66-
contactPerson,
67-
contactTitle,
83+
primaryContactName,
84+
primaryContactEmail,
85+
primaryContactPhone,
86+
secondaryContactName,
87+
secondaryContactEmail,
6888
description,
6989
website,
7090
address,
7191
city,
7292
state,
7393
zipCode,
74-
phoneNumber,
94+
region,
95+
organizationType,
96+
membershipActive,
97+
membershipDate,
98+
membershipRenewalDate,
99+
organizationSize,
75100
tags,
76101
} = req.body;
77102

78-
if (!email || !password || !name || !contactPerson) {
103+
if (
104+
!email ||
105+
!password ||
106+
!name ||
107+
!primaryContactName ||
108+
!primaryContactEmail ||
109+
!primaryContactPhone
110+
) {
79111
return res
80112
.status(400)
81-
.json({ error: 'Email, password, name, and contact person are required' });
113+
.json({
114+
error:
115+
'Email, password, name, and primary contact information (name, email, phone) are required',
116+
});
82117
}
83118
const existingOrg = await prisma.organization.findFirst({
84119
where: {
@@ -102,15 +137,23 @@ export const registerOrganization = async (req: Request, res: Response) => {
102137
data: {
103138
email,
104139
name,
105-
contactPerson,
106-
contactTitle,
140+
primaryContactName,
141+
primaryContactEmail,
142+
primaryContactPhone,
143+
secondaryContactName,
144+
secondaryContactEmail,
107145
description,
108146
website,
109147
address,
110148
city,
111149
state,
112150
zipCode,
113-
phoneNumber,
151+
region,
152+
organizationType,
153+
membershipActive: membershipActive || false,
154+
membershipDate: membershipDate ? new Date(membershipDate) : null,
155+
membershipRenewalDate: membershipRenewalDate ? new Date(membershipRenewalDate) : null,
156+
organizationSize,
114157
tags: organizationTags,
115158
firebaseUid: firebaseUser.uid,
116159
role: 'MEMBER',
@@ -180,9 +223,17 @@ export const updateOrganization = async (req: AuthenticatedRequest, res: Respons
180223
city,
181224
state,
182225
zipCode,
183-
phoneNumber,
184-
contactPerson,
185-
contactTitle,
226+
primaryContactName,
227+
primaryContactEmail,
228+
primaryContactPhone,
229+
secondaryContactName,
230+
secondaryContactEmail,
231+
region,
232+
organizationType,
233+
membershipActive,
234+
membershipDate,
235+
membershipRenewalDate,
236+
organizationSize,
186237
tags,
187238
role,
188239
status,
@@ -217,9 +268,17 @@ export const updateOrganization = async (req: AuthenticatedRequest, res: Respons
217268
city,
218269
state,
219270
zipCode,
220-
phoneNumber,
221-
contactPerson,
222-
contactTitle,
271+
primaryContactName,
272+
primaryContactEmail,
273+
primaryContactPhone,
274+
secondaryContactName,
275+
secondaryContactEmail,
276+
region,
277+
organizationType,
278+
membershipActive,
279+
membershipDate: membershipDate ? new Date(membershipDate) : undefined,
280+
membershipRenewalDate: membershipRenewalDate ? new Date(membershipRenewalDate) : undefined,
281+
organizationSize,
223282
tags,
224283
};
225284
if (isAdmin) {

backend/prisma/schema.prisma

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,49 @@ enum OrganizationStatus {
2626
SUSPENDED // Admin blocked access
2727
}
2828

29+
// Tennessee regions for member organizations
30+
enum TennesseeRegion {
31+
EAST
32+
MIDDLE
33+
WEST
34+
}
35+
36+
// Organization size categories for dues calculation
37+
enum OrganizationSize {
38+
SMALL
39+
MEDIUM
40+
LARGE
41+
EXTRA_LARGE
42+
}
43+
2944
// The 45+ nonprofit organizations that are part of the Tennessee Coalition for Better Aging
45+
// Can discuss splitting up this model into addresses table and org table for db normalization standards later
3046
model Organization {
3147
id String @id @default(cuid())
3248
firebaseUid String @unique // Links to Firebase Auth
3349
name String @unique
34-
email String @unique // Primary contact email
50+
email String @unique // Organization email
3551
description String?
3652
website String?
3753
address String?
3854
city String?
3955
state String?
4056
zipCode String?
41-
phoneNumber String?
42-
contactPerson String? // Primary contact name
43-
contactTitle String? // Title of primary contact
57+
primaryContactName String
58+
primaryContactEmail String
59+
primaryContactPhone String
60+
secondaryContactName String?
61+
secondaryContactEmail String?
62+
region TennesseeRegion? // East, Middle, or West Tennessee
63+
organizationType String? // Type of organization
64+
membershipActive Boolean @default(false)
65+
membershipDate DateTime? // Date when dues were paid/membership started
66+
membershipRenewalDate DateTime? // Date when membership needs to be renewed
67+
organizationSize OrganizationSize? // Size category for dues calculation
68+
4469
role OrganizationRole @default(MEMBER)
4570
status OrganizationStatus @default(PENDING)
46-
tags String[] // Location/focus tags like ["West Tennessee", "Healthcare", "Senior Services"]
71+
tags String[] // Focus area tags like ["Healthcare", "Senior Services", "Mental Health"]
4772
4873
@@map("organizations")
4974
}

0 commit comments

Comments
 (0)