Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
31 changes: 16 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ CI (`.github/workflows/ci.yml`, on PR + push to `main`) runs **lint + check-type
- Don't duplicate cross-app types per-app — put shared API types/DTOs in `@repo/contracts`, and import the DB client/types from `@repo/db` (never re-declare them).

<!-- 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 All @@ -105,22 +106,22 @@ This project is indexed by GitNexus as **bootcamp-starter** (3700 symbols, 7360

## Resources

| Resource | Use for |
|----------|---------|
| `gitnexus://repo/bootcamp-starter/context` | Codebase overview, check index freshness |
| `gitnexus://repo/bootcamp-starter/clusters` | All functional areas |
| `gitnexus://repo/bootcamp-starter/processes` | All execution flows |
| `gitnexus://repo/bootcamp-starter/process/{name}` | Step-by-step execution trace |
| Resource | Use for |
| ------------------------------------------------- | ---------------------------------------- |
| `gitnexus://repo/bootcamp-starter/context` | Codebase overview, check index freshness |
| `gitnexus://repo/bootcamp-starter/clusters` | All functional areas |
| `gitnexus://repo/bootcamp-starter/processes` | All execution flows |
| `gitnexus://repo/bootcamp-starter/process/{name}` | Step-by-step execution trace |

## CLI

| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
| Task | Read this skill file |
| -------------------------------------------- | ----------------------------------------------------------- |
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |

<!-- gitnexus:end -->
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