Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add REST Endpoint for DAC Application Approval with Validation and State Management #148

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
106 changes: 53 additions & 53 deletions apps/api/src/api/application-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import { ApplicationState, ApplicationStates, ApproveApplication } from '@pcgl-daco/data-model/src/types.js';
import { ApplicationStates, ApproveApplication } from '@pcgl-daco/data-model/src/types.js';

import { getDbInstance } from '@/db/index.js';
import { ApplicationListRequest } from '@/routes/types.js';
import applicationService from '@/service/application-service.js';
import { type ApplicationContentUpdates, type ApplicationService } from '@/service/types.js';
import { failure } from '@/utils/results.js';
import { canTransitionTo } from '@/service/ApplicationStateMachine.js';
import { ApplicationStateManager } from './states.js';

/**
* Creates a new application and returns the created data.
Expand Down Expand Up @@ -108,60 +108,60 @@ export const getApplicationById = async ({ applicationId }: { applicationId: num
};

export const approveApplication = async ({
applicationId,
approverId,
applicationId,
approverId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approverId is unused here

}: ApproveApplication): Promise<{
success: boolean;
message?: string;
errors?: string | Error;
data?: any;
success: boolean;
message?: string;
errors?: string | Error;
data?: any;
}> => {
try {
// Fetch application
const database = getDbInstance();
const service: ApplicationService = applicationService(database);
const result = await service.getApplicationById({ id:applicationId });

if (!result.success) {
return {
success: false,
message: 'Application not found.',
errors: 'ApplicationNotFound',
};
}

const { state } = result.data;
if(state === ApplicationStates.APPROVED){
return {
success: false,
message: 'application is already approved.',
errors: 'ApprovalConflict',
};
}
try {
// Fetch application
const database = getDbInstance();
const service: ApplicationService = applicationService(database);
const result = await service.getApplicationById({ id: applicationId });

if (!result.success) {
return {
success: false,
message: 'Application not found.',
errors: 'ApplicationNotFound',
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be (!result.success) { return result }
The success, errors and message will already be populated by getApplicationById

}

const application = result.data;

const appStateManager = new ApplicationStateManager(application);

if (appStateManager.state === ApplicationStates.APPROVED) {
return {
success: false,
message: 'Application is already approved.',
errors: 'ApprovalConflict',
};
}

const approvalResult = await appStateManager.approveDacReview();
if (!approvalResult.success) {
return {
success: false,
message: 'Failed to approve application.',
errors: 'StateTransitionError',
};
}

const update = { state: appStateManager.state, approved_at: new Date() };
const updatedResult = await service.findOneAndUpdate({ id: applicationId, update });

// Check if we can transition to "APPROVED" state
if (!canTransitionTo(state as ApplicationState, ApplicationStates.APPROVED)) {
return {
success: false,
message: 'Invalid state transition.',
errors: 'InvalidState',
success: true,
data: updatedResult,
};
}

result.data.state = ApplicationStates.APPROVED;

const update = { state: ApplicationStates.APPROVED, approved_at: new Date()};

// Save changes
const updated_result = await service.findOneAndUpdate( { id:applicationId , update });

return { success: true, data: updated_result };
} catch (error) {
const message = `Unable to approve application with id: ${applicationId}`;
console.error(message);
console.error(error);
return failure(message, error);
}
} catch (error) {
const message = `Unable to approve application with id: ${applicationId}`;
console.error(message);
console.error(error);
return failure(message, error);
}
};


2 changes: 1 addition & 1 deletion apps/api/src/resources/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ components:
type: number
description: The id of the Applicant
applicationId:
description: The id of the Applicantion
description: The id of the Application
type: number

responses:
Expand Down
112 changes: 61 additions & 51 deletions apps/api/src/routes/application-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@
import bodyParser from 'body-parser';
import express, { Request } from 'express';


import { approveApplication, createApplication, editApplication, getAllApplications, getApplicationById } from '@/api/application-api.js';
import {
approveApplication,
createApplication,
editApplication,
getAllApplications,
getApplicationById,
} from '@/api/application-api.js';

const applicationRouter = express.Router();
const jsonParser = bodyParser.json();
Expand Down Expand Up @@ -137,67 +142,72 @@ applicationRouter.get(
);

applicationRouter.post('/applications/approve', jsonParser, async (req, res) => {

const { applicationId, approverId }: { applicationId?: number; approverId?: number } = req.body;

if (typeof applicationId !== 'number' || typeof approverId !== 'number') {
res.status(400).send({
message: 'Invalid request. Both applicationId and approverId are required and must be numbers.',
errors: 'MissingOrInvalidParameters',
message: 'Invalid request. Both applicationId and approverId are required and must be numbers.',
errors: 'MissingOrInvalidParameters',
});
return;
}


// Validate input
if (!applicationId || !approverId) {
res.status(400).send({
message: 'Invalid request. Both applicationId and approverId are required.',
errors: 'MissingParameters',
}

// Validate input
if (!applicationId || !approverId) {
res.status(400).send({
message: 'Invalid request. Both applicationId and approverId are required.',
errors: 'MissingParameters',
});
}
try {
}

try {
// Call the service function
const result = await approveApplication({ applicationId, approverId });

if (result.success) {
res.status(200).send({
message: 'Application approved successfully.',
data: result.data,
});
res.status(200).send({
message: 'Application approved successfully.',
data: result.data,
});
}

const resultErrors = String(result.errors);

if (resultErrors === 'ApplicationNotFound') {
res.status(404).send({
message: result.message,
errors: resultErrors,
});
} else if (resultErrors === 'ApprovalConflict') {
res.status(409).send({
message: result.message,
errors: resultErrors,
});
} else if (resultErrors === 'InvalidState') {
res.status(400).send({
message: result.message,
errors: resultErrors,
});

let status = 200;
let message = 'Application approved successfully.';
let errors = null;

if (!result.success) {
// Set appropriate error details
switch (result.errors) {
case 'ApplicationNotFound':
status = 404;
message = result.message || 'Application not found.';
errors = result.errors;
break;
case 'ApprovalConflict':
status = 409;
message = result.message || 'Approval conflict detected.';
errors = result.errors;
break;
case 'InvalidState':
status = 400;
message = result.message || 'Invalid application state.';
errors = result.errors;
break;
default:
status = 500;
message = 'An unexpected error occurred.';
errors = result.errors;
}
}
Azher2Ali marked this conversation as resolved.
Show resolved Hide resolved

res.status(500).send({
message: 'An unexpected error occurred.',
errors: resultErrors,
});
} catch (error) {
res.status(500).send({
message: 'Internal server error.',
errors: String(error),

// Send response
res.status(status).send({ message, errors, data: result.success ? result.data : undefined });
} catch (error) {
res.status(500).send({
message: 'Internal server error.',
errors: String(error),
});
}
}
);
});

export default applicationRouter;
23 changes: 0 additions & 23 deletions apps/api/src/service/ApplicationStateMachine.ts

This file was deleted.

9 changes: 3 additions & 6 deletions packages/data-model/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,7 @@ export type Application = {
contents: ApplicationContents;
};

export type ApproveApplication = {
export type ApproveApplication = {
applicationId: number; // The ID of the application to be approved
approverId: number; // The ID of the user approving the application
}

export type ApplicationState = typeof ApplicationStates[keyof typeof ApplicationStates];

approverId: number; // The ID of the user approving the application
};