Skip to content

Commit

Permalink
🎉 feat: courthouse (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Jan 3, 2022
1 parent 5ea3ab6 commit 5bd66e9
Show file tree
Hide file tree
Showing 16 changed files with 806 additions and 34 deletions.
36 changes: 36 additions & 0 deletions packages/api/prisma/migrations/20220103114738_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- CreateEnum
CREATE TYPE "ExpungementRequestStatus" AS ENUM ('ACCEPTED', 'DENIED', 'PENDING');

-- AlterTable
ALTER TABLE "Record" ADD COLUMN "expungementRequestId" TEXT;

-- AlterTable
ALTER TABLE "RecordLog" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "Warrant" ADD COLUMN "expungementRequestId" TEXT;

-- CreateTable
CREATE TABLE "ExpungementRequest" (
"id" TEXT NOT NULL,
"citizenId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"status" "ExpungementRequestStatus" NOT NULL DEFAULT E'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ExpungementRequest_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Record" ADD CONSTRAINT "Record_expungementRequestId_fkey" FOREIGN KEY ("expungementRequestId") REFERENCES "ExpungementRequest"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Warrant" ADD CONSTRAINT "Warrant_expungementRequestId_fkey" FOREIGN KEY ("expungementRequestId") REFERENCES "ExpungementRequest"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ExpungementRequest" ADD CONSTRAINT "ExpungementRequest_citizenId_fkey" FOREIGN KEY ("citizenId") REFERENCES "Citizen"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ExpungementRequest" ADD CONSTRAINT "ExpungementRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
78 changes: 53 additions & 25 deletions packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ model User {
vehicles RegisteredVehicle[]
weapons Weapon[]
notifications Notification[]
executedNotifictions Notification[] @relation("executor")
executedNotifictions Notification[] @relation("executor")
medicalRecords MedicalRecord[]
bleeterPosts BleeterPost[]
towCalls TowCall[]
Expand All @@ -103,6 +103,7 @@ model User {
emsFdDeputies EmsFdDeputy[]
TaxiCall TaxiCall[]
TruckLog TruckLog[]
ExpungementRequest ExpungementRequest[]
}

enum StatusViewMode {
Expand Down Expand Up @@ -160,6 +161,7 @@ model Citizen {
updatedAt DateTime @default(now()) @updatedAt
RecordRelease RecordRelease[]
RecordLog RecordLog[]
ExpungementRequest ExpungementRequest[]
}

enum Rank {
Expand Down Expand Up @@ -697,20 +699,22 @@ enum BoloType {

// tickets, arrest reports, warrants, written warnings
model Record {
id String @id @default(uuid())
type RecordType
citizen Citizen @relation(fields: [citizenId], references: [id], onDelete: Cascade)
citizenId String
officer Officer @relation(fields: [officerId], references: [id], onDelete: Cascade)
officerId String
violations Violation[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
postal String @db.VarChar(255)
notes String? @db.Text
release RecordRelease? @relation(fields: [releaseId], references: [id])
releaseId String?
RecordLog RecordLog[]
id String @id @default(uuid())
type RecordType
citizen Citizen @relation(fields: [citizenId], references: [id], onDelete: Cascade)
citizenId String
officer Officer @relation(fields: [officerId], references: [id], onDelete: Cascade)
officerId String
violations Violation[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
postal String @db.VarChar(255)
notes String? @db.Text
release RecordRelease? @relation(fields: [releaseId], references: [id])
releaseId String?
RecordLog RecordLog[]
ExpungementRequest ExpungementRequest? @relation(fields: [expungementRequestId], references: [id])
expungementRequestId String?
}

model RecordRelease {
Expand All @@ -733,16 +737,18 @@ enum RecordType {
}

model Warrant {
id String @id @default(uuid())
citizen Citizen @relation(fields: [citizenId], references: [id], onDelete: Cascade)
citizenId String
officer Officer @relation(fields: [officerId], references: [id], onDelete: Cascade)
officerId String
description String @db.Text
status WarrantStatus @default(ACTIVE)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
RecordLog RecordLog[]
id String @id @default(uuid())
citizen Citizen @relation(fields: [citizenId], references: [id], onDelete: Cascade)
citizenId String
officer Officer @relation(fields: [officerId], references: [id], onDelete: Cascade)
officerId String
description String @db.Text
status WarrantStatus @default(ACTIVE)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
RecordLog RecordLog[]
ExpungementRequest ExpungementRequest? @relation(fields: [expungementRequestId], references: [id])
expungementRequestId String?
}

model RecordLog {
Expand All @@ -753,13 +759,35 @@ model RecordLog {
recordId String?
warrant Warrant? @relation(fields: [warrantId], references: [id])
warrantId String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}

enum WarrantStatus {
ACTIVE
INACTIVE
}

// Expungement requests
model ExpungementRequest {
id String @id @default(uuid())
citizen Citizen @relation(fields: [citizenId], references: [id], onDelete: Cascade)
citizenId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
status ExpungementRequestStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
warrants Warrant[]
records Record[]
}

enum ExpungementRequestStatus {
ACCEPTED
DENIED
PENDING
}

// ems-fd
model EmsFdDeputy {
id String @id @default(cuid())
Expand Down
70 changes: 70 additions & 0 deletions packages/api/src/controllers/admin/manage/Courthouse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ExpungementRequestStatus } from "@prisma/client";
import { Controller } from "@tsed/di";
import { BadRequest, NotFound } from "@tsed/exceptions";
import { UseBeforeEach } from "@tsed/platform-middlewares";
import { BodyParams, PathParams } from "@tsed/platform-params";
import { Get, Put } from "@tsed/schema";
import { expungementRequestInclude } from "controllers/court/CourtController";
import { prisma } from "lib/prisma";
import { IsAuth } from "middlewares/index";

@UseBeforeEach(IsAuth)
@Controller("/admin/manage/expungement-requests")
export class ManageCourthouseController {
@Get("/")
async getRequests() {
const requests = await prisma.expungementRequest.findMany({
include: expungementRequestInclude,
});

return requests;
}

@Put("/:id")
async updateExpungementRequest(
@PathParams("id") id: string,
@BodyParams("type") type: ExpungementRequestStatus,
) {
const isCorrect = Object.values(ExpungementRequestStatus).some((v) => v === type);

if (!isCorrect) {
throw new BadRequest("invalidType");
}

const request = await prisma.expungementRequest.findUnique({
where: { id },
include: expungementRequestInclude,
});

if (!request) {
throw new NotFound("requestNotFound");
}

if (type === ExpungementRequestStatus.ACCEPTED) {
await Promise.all(
request.warrants?.map(async (warrant) => {
await prisma.warrant.delete({
where: { id: warrant.id },
});
}),
);

await Promise.all(
request.records?.map(async (record) => {
await prisma.record.delete({
where: { id: record.id },
});
}),
);
}

const updated = await prisma.expungementRequest.update({
where: { id },
data: {
status: type,
},
});

return updated;
}
}
126 changes: 126 additions & 0 deletions packages/api/src/controllers/court/CourtController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { User } from "@prisma/client";
import { BodyParams, Context, PathParams, UseBeforeEach } from "@tsed/common";
import { Controller } from "@tsed/di";
import { BadRequest, NotFound } from "@tsed/exceptions";
import { Get, JsonRequestBody, Post } from "@tsed/schema";
import { citizenInclude } from "controllers/citizen/CitizenController";
import { prisma } from "lib/prisma";
import { IsAuth } from "middlewares/IsAuth";

export const expungementRequestInclude = {
citizen: true,
warrants: true,
records: { include: { violations: { include: { penalCode: true } } } },
};

@Controller("/expungement-requests")
@UseBeforeEach(IsAuth)
export class CourtController {
@Get("/")
async getRequestPerUser(@Context("user") user: User) {
const requests = await prisma.expungementRequest.findMany({
where: {
userId: user.id,
},
include: expungementRequestInclude,
});

return requests;
}

@Get("/:citizenId")
async getCitizensRecords(
@Context("user") user: User,
@PathParams("citizenId") citizenId: string,
) {
const citizen = await prisma.citizen.findFirst({
where: { id: citizenId, userId: user.id },
include: { ...citizenInclude, warrants: true },
});

if (!citizen) {
throw new NotFound("citizenNotFound");
}

return citizen;
}

@Post("/:citizenId")
async requestExpungement(
@Context("user") user: User,
@PathParams("citizenId") citizenId: string,
@BodyParams() body: JsonRequestBody,
) {
const citizen = await prisma.citizen.findFirst({
where: { id: citizenId, userId: user.id },
});

if (!citizen) {
throw new NotFound("citizenNotFound");
}

const request = await prisma.expungementRequest.create({
data: {
citizenId: citizen.id,
userId: user.id,
},
include: expungementRequestInclude,
});

const warrants = body.get("warrants") as string[];
const arrestReports = body.get("arrestReports") as string[];
const tickets = body.get("tickets") as string[];

if (arrestReports.length <= 0 && tickets.length <= 0 && warrants.length <= 0) {
throw new BadRequest("mustSpecifyMinOneArray");
}

const updatedRecords = await Promise.all(
[...arrestReports, ...tickets].map(async (id) => {
const existing = await prisma.expungementRequest.findFirst({
where: { records: { some: { id } }, status: "PENDING" },
});

if (existing) {
return error(new BadRequest("recordOrWarrantAlreadyLinked"), request.id);
}

return prisma.expungementRequest.update({
where: { id: request.id },
data: {
records: { connect: { id } },
},
});
}),
);

const updatedWarrants = await Promise.all(
warrants.map(async (id) => {
const existing = await prisma.expungementRequest.findFirst({
where: { warrants: { some: { id } }, status: "PENDING" },
});

if (existing) {
return error(new BadRequest("recordOrWarrantAlreadyLinked"), request.id);
}

return prisma.expungementRequest.update({
where: { id: request.id },
data: {
warrants: { connect: { id } },
},
});
}),
);

return { ...request, warrants: updatedWarrants, records: updatedRecords };
}
}

async function error<T extends Error = Error>(error: T, id: string) {
await prisma.expungementRequest.delete({
where: { id },
});

throw error;
}
1 change: 1 addition & 0 deletions packages/api/src/middlewares/IsEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const featuresRoute: Partial<Record<Feature, string>> = {
DISCORD_AUTH: "/v1/auth/discord",
WEAPON_REGISTRATION: "/v1/weapons",
CALLS_911: "/v1/911-calls",
COURTHOUSE: "/v1/expungement-requests",
};

@Middleware()
Expand Down
1 change: 1 addition & 0 deletions packages/client/locales/en/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"MANAGE_BUSINESSES": "Manage Businesses",
"MANAGE_USERS": "Manage Users",
"MANAGE_UNITS": "Manage Units",
"MANAGE_EXPUNGEMENT_REQUESTS": "Manage Expungement Requests",
"MANAGE_CAD_SETTINGS": "CAD Settings",
"allUsers": "All Users",
"pendingUsers": "Pending Users",
Expand Down
6 changes: 4 additions & 2 deletions packages/client/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"login": "Login",
"register": "Register",
"callHistory": "Call History",
"citizenLogs": "Citizen Logs"
"citizenLogs": "Citizen Logs",
"courthouse": "Courthouse"
},
"Errors": {
"unknown": "An unexpected error occurred",
Expand Down Expand Up @@ -95,6 +96,7 @@
"discordAccountAlreadyLinked": "This Discord account is already linked to another account.",
"cannotRegisterFirstWithDiscord": "The first account must be registered without Discord. You can link Discord once the account is created.",
"invalidRegistrationCode": "The registration code you provided is invalid.",
"unitSuspended": "This unit is suspended. Please contact your supervisor."
"unitSuspended": "This unit is suspended. Please contact your supervisor.",
"recordOrWarrantAlreadyLinked": "A record or a warrant has already been requested. PLease remove the already requested record/warrant."
}
}
Loading

0 comments on commit 5bd66e9

Please sign in to comment.