import { BadRequestException, Body, Controller, Headers, Post, Req } from "@nestjs/common"; import { Request } from "express"; import { ok } from "../common/response"; import { PlaidService } from "./plaid.service"; import { CurrentUser } from "../common/decorators/current-user.decorator"; import { Public } from "../common/decorators/public.decorator"; import { OpaqueIdService } from "../common/opaque-id.service"; import { ViewRefService } from "../common/view-ref.service"; import { PrismaService } from "../prisma/prisma.service"; @Controller("plaid") export class PlaidController { constructor( private readonly plaidService: PlaidService, private readonly opaqueIds: OpaqueIdService, private readonly viewRefs: ViewRefService, private readonly prisma: PrismaService, ) {} @Post("link-token") async createLinkToken(@CurrentUser() userId: string) { const data = await this.plaidService.createLinkToken(userId); return ok(data); } @Post("exchange") async exchange( @CurrentUser() userId: string, @Body() payload: { publicToken: string }, ) { const data = await this.plaidService.exchangePublicTokenForUser(userId, payload.publicToken); return ok(data); } @Post("update-link-token") async createUpdateLinkToken( @CurrentUser() userId: string, @Body() payload: { accountId: string }, ) { const accountId = await this.resolveAccountHandle(userId, payload.accountId); const data = await this.plaidService.createUpdateModeLinkToken(userId, accountId); return ok(data); } @Post("repair-complete") async repairComplete( @CurrentUser() userId: string, @Body() payload: { accountId: string }, ) { const accountId = await this.resolveAccountHandle(userId, payload.accountId); const data = await this.plaidService.markItemRepairComplete(userId, accountId); return ok(data); } @Public() @Post("webhook") async webhook( @Body() payload: Record, @Headers("plaid-verification") verification: string | undefined, @Req() request: Request & { rawBody?: Buffer }, ) { const data = await this.plaidService.handleWebhook(payload, verification, request.rawBody); return ok(data); } private async resolveAccountHandle(userId: string, handle: string) { try { return this.opaqueIds.decode("account", userId, handle); } catch { const accounts = await this.prisma.account.findMany({ where: { userId, isActive: true }, select: { id: true }, }); const match = accounts.find((account) => this.viewRefs.matches(userId, "account", account.id, handle)); if (!match) throw new BadRequestException("Invalid resource identifier."); return match.id; } } }