Repository: Code-4-Community/green-infrastructure
Pull request #85: Automatically create new user if first time application and no userId
Author: login: denniwang
Created: 2025-03-26T14:37:57Z | Merged: 2025-04-10T16:00:49Z

Added functionality to create a new user if no userId provided and we are submitting an application. We are requiring more information to create a new request though (first name, last name, email, phone, zip, DOB, isFirstApplication, site id, names, date applied)
Screenshot of main trying to create a new application with no userId. Red arrow pointing to the line causing the error complaining about a missing userId.
![image](https://github.com/user-attachments/assets/6deac7b3-06bf-4c73-ac2b-db2cfef620ab)
After changes (WITH SAME POSTMAN REQUEST) we have a userid in the application request, and sucesfully sent confirmation email.
![image](https://github.com/user-attachments/assets/cfd737b5-d4f8-47da-9c26-5f77114fc941)



Diff:
diff --git a/apps/backend/src/applications/applications.controller.ts b/apps/backend/src/applications/applications.controller.ts
index e96d9dbf7..b160eb0bb 100644
--- a/apps/backend/src/applications/applications.controller.ts
+++ b/apps/backend/src/applications/applications.controller.ts
@@ -1,10 +1,18 @@
-import { Controller, Get, Put, Post, Body, Param, Query } from '@nestjs/common';
+import {
+  Controller,
+  Get,
+  Put,
+  Post,
+  Body,
+  Param,
+  Query,
+  BadRequestException,
+} from '@nestjs/common';
 import { ApplicationsService } from './applications.service';
 import { ApplicationsModel } from './applications.model';
 import { ApplicationStatus } from './applications.model';
 import { NewApplicationInput } from '../dtos/newApplicationsDTO';
 
-
 @Controller('applications')
 export class ApplicationsController {
   constructor(private applicationsService: ApplicationsService) {}
@@ -29,7 +37,7 @@ export class ApplicationsController {
   @Put('editApplication/:appId')
   public async changeApplicationStatus(
     @Param('appId') appId: number,
-    @Query('applicationStatus') appStatus: ApplicationStatus
+    @Query('applicationStatus') appStatus: ApplicationStatus,
   ): Promise<ApplicationsModel> {
     // Check if the provided appStatus is a valid enum value
     if (!Object.values(ApplicationStatus).includes(appStatus)) {
@@ -39,9 +47,12 @@ export class ApplicationsController {
     return this.applicationsService.updateApplicationStatus(appId, appStatus);
   }
 
-   @Post()
+  @Post()
   public async postApplication(@Body() applicationData: NewApplicationInput) {
+    try {
       return this.applicationsService.postApplication(applicationData);
+    } catch (error) {
+      throw new BadRequestException(error.message);
+    }
   }
-
 }
diff --git a/apps/backend/src/applications/applications.model.ts b/apps/backend/src/applications/applications.model.ts
index 7341ac659..a76c10416 100644
--- a/apps/backend/src/applications/applications.model.ts
+++ b/apps/backend/src/applications/applications.model.ts
@@ -20,8 +20,8 @@ export type ApplicationInputModel = {
   siteId: { N: string },
   names: { SS: string[] },  // For an array of strings, use SS (String Set) in DynamoDB
   status: { S: string },
-  dateApplied: { S: string },  // Date should be formatted as a string, typically ISO string
-  isFirstApplication: { S: string },
+  dateApplied: { S: string },  // Date should be formatted as a string
+  isFirstApplication: { BOOL: boolean }, // Change to BOOL type to match actual usage
 };
 
 export enum ApplicationStatus {
diff --git a/apps/backend/src/applications/applications.service.ts b/apps/backend/src/applications/applications.service.ts
index e14d7c116..639f36c62 100644
--- a/apps/backend/src/applications/applications.service.ts
+++ b/apps/backend/src/applications/applications.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, BadRequestException } from '@nestjs/common';
 import { ApplicationInputModel, ApplicationsModel } from './applications.model';
 import { DynamoDbService } from '../dynamodb';
 import { ApplicationStatus } from './applications.model';
@@ -6,6 +6,7 @@ import { NewApplicationInput } from '../dtos/newApplicationsDTO';
 import { LambdaService } from '../lambda';
 import { UserService } from '../user/user.service';
 import { SiteService } from '../site/site.service';
+import { Role } from '../user/user.model';
 
 @Injectable()
 export class ApplicationsService {
@@ -104,23 +105,85 @@ export class ApplicationsService {
   }
 
   public async postApplication(applicationData: NewApplicationInput) {
+    let userId = applicationData.userId;
+
+    // Handle first-time applications - create new user
+    if (applicationData.isFirstApplication === true) {
+      if (!userId) {
+        // Validate required fields for user creation
+        if (
+          !applicationData.firstName ||
+          !applicationData.lastName ||
+          !applicationData.email ||
+          !applicationData.phoneNumber ||
+          !applicationData.zipCode ||
+          !applicationData.birthDate
+        ) {
+          throw new BadRequestException(
+            'First name, last name, email, phone number, zip code, and birth date are required for first-time applications',
+          );
+        }
+
+        // Create a new user using postUser method
+        try {
+          const newUserResult = await this.userService.postUser(
+            {
+              firstName: applicationData.firstName,
+              lastName: applicationData.lastName,
+              email: applicationData.email,
+              phoneNumber: applicationData.phoneNumber,
+              zipCode: applicationData.zipCode,
+              birthDate: applicationData.birthDate,
+            },
+            Role.VOLUNTEER,
+          );
+
+          // Extract the new user ID from the result - fix the way we access newUserID
+          userId = parseInt(newUserResult.newUserID);
+          console.log('Created new user with ID:', userId);
+        } catch (error) {
+          throw new BadRequestException(
+            `Failed to create new user: ${error.message}`,
+          );
+        }
+      } else {
+        throw new BadRequestException(
+          'Not expecting userId for first-time applications.',
+        );
+      }
+    } else if (!userId) {
+      // For non-first-time applications, userId is required
+      throw new BadRequestException(
+        'User ID is required for non-first-time applications',
+      );
+    }
+
+    // Ensure we have names array to avoid undefined issues
+    const names = applicationData.names || [];
+
     const newId =
       (await this.dynamoDbService.getHighestAppId(this.tableName)) + 1;
-    const applicationModel = this.PostInputToApplicationModel(
-      applicationData,
-      newId.toString(),
-    );
-    console.log('Received application data:', applicationData);
+
     try {
+      // Create application with proper types
+      const applicationModel = {
+        appId: { N: newId.toString() },
+        userId: { N: userId.toString() },
+        siteId: { N: applicationData.siteId.toString() },
+        names: { SS: names.length > 0 ? names : [''] }, // Ensure non-empty array with at least one element
+        status: { S: applicationData.status || ApplicationStatus.PENDING },
+        dateApplied: {
+          S: applicationData.dateApplied || new Date().toISOString(),
+        },
+        isFirstApplication: { BOOL: applicationData.isFirstApplication }, // Use BOOL type directly
+      };
+
       const result = await this.dynamoDbService.postItem(
         this.tableName,
         applicationModel,
       );
 
-      if (result.$metadata.httpStatusCode !== 200) {
-        throw new Error('Error posting application');
-      }
-      const user = await this.userService.getUser(applicationData.userId);
+      const user = await this.userService.getUser(userId);
       const site = await this.siteService.getSite(applicationData.siteId);
       const name = user.firstName;
       const email = user.email;
@@ -132,7 +195,7 @@ export class ApplicationsService {
         userEmail: email,
         siteName: siteName,
         timeFrame: timeFrame,
-        userId: applicationData.userId,
+        userId: userId,
       };
 
       const lambdaResult = await this.lambdaService.invokeLambda(
@@ -143,10 +206,12 @@ export class ApplicationsService {
 
       return { ...result, newApplicationId: newId.toString() };
     } catch (e) {
+      console.error('Error posting application:', e);
       throw new Error('Unable to post new application: ' + e);
     }
   }
 
+  // Fix PostInputToApplicationModel method to handle the BOOL type correctly
   private PostInputToApplicationModel = (
     input: NewApplicationInput,
     appId: string,
@@ -155,10 +220,10 @@ export class ApplicationsService {
       appId: { N: appId },
       userId: { N: input.userId.toString() },
       siteId: { N: input.siteId.toString() },
-      names: { SS: input.names },
+      names: { SS: input.names && input.names.length > 0 ? input.names : [''] }, // Ensure non-empty
       status: { S: input.status as ApplicationStatus },
       dateApplied: { S: input.dateApplied },
-      isFirstApplication: { S: input.isFirstApplication.toString() },
+      isFirstApplication: { BOOL: input.isFirstApplication }, // Change to BOOL type
     };
   };
 
diff --git a/apps/backend/src/dtos/newApplicationsDTO.ts b/apps/backend/src/dtos/newApplicationsDTO.ts
index 8f3ef15ef..066653e3e 100644
--- a/apps/backend/src/dtos/newApplicationsDTO.ts
+++ b/apps/backend/src/dtos/newApplicationsDTO.ts
@@ -1,5 +1,13 @@
 import { ApiProperty } from '@nestjs/swagger';
-import { IsNumber, IsArray, IsString, IsBoolean, IsEnum, IsOptional } from 'class-validator';
+import {
+  IsNumber,
+  IsArray,
+  IsString,
+  IsBoolean,
+  IsEnum,
+  IsOptional,
+  IsEmail,
+} from 'class-validator';
 
 export enum ApplicationStatus {
   APPROVED = 'Approved',
@@ -9,11 +17,75 @@ export enum ApplicationStatus {
 
 export class NewApplicationInput {
   @ApiProperty({
-    description: 'User ID of the applicant',
+    description:
+      'User ID of the applicant (optional for first-time applications)',
     example: 123,
+    required: false,
   })
   @IsNumber()
-  userId: number;
+  @IsOptional()
+  userId?: number;
+
+  @ApiProperty({
+    description:
+      'First name of the applicant (required for first-time applications)',
+    example: 'John',
+    required: false,
+  })
+  @IsString()
+  @IsOptional()
+  firstName?: string;
+
+  @ApiProperty({
+    description:
+      'Last name of the applicant (required for first-time applications)',
+    example: 'Doe',
+    required: false,
+  })
+  @IsString()
+  @IsOptional()
+  lastName?: string;
+
+  @ApiProperty({
+    description:
+      'Email of the applicant (required for first-time applications)',
+    example: 'john.doe@example.com',
+    required: false,
+  })
+  @IsString()
+  @IsEmail()
+  @IsOptional()
+  email?: string;
+
+  @ApiProperty({
+    description:
+      'Phone number of the applicant (required for first-time applications)',
+    example: '6175551234',
+    required: false,
+  })
+  @IsString()
+  @IsOptional()
+  phoneNumber?: string;
+
+  @ApiProperty({
+    description:
+      'ZIP code of the applicant (required for first-time applications)',
+    example: '02108',
+    required: false,
+  })
+  @IsString()
+  @IsOptional()
+  zipCode?: string;
+
+  @ApiProperty({
+    description:
+      'Birth date of the applicant (required for first-time applications)',
+    example: '1990-01-01',
+    required: false,
+  })
+  @IsString()
+  @IsOptional()
+  birthDate?: string;
 
   @ApiProperty({
     description: 'Site ID associated with the application',
@@ -23,7 +95,8 @@ export class NewApplicationInput {
   siteId: number;
 
   @ApiProperty({
-    description: 'Array of names associated with the application (defaults to an empty array if not provided)',
+    description:
+      'Array of names associated with the application (defaults to an empty array if not provided)',
     example: ['John Doe', 'Jane Doe'],
     required: false,
   })
@@ -43,7 +116,8 @@ export class NewApplicationInput {
   status: ApplicationStatus = ApplicationStatus.PENDING; // Default to "PENDING"
 
   @ApiProperty({
-    description: 'Date the application was submitted (defaults to an ISO string if not provided)',
+    description:
+      'Date the application was submitted (defaults to an ISO string if not provided)',
     example: '2025-02-06T10:00:00Z',
     required: false,
   })


Reviews:
user: login: hams7504
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-08T21:41:30Z
user: login: hams7504
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-08T21:48:08Z
user: login: hams7504
body: Thanks for the changes!
state: APPROVED
author_association: NONE
submitted_at: 2025-04-09T22:16:24Z
user: login: mahekagg
state: APPROVED
author_association: NONE
submitted_at: 2025-04-09T22:24:21Z

Review comments:
diff_hunk: @@ -104,23 +105,78 @@ export class ApplicationsService {
   }
 
   public async postApplication(applicationData: NewApplicationInput) {
-    const newId =
-      (await this.dynamoDbService.getHighestAppId(this.tableName)) + 1;
-    const applicationModel = this.PostInputToApplicationModel(
-      applicationData,
-      newId.toString(),
-    );
-    console.log('Received application data:', applicationData);
+    let userId = applicationData.userId;
+
+    // Handle first-time applications - create new user
+    if (applicationData.isFirstApplication === true) {
+      if (!userId) {
+        // Validate required fields for user creation
+        if (
+          !applicationData.firstName ||
+          !applicationData.lastName ||
+          !applicationData.email ||
+          !applicationData.phoneNumber ||
+          !applicationData.zipCode ||
+          !applicationData.birthDate
+        ) {
path: apps/backend/src/applications/applications.service.ts
user: login: hams7504
body: Nice job validating these fields!
created_at: 2025-04-08T21:41:30Z
author_association: NONE
diff_hunk: @@ -155,10 +213,10 @@ export class ApplicationsService {
       appId: { N: appId },
       userId: { N: input.userId.toString() },
       siteId: { N: input.siteId.toString() },
-      names: { SS: input.names },
+      names: { SS: input.names && input.names.length > 0 ? input.names : [""] }, // Ensure non-empty
path: apps/backend/src/applications/applications.service.ts
user: login: hams7504
body: Good change 👍
created_at: 2025-04-08T21:48:07Z
author_association: NONE

PR comments:
user: login: hams7504
created_at: 2025-04-08T21:59:09Z
body: Nice job with error handling!
author_association: NONE
user: login: hams7504
created_at: 2025-04-09T17:49:58Z
body: Works well!
- checked that the new user object was created if no user ID was passed in
- new application object created
- email confirming that the application was submitted was sent

One small nitpick: 
Could you add a BadRequestException case for when there is a userID provided but firstTimeApplication is true? Right now, I think a new user would be created for this case. This case should not be possible which is why it's a nitpick.
author_association: NONE