feat(ocr) - route fixed, saving done
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
HOST="localhost"
|
HOST=localhost
|
||||||
PORT=5000
|
PORT=5000
|
||||||
FRONTEND_URL="http://localhost:3000"
|
FRONTEND_URL=http://localhost:3000
|
||||||
JWT_SECRET = 'dentalsecret'
|
JWT_SECRET = 'dentalsecret'
|
||||||
DB_HOST="localhost"
|
DB_HOST=localhost
|
||||||
DB_USER="postgres"
|
DB_USER=postgres
|
||||||
DB_PASSWORD="mypassword"
|
DB_PASSWORD=mypassword
|
||||||
DB_NAME="dentalapp"
|
DB_NAME=dentalapp
|
||||||
DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp
|
DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/dentalapp
|
||||||
@@ -152,6 +152,42 @@ router.get("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/payments/full-ocr-import
|
||||||
|
router.post(
|
||||||
|
"/full-ocr-import",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const { rows } = req.body;
|
||||||
|
if (!rows || !Array.isArray(rows)) {
|
||||||
|
return res.status(400).json({ message: "Invalid OCR payload" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentIds = await paymentService.fullOcrPaymentService.importRows(
|
||||||
|
rows,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: "OCR rows imported successfully",
|
||||||
|
paymentIds,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ message: "Unknown error importing OCR payments" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// POST /api/payments/:claimId
|
// POST /api/payments/:claimId
|
||||||
router.post("/:claimId", async (req: Request, res: Response): Promise<any> => {
|
router.post("/:claimId", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import Decimal from "decimal.js";
|
import Decimal from "decimal.js";
|
||||||
import { NewTransactionPayload, Payment, PaymentStatus } from "@repo/db/types";
|
import {
|
||||||
|
NewTransactionPayload,
|
||||||
|
OcrRow,
|
||||||
|
Payment,
|
||||||
|
PaymentMethod,
|
||||||
|
paymentMethodOptions,
|
||||||
|
PaymentStatus,
|
||||||
|
} from "@repo/db/types";
|
||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
import { prisma } from "@repo/db/client";
|
import { prisma } from "@repo/db/client";
|
||||||
|
import { convertOCRDate } from "../utils/dateUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate transactions against a payment record
|
* Validate transactions against a payment record
|
||||||
@@ -149,3 +157,112 @@ export async function updatePayment(
|
|||||||
await validateTransactions(paymentId, serviceLineTransactions, options);
|
await validateTransactions(paymentId, serviceLineTransactions, options);
|
||||||
return applyTransactions(paymentId, serviceLineTransactions, userId);
|
return applyTransactions(paymentId, serviceLineTransactions, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handling full-ocr-payments-import
|
||||||
|
|
||||||
|
export const fullOcrPaymentService = {
|
||||||
|
async importRows(rows: OcrRow[], userId: number) {
|
||||||
|
const results: number[] = [];
|
||||||
|
|
||||||
|
for (const [index, row] of rows.entries()) {
|
||||||
|
try {
|
||||||
|
if (!row.patientName || !row.insuranceId) {
|
||||||
|
throw new Error(
|
||||||
|
`Row ${index + 1}: missing patientName or insuranceId`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!row.procedureCode) {
|
||||||
|
throw new Error(`Row ${index + 1}: missing procedureCode`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const billed = new Decimal(row.totalBilled ?? 0);
|
||||||
|
const allowed = new Decimal(row.totalAllowed ?? row.totalBilled ?? 0);
|
||||||
|
const paid = new Decimal(row.totalPaid ?? 0);
|
||||||
|
|
||||||
|
const adjusted = billed.minus(allowed); // write-off
|
||||||
|
const due = billed.minus(paid).minus(adjusted); // patient responsibility
|
||||||
|
|
||||||
|
// Step 1–3 in a transaction
|
||||||
|
const { paymentId, serviceLineId } = await prisma.$transaction(
|
||||||
|
async (tx) => {
|
||||||
|
// 1. Find or create patient
|
||||||
|
let patient = await tx.patient.findFirst({
|
||||||
|
where: { insuranceId: row.insuranceId.toString() },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!patient) {
|
||||||
|
const [firstNameRaw, ...rest] = (row.patientName ?? "")
|
||||||
|
.trim()
|
||||||
|
.split(" ");
|
||||||
|
const firstName = firstNameRaw || "Unknown";
|
||||||
|
const lastName = rest.length > 0 ? rest.join(" ") : "Unknown";
|
||||||
|
|
||||||
|
patient = await tx.patient.create({
|
||||||
|
data: {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
insuranceId: row.insuranceId.toString(),
|
||||||
|
dateOfBirth: new Date(Date.UTC(1900, 0, 1)), // fallback (1900, jan, 1)
|
||||||
|
gender: "",
|
||||||
|
phone: "",
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create payment (claimId null) — IMPORTANT: start with zeros, due = billed
|
||||||
|
const payment = await tx.payment.create({
|
||||||
|
data: {
|
||||||
|
patientId: patient.id,
|
||||||
|
userId,
|
||||||
|
totalBilled: billed,
|
||||||
|
totalPaid: new Decimal(0),
|
||||||
|
totalAdjusted: new Decimal(0),
|
||||||
|
totalDue: billed,
|
||||||
|
status: "PENDING", // updatePayment will fix it
|
||||||
|
notes: `OCR import from ${row.sourceFile ?? "Unknown file"}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Create service line — IMPORTANT: start with zeros, due = billed
|
||||||
|
const serviceLine = await tx.serviceLine.create({
|
||||||
|
data: {
|
||||||
|
paymentId: payment.id,
|
||||||
|
procedureCode: row.procedureCode,
|
||||||
|
toothNumber: row.toothNumber ?? null,
|
||||||
|
toothSurface: row.toothSurface ?? null,
|
||||||
|
procedureDate: convertOCRDate(row.procedureDate),
|
||||||
|
totalBilled: billed,
|
||||||
|
totalPaid: new Decimal(0),
|
||||||
|
totalAdjusted: new Decimal(0),
|
||||||
|
totalDue: billed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { paymentId: payment.id, serviceLineId: serviceLine.id };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 4: AFTER commit, recalc using updatePayment (global prisma can see it now)
|
||||||
|
// Build transaction & let updatePayment handle recalculation
|
||||||
|
const txn = {
|
||||||
|
serviceLineId,
|
||||||
|
paidAmount: paid.toNumber(),
|
||||||
|
adjustedAmount: adjusted.toNumber(),
|
||||||
|
method: "OTHER" as PaymentMethod,
|
||||||
|
receivedDate: new Date(),
|
||||||
|
notes: "OCR import",
|
||||||
|
};
|
||||||
|
|
||||||
|
await updatePayment(paymentId, [txn], userId);
|
||||||
|
|
||||||
|
results.push(paymentId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Failed to import OCR row ${index + 1}:`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -820,12 +820,14 @@ export const storage: IStorage = {
|
|||||||
serviceLines: true,
|
serviceLines: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
serviceLines: true,
|
||||||
serviceLineTransactions: {
|
serviceLineTransactions: {
|
||||||
include: {
|
include: {
|
||||||
serviceLine: true,
|
serviceLine: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updatedBy: true,
|
updatedBy: true,
|
||||||
|
patient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -852,12 +854,14 @@ export const storage: IStorage = {
|
|||||||
serviceLines: true,
|
serviceLines: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
serviceLines: true,
|
||||||
serviceLineTransactions: {
|
serviceLineTransactions: {
|
||||||
include: {
|
include: {
|
||||||
serviceLine: true,
|
serviceLine: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updatedBy: true,
|
updatedBy: true,
|
||||||
|
patient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -882,12 +886,14 @@ export const storage: IStorage = {
|
|||||||
serviceLines: true,
|
serviceLines: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
serviceLines: true,
|
||||||
serviceLineTransactions: {
|
serviceLineTransactions: {
|
||||||
include: {
|
include: {
|
||||||
serviceLine: true,
|
serviceLine: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updatedBy: true,
|
updatedBy: true,
|
||||||
|
patient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -915,12 +921,14 @@ export const storage: IStorage = {
|
|||||||
serviceLines: true,
|
serviceLines: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
serviceLines: true,
|
||||||
serviceLineTransactions: {
|
serviceLineTransactions: {
|
||||||
include: {
|
include: {
|
||||||
serviceLine: true,
|
serviceLine: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updatedBy: true,
|
updatedBy: true,
|
||||||
|
patient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -950,12 +958,14 @@ export const storage: IStorage = {
|
|||||||
serviceLines: true,
|
serviceLines: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
serviceLines: true,
|
||||||
serviceLineTransactions: {
|
serviceLineTransactions: {
|
||||||
include: {
|
include: {
|
||||||
serviceLine: true,
|
serviceLine: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updatedBy: true,
|
updatedBy: true,
|
||||||
|
patient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export function extractDobParts(date: Date) {
|
|
||||||
return {
|
|
||||||
dob_day: date.getUTCDate(),
|
|
||||||
dob_month: date.getUTCMonth() + 1,
|
|
||||||
dob_year: date.getUTCFullYear(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
26
apps/Backend/src/utils/dateUtils.ts
Normal file
26
apps/Backend/src/utils/dateUtils.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Convert any OCR string-like value into a safe string.
|
||||||
|
*/
|
||||||
|
export function toStr(val: string | number | null | undefined): string {
|
||||||
|
if (val == null) return "";
|
||||||
|
return String(val).trim();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert OCR date strings like "070825" (MMDDYY) into a JS Date object.
|
||||||
|
* Example: "070825" → 2025-08-07.
|
||||||
|
*/
|
||||||
|
export function convertOCRDate(input: string | number | null | undefined): Date {
|
||||||
|
const raw = toStr(input);
|
||||||
|
|
||||||
|
if (!/^\d{6}$/.test(raw)) {
|
||||||
|
throw new Error(`Invalid OCR date format: ${raw}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = parseInt(raw.slice(0, 2), 10) - 1;
|
||||||
|
const day = parseInt(raw.slice(2, 4), 10);
|
||||||
|
const year2 = parseInt(raw.slice(4, 6), 10);
|
||||||
|
const year = year2 < 50 ? 2000 + year2 : 1900 + year2;
|
||||||
|
|
||||||
|
return new Date(year, month, day);
|
||||||
|
}
|
||||||
@@ -156,41 +156,56 @@ export default function PaymentOCRBlock() {
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
const payload = rows.map((row) => {
|
const skipped: string[] = [];
|
||||||
const billed = Number(row["Billed Amount"] ?? 0);
|
|
||||||
const allowed = Number(row["Allowed Amount"] ?? 0);
|
|
||||||
const paid = Number(row["Paid Amount"] ?? 0);
|
|
||||||
|
|
||||||
return {
|
const payload = rows
|
||||||
patientId: parseInt(row["Patient ID"] as string, 10),
|
.map((row, idx) => {
|
||||||
totalBilled: billed,
|
const patientName = row["Patient Name"];
|
||||||
totalPaid: paid,
|
const patientId = row["Patient ID"];
|
||||||
totalAdjusted: billed - allowed, // ❗ write-off
|
const procedureCode = row["CDT Code"];
|
||||||
totalDue: allowed - paid, // ❗ patient responsibility
|
|
||||||
notes: `OCR import - CDT ${row["CDT Code"]}, Tooth ${row["Tooth"]}, Date ${row["Date SVC"]}`,
|
if (!patientName || !patientId || !procedureCode) {
|
||||||
serviceLine: {
|
skipped.push(`Row ${idx + 1} (missing name/id/procedureCode)`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
patientName,
|
||||||
|
insuranceId: patientId,
|
||||||
|
icn: row["ICN"] ?? null,
|
||||||
procedureCode: row["CDT Code"],
|
procedureCode: row["CDT Code"],
|
||||||
procedureDate: convertOCRDate(row["Date SVC"]), // you’ll parse "070825" → proper Date
|
toothNumber: row["Tooth"] ?? null,
|
||||||
toothNumber: row["Tooth"],
|
toothSurface: row["Surface"] ?? null,
|
||||||
totalBilled: billed,
|
procedureDate: row["Date SVC"] ?? null,
|
||||||
totalPaid: paid,
|
totalBilled: Number(row["Billed Amount"] ?? 0),
|
||||||
totalAdjusted: billed - allowed,
|
totalAllowed: Number(row["Allowed Amount"] ?? 0),
|
||||||
totalDue: allowed - paid,
|
totalPaid: Number(row["Paid Amount"] ?? 0),
|
||||||
},
|
sourceFile: row["Source File"] ?? null,
|
||||||
transaction: {
|
};
|
||||||
paidAmount: paid,
|
})
|
||||||
adjustedAmount: billed - allowed, // same as totalAdjusted
|
.filter((r) => r !== null);
|
||||||
method: "OTHER", // fallback, since OCR doesn’t give this
|
|
||||||
receivedDate: new Date(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await apiRequest(
|
if (skipped.length > 0) {
|
||||||
"POST",
|
toast({
|
||||||
"/api/payments/ocr",
|
title:
|
||||||
JSON.stringify({ payments: payload })
|
"Some rows skipped, because of either no patient Name or MemberId given.",
|
||||||
);
|
description: skipped.join(", "),
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "No valid rows to save",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await apiRequest("POST", "/api/payments/full-ocr-import", {
|
||||||
|
rows: payload,
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error("Failed to save OCR payments");
|
if (!res.ok) throw new Error("Failed to save OCR payments");
|
||||||
|
|
||||||
|
|||||||
@@ -370,6 +370,11 @@ export default function PaymentsRecentTable({
|
|||||||
paymentsData?.totalCount || 0
|
paymentsData?.totalCount || 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getName = (p: PaymentWithExtras) =>
|
||||||
|
p.patient
|
||||||
|
? `${p.patient.firstName} ${p.patient.lastName}`.trim()
|
||||||
|
: (p.patientName ?? "Unknown");
|
||||||
|
|
||||||
const getInitials = (fullName: string) => {
|
const getInitials = (fullName: string) => {
|
||||||
const parts = fullName.trim().split(/\s+/);
|
const parts = fullName.trim().split(/\s+/);
|
||||||
const filteredParts = parts.filter((part) => part.length > 0);
|
const filteredParts = parts.filter((part) => part.length > 0);
|
||||||
@@ -480,7 +485,7 @@ export default function PaymentsRecentTable({
|
|||||||
<TableHead>Claim ID</TableHead>
|
<TableHead>Claim ID</TableHead>
|
||||||
<TableHead>Patient Name</TableHead>
|
<TableHead>Patient Name</TableHead>
|
||||||
<TableHead>Amount</TableHead>
|
<TableHead>Amount</TableHead>
|
||||||
<TableHead>Claim Submitted on</TableHead>
|
<TableHead>Service Date</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -519,6 +524,14 @@ export default function PaymentsRecentTable({
|
|||||||
const totalPaid = Number(payment.totalPaid || 0);
|
const totalPaid = Number(payment.totalPaid || 0);
|
||||||
const totalDue = Number(payment.totalDue || 0);
|
const totalDue = Number(payment.totalDue || 0);
|
||||||
|
|
||||||
|
const displayName = getName(payment);
|
||||||
|
const submittedOn =
|
||||||
|
payment.serviceLines?.[0]?.procedureDate ??
|
||||||
|
payment.claim?.createdAt ??
|
||||||
|
payment.createdAt ??
|
||||||
|
payment.serviceLineTransactions?.[0]?.receivedDate ??
|
||||||
|
null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={payment.id}>
|
<TableRow key={payment.id}>
|
||||||
{allowCheckbox && (
|
{allowCheckbox && (
|
||||||
@@ -547,13 +560,13 @@ export default function PaymentsRecentTable({
|
|||||||
className={`h-10 w-10 ${getAvatarColor(Number(payment.id))}`}
|
className={`h-10 w-10 ${getAvatarColor(Number(payment.id))}`}
|
||||||
>
|
>
|
||||||
<AvatarFallback className="text-white">
|
<AvatarFallback className="text-white">
|
||||||
{getInitials(payment.patientName)}
|
{getInitials(displayName)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">
|
||||||
{payment.patientName}
|
{displayName}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
PID-{payment.patientId?.toString().padStart(4, "0")}
|
PID-{payment.patientId?.toString().padStart(4, "0")}
|
||||||
@@ -585,7 +598,7 @@ export default function PaymentsRecentTable({
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{formatDateToHumanReadable(payment.paymentDate)}
|
{formatDateToHumanReadable(submittedOn)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
GOOGLE_APPLICATION_CREDENTIALS=google_credentials.json
|
GOOGLE_APPLICATION_CREDENTIALS=google_credentials.json
|
||||||
HOST="0.0.0.0"
|
HOST=0.0.0.0
|
||||||
PORT="5003"
|
PORT=5003
|
||||||
@@ -52,7 +52,7 @@ export interface InputServiceLine {
|
|||||||
export type ClaimWithServiceLines = Claim & {
|
export type ClaimWithServiceLines = Claim & {
|
||||||
serviceLines: {
|
serviceLines: {
|
||||||
id: number;
|
id: number;
|
||||||
claimId: number;
|
claimId: number | null;
|
||||||
procedureCode: string;
|
procedureCode: string;
|
||||||
procedureDate: Date;
|
procedureDate: Date;
|
||||||
oralCavityArea: string | null;
|
oralCavityArea: string | null;
|
||||||
|
|||||||
@@ -99,3 +99,18 @@ export const newTransactionPayloadSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type NewTransactionPayload = z.infer<typeof newTransactionPayloadSchema>;
|
export type NewTransactionPayload = z.infer<typeof newTransactionPayloadSchema>;
|
||||||
|
|
||||||
|
// OCR Payment - row
|
||||||
|
export interface OcrRow {
|
||||||
|
patientName: string;
|
||||||
|
insuranceId: string | number;
|
||||||
|
icn?: string | null;
|
||||||
|
procedureCode: string;
|
||||||
|
toothNumber?: string | null;
|
||||||
|
toothSurface?: string | null;
|
||||||
|
procedureDate: string | null;
|
||||||
|
totalBilled: number;
|
||||||
|
totalAllowed?: number;
|
||||||
|
totalPaid: number;
|
||||||
|
sourceFile?: string | null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user