Software Engineer
Loading posts...

How we designed and built a comprehensive inventory tracking system for HackPSU using Next.js, React Query, and NestJS to manage thousands of items across multiple event locations.

How I designed and implemented the bulk templated email delivery platform using React, SendGrid, MJML and NestJS for HackPSU organizers.

How we designed and implemented HackPSU's new authentication platform using Next.js, Firebase, and session cookies to secure multiple subdomains with seamless UX.
Feel free to contact me at kanishksachdev@gmail.com
Picture this: It's 11:47 PM on a Tuesday, and I'm watching our finance team filling out their 23rd university reimbursement form of the week. Each form has 47 fields, including gems like "Object Code" (what even is that?) and "Fund Source Authorization" (your guess is as good as mine). Meanwhile, there are 104 more reimbursement requests sitting in our Google Form responses, each one requiring this same manual PDF torture.
The real kicker? Half these requests are for duplicates or submissions that are not valid, but each one still needs the full bureaucratic treatment. They were spending more time on paperwork than the actual expense cost.
That's when I decided to automate the whole mess. Here's how I built a system that turns reimbursement chaos into a one-click operation, and why sometimes the best code solves the most boring problems.
HackPSU runs on expenses. Lots of them. We're talking about expenses covering everything from the huge pizza orders to a packet of spoons during runs to Sam's Club. Each expense, no matter how small, needs to go through our university's reimbursement system to get paid back. And that means filling out a PDF form, printing it, scanning it, and emailing it to the finance office.
Here's the workflow that was slowly killing us:
The real problem? That step 3. Each PDF form took 10-15 minutes to fill out manually. With almost a hundred requests from our spring event, that's over 20 hours of copying data from one place to another.
The breaking point came we had a backlog of 104 reimbursement requests, and we were staring down the barrel of another 20-hour manual data entry marathon. I knew there had to be a better way.
I decided to solve this with a full-stack application that could handle the entire reimbursement lifecycle. The key insight: automate the tedious stuff, streamline the human decisions.
Core Requirements:
We built this on our existing HackPSU platform to seamlessly integrate with our other systems. Here’s the stack we chose:
Frontend: Next.js 15 + TypeScript
Backend: NestJS + MySQL
PDF Generation: pdf-lib
File Storage: GCP Storage Bucket
The core problem was turning our digital reimbursement data into the university's required PDF format. The form has 47 fields with names like "Text1", "Text2", "Check Box1" (thanks, university IT).
I used pdf-lib to programmatically fill forms:
async populateReimbursementForm(
templateName: string,
data: ReimbursementForm,
newTitle: string,
): Promise<Uint8Array> {
const [templateBytes] = await this.fetchTemplate(templateName);
const pdfDoc = await PDFDocument.load(templateBytes);
await this.populatePdfDocument(pdfDoc, data, ReimbursementFormMappings);
pdfDoc.setTitle(newTitle);
return pdfDoc.save();
}
The tricky part was mapping our clean data structure to the PDF's cryptic field names:
const ReimbursementFormMappings = {
payeeName: 'Text5', // Who gets paid
amount1: 'Text12', // Dollar amount
description1: 'Text15', // What it's for
objectCode1: 'Text18', // University accounting code
unrestricted30: 'Check Box1', // Fund source checkbox
// ... 42 more mappings
}
Here's the kicker, there are multiple variations of these field names across different PDF versions. Some PDFs use "Check Box1", others use "Check Box 1" or "CheckBox1" (seriously, why?).
Reimbursements flow through four states: Pending → Approved/Rejected → Deposit. Each status change triggers automated actions that used to require manual work.
@Patch(":id/status")
async updateStatus(
@Param("id") id: string,
@Body() statusData: OptionalStatus,
): Promise<Finance> {
const finance = await this.financeRepo.findOne(id).exec();
if (finance.status !== Status.PENDING) {
throw new BadRequestException("Cannot update non-pending record");
}
finance.status = statusData.status;
const updatedFinance = await this.financeRepo.patchOne(id, finance).exec();
if (updatedFinance.status === Status.APPROVED) {
// 1. Generate filled PDF form
const pdfBytes = await this.populateReimbursementForm(/* ... */);
// 2. Upload to storage
const formUrl = await this.uploadReimbursementForm(id, pdfBytes);
// 3. Email finance team with attachments
await this.sendGridService.send({
to: "finance@hackpsu.org",
subject: "Reimbursement Form Completed",
attachments: [
{ content: pdfBytes, filename: `${id}_form.pdf` },
{ content: receiptFile, filename: `${id}_receipt.pdf` }
]
});
// 4. Notify submitter
await this.sendGridService.send({
to: submitterEmail,
subject: "HackPSU Reimbursement Approved",
message: approvalTemplate
});
}
return updatedFinance;
}
The key insight: when someone clicks "Approve," the system should handle everything else automatically. Generate the PDF, email it to the right people, notify the submitter, and update all tracking records.
I used TanStack Query for state management to make status updates feel instant, even on slow event WiFi:
function useUpdateStatus() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: { id: string; status: Status }) =>
financeApi.updateStatus(data.id, data.status),
// Update UI immediately, fix later if it fails
onMutate: async ({ id, status }) => {
await queryClient.cancelQueries({ queryKey: ['finances'] })
const previousFinances = queryClient.getQueryData(['finances'])
queryClient.setQueryData(['finances'], (old: Finance[]) =>
old?.map((finance) =>
finance.id === id ? { ...finance, status } : finance,
),
)
return { previousFinances }
},
onError: (err, variables, context) => {
queryClient.setQueryData(['finances'], context?.previousFinances)
toast.error(`Status update failed: ${err.message}`)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['finances'] })
},
})
}
This makes the interface feel snappy even when network requests take 2-3 seconds. Users see their changes immediately, and if something goes wrong, we roll back and show an error.
The Old Process:
After Automation:
Technical Performance:
The real impact wasn't just time savings, it was stress elimination. Our finance team went from dreading reimbursement processing to actually enjoying the analytics insights the system provided.
We started to monitor trends in expenses, identified places to cut costs, and even predict future budget needs based on past data. Suddenly, finance wasn't just a necessary evil, it was a strategic advantage.
Automate the boring stuff first: PDF generation and email workflows had the biggest impact on user happiness, even though they're not the coolest features. People like to know when their money is coming, and they hate filling out forms.
TypeScript is non-negotiable for financial data: Runtime type errors with money calculations aren't just bugs, they're potential disasters.
Error messages matter: "ValidationError: field submitterId is required" doesn't help a tired organizer at 2 AM. We rewrote all error messages to be human-friendly.
Mobile-first, always: People want to submit reimbursements from their phones, not hunt down a laptop to fill out a form.
Scanning receipts to automatically extract data? Using AI to find fishy trends in data? Fuzzy hashing to prevent duplicate submissions? Maybe. But for now, we're focused on automating workflows of other teams at HackPSU. The finance system was just the first step in making our operations smoother.
We built a system that automatically generates university reimbursement PDFs, handles email notifications, and provides real-time tracking for HackPSU expenses. The result: 95% reduction in processing time and zero manual PDF filling.
Key technologies: Next.js, NestJS, pdf-lib for PDF manipulation, TanStack Query for optimistic updates, and GCP for file storage.
Real impact: Saved 19 hours per event in manual work and eliminated the stress of managing 100+ reimbursement requests through spreadsheets and manual processes.
Sometimes the best software solves the most mundane problems. Nobody gets excited about reimbursement workflows, but when you remove friction from necessary processes, you free people up to focus on work that actually matters.