Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ CI (`.github/workflows/ci.yml`, on PR + push to `main`) runs **lint + check-type
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **bootcamp-starter** (3700 symbols, 7360 relationships, 139 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **bootcamp-starter** (4026 symbols, 7977 relationships, 149 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
41 changes: 31 additions & 10 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,50 @@
# syntax=docker/dockerfile:1
# Property Manager — API (NestJS) image. Build context = repo root.
# docker build -f apps/api/Dockerfile -t <reg>/property-manager-api:<tag> .
#
# Slim, turbo-pruned build. `turbo prune property-manager-be --docker` carves out
# ONLY the api plus its internal workspace deps (@repo/db, @repo/contracts) and a
# pruned lockfile — so the WEB workspace's ~700MB of deps (next, @next, @img,
# lucide-react, …) never enter this image. A final `npm prune --omit=dev` then
# strips the build toolchain (typescript, nest cli, prisma CLI, jest, …). The
# generated Prisma client is preserved across that prune (see note below).

# ---------- builder ----------
# ---------- pruner: carve the api subset out of the monorepo ----------
FROM node:22-slim AS pruner
WORKDIR /app
COPY . .
# node_modules is dockerignored here, so fetch a pinned turbo to run the prune
# (prune only reads package.json files + the lockfile — no install needed).
RUN npx --yes turbo@2.10.0 prune property-manager-be --docker

# ---------- builder: install pruned deps + build ----------
FROM node:22-slim AS builder
# openssl → Prisma engine. (No node-gyp toolchain: the only native dep,
# msgpackr-extract, is optional and fails soft to a pure-JS path.)
# openssl → Prisma engine.
RUN apt-get update && apt-get install -y --no-install-recommends \
openssl ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
# Install against the pruned lockfile first (keeps this layer cacheable).
COPY --from=pruner /app/out/json/ .
RUN npm ci
# Build shared workspace packages in EXPLICIT order first. apps/* import
# @repo/contracts but don't all declare it as a dependency, so turbo's graph
# can race and build the app before contracts/dist exists. Build deps by hand.
# Bring in the pruned source, then build shared packages in EXPLICIT order.
# (apps/api now declares @repo/contracts, so `turbo build` would order this too,
# but the explicit sequence keeps the image build self-contained and legible.)
COPY --from=pruner /app/out/full/ .
RUN npm run build --workspace=@repo/contracts
RUN npm run db:generate --workspace=@repo/db
RUN npm run db:build --workspace=@repo/db
# API build: `npm run build` = rm tsbuildinfo && nest build.
RUN cd apps/api && npm run build
# Fail loudly if the Nest build did not emit.
RUN test -f apps/api/dist/main.js
# Strip devDependencies. `npm prune` also deletes the UNTRACKED generated Prisma
# client at node_modules/.prisma, so back it up and restore it afterwards
# (@prisma/client itself is a prod dep and survives). tsconfig-paths is a prod
# dep — start:prod registers it at runtime.
RUN cp -r node_modules/.prisma /tmp/dot-prisma
RUN npm prune --omit=dev
RUN rm -rf node_modules/.prisma && cp -r /tmp/dot-prisma node_modules/.prisma && rm -rf /tmp/dot-prisma

# ---------- runtime ----------
FROM node:22-slim AS runtime
Expand All @@ -30,9 +53,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production
WORKDIR /app
# Copy the whole built monorepo. We intentionally keep devDependencies because
# `start:prod` registers `tsconfig-paths` (a devDependency) at runtime, and the
# generated Prisma client/engine lives under node_modules/.prisma.
# Copy the pruned+built monorepo subset (api + @repo/db + @repo/contracts, prod deps).
COPY --from=builder /app ./
EXPOSE 20101
# start:prod = node -e "require('tsconfig-paths').register({baseUrl:'./dist',...}); require('./dist/main')"
Expand Down
5 changes: 3 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@nestjs/terminus": "^11.1.1",
"@nestjs/throttler": "^6.5.0",
"@prisma/adapter-pg": "^7.8.0",
"@repo/contracts": "*",
"@repo/db": "*",
"axios": "^1.15.2",
"bullmq": "^5.80.6",
Expand All @@ -52,7 +53,8 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"stripe": "^22.1.1",
"swagger-ui-express": "^5.0.1"
"swagger-ui-express": "^5.0.1",
"tsconfig-paths": "^4.2.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
Expand Down Expand Up @@ -82,7 +84,6 @@
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,63 @@ export class InvoicePaymentsController {
private readonly orgScope: OrgScopeService,
) {}

/**
* Org-wide rent-payment register (paginated). Also serves the invoice-detail
* payment list via `invoiceId`. A supervisor is narrowed to their assigned
* buildings inside the service.
*/
@Roles(Role.ORG_ADMIN, Role.FINANCE, Role.SUPERVISOR)
@Get()
async getInvoicePayments(
@CurrentUser() user: AuthenticatedUser,
@Query('invoiceId') invoiceId?: string,
@Query('buildingId') buildingId?: string,
@Query('renterId') renterId?: string,
@Query('method') method?: string,
@Query('from') from?: string,
@Query('to') to?: string,
@Query('q') q?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
const { orgId, role } = await this.orgScope.resolveForCaller(user);
return this.invoicePaymentsService.findAll(
orgId,
user.sub,
role,
return this.invoicePaymentsService.findAll(orgId, user.sub, role, {
invoiceId,
);
buildingId,
renterId,
method,
from,
to,
q,
page: page ? Number(page) : undefined,
limit: limit ? Number(limit) : undefined,
Comment on lines +55 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Non-numeric page/limit become NaN and reach Prisma as skip/take. The root cause is that the query string is converted with Number() without a finiteness check, and the service clamp propagates NaN (Math.max(1, NaN) is NaN), so ?page=abc returns a 500 instead of falling back to the defaults.

  • apps/api/src/modules/invoice-payments/invoice-payments.controller.ts#L55-L56: only pass the parsed value through when Number.isFinite() holds, otherwise leave it undefined.
  • apps/api/src/modules/invoice-payments/invoice-payments.service.ts#L311-L315: guard the clamp with a finite check so page/limit always resolve to 1/DEFAULT_LIMIT for unusable input.
📍 Affects 2 files
  • apps/api/src/modules/invoice-payments/invoice-payments.controller.ts#L55-L56 (this comment)
  • apps/api/src/modules/invoice-payments/invoice-payments.service.ts#L311-L315
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/modules/invoice-payments/invoice-payments.controller.ts` around
lines 55 - 56, Prevent non-finite query values from reaching Prisma: in
apps/api/src/modules/invoice-payments/invoice-payments.controller.ts lines
55-56, only pass parsed page and limit values when Number.isFinite() succeeds,
otherwise use undefined; in
apps/api/src/modules/invoice-payments/invoice-payments.service.ts lines 311-315,
guard the pagination clamp so unusable page and limit values resolve to 1 and
DEFAULT_LIMIT respectively.

});
}

/**
* Declared BEFORE `:id` — Nest matches routes in declaration order, so the
* dynamic param would otherwise swallow `/summary`.
*/
@Roles(Role.ORG_ADMIN, Role.FINANCE, Role.SUPERVISOR)
@Get('summary')
async getInvoicePaymentSummary(
@CurrentUser() user: AuthenticatedUser,
@Query('buildingId') buildingId?: string,
@Query('renterId') renterId?: string,
@Query('method') method?: string,
@Query('from') from?: string,
@Query('to') to?: string,
@Query('q') q?: string,
) {
const { orgId, role } = await this.orgScope.resolveForCaller(user);
return this.invoicePaymentsService.summary(orgId, user.sub, role, {
buildingId,
renterId,
method,
from,
to,
q,
});
}

@Roles(Role.ORG_ADMIN, Role.FINANCE, Role.SUPERVISOR)
Expand Down
Loading
Loading