diff --git a/apps/api/src/api/application-api.ts b/apps/api/src/api/application-api.ts index 42b47c2a..acab3c97 100644 --- a/apps/api/src/api/application-api.ts +++ b/apps/api/src/api/application-api.ts @@ -17,13 +17,14 @@ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import { ApplicationStates } 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 { ApplicationStateManager } from './states.js'; /** * Creates a new application and returns the created data. @@ -105,3 +106,62 @@ export const getApplicationById = async ({ applicationId }: { applicationId: num return result; }; + +export const approveApplication = async ({ + applicationId, + approverId, +}: ApproveApplication): Promise<{ + 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 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 }); + + return { + success: true, + data: updatedResult, + }; + } catch (error) { + const message = `Unable to approve application with id: ${applicationId}`; + console.error(message); + console.error(error); + return failure(message, error); + } +}; diff --git a/apps/api/src/resources/swagger.yaml b/apps/api/src/resources/swagger.yaml index 910cbd7e..bfeea9fa 100644 --- a/apps/api/src/resources/swagger.yaml +++ b/apps/api/src/resources/swagger.yaml @@ -196,6 +196,43 @@ paths: application/json: schema: $ref: '#/components/schemas/ApplicationRecord' + /applications/approve: + post: + tags: + - Applications + summary: Approve an application. + requestBody: + description: Request body containing approval details. + required: true + content: + application/json: + schema: + $ref: '#/components/requests/ApproveApplication' + responses: + '200': + description: Application successfully approved. + content: + application/json: + schema: + $ref: '#/components/schemas/Applications' + '400': + description: Invalid request - missing or invalid approval data. + content: + application/json: + schema: + $ref: '#/components/responses/ClientErrors' + '404': + description: Application not found. + content: + application/json: + schema: + $ref: '#/components/responses/ClientErrors' + '500': + description: Server error - Unable to process approval. + content: + application/json: + schema: + $ref: '#/components/responses/ServerErrors' components: schemas: @@ -371,6 +408,19 @@ components: type: object $ref: '#/components/schemas/ApplicationContents' + ApproveApplication: + type: object + required: + - applicantId + - applicationId + properties: + applicantId: + type: number + description: The id of the Applicant + applicationId: + description: The id of the Application + type: number + responses: ClientErrors: type: object diff --git a/apps/api/src/routes/application-router.ts b/apps/api/src/routes/application-router.ts index 61c5e172..5c378ba2 100644 --- a/apps/api/src/routes/application-router.ts +++ b/apps/api/src/routes/application-router.ts @@ -20,7 +20,13 @@ import bodyParser from 'body-parser'; import express, { Request } from 'express'; -import { 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(); @@ -135,4 +141,73 @@ 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', + }); + return; + } + + // Validate input + if (!applicationId || !approverId) { + res.status(400).send({ + message: 'Invalid request. Both applicationId and approverId are required.', + errors: 'MissingParameters', + }); + } + + 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, + }); + } + + 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; + } + } + + // 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; diff --git a/packages/data-model/src/types.ts b/packages/data-model/src/types.ts index 1a7267da..d77ac912 100644 --- a/packages/data-model/src/types.ts +++ b/packages/data-model/src/types.ts @@ -223,3 +223,8 @@ export type Application = { expires_at: Date; contents: ApplicationContents; }; + +export type ApproveApplication = { + applicationId: number; // The ID of the application to be approved + approverId: number; // The ID of the user approving the application +};