Skip to content

store hourly rate per client#56

Merged
grmbyrn merged 3 commits into
mainfrom
wl-44
Mar 30, 2026
Merged

store hourly rate per client#56
grmbyrn merged 3 commits into
mainfrom
wl-44

Conversation

@grmbyrn
Copy link
Copy Markdown
Owner

@grmbyrn grmbyrn commented Mar 30, 2026

#44

Summary by CodeRabbit

  • New Features

    • Allow entering a custom hourly rate when creating invoices; falls back to client rate if not provided.
  • Bug Fixes

    • Ensure hourly rates are treated as numbers and validated (rejects invalid/negative rates); improved calculation precision for earnings and invoice amounts.
  • Tests

    • Updated and expanded tests, including an end-to-end style test covering client selection, hourly-rate prefill, date inputs, invoice generation, and request payload verification.
  • Chores

    • Database schema updates to store hourly rates and track client updates (migrations applied).

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worklog Ready Ready Preview, Comment Mar 30, 2026 5:57pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Adds support for specifying and persisting an invoice hourly rate: DB schema updated to store invoice/hourly rates and client rate precision, API endpoints accept/validate an optional hourlyRate and use it for calculations, frontend form prefills/allows overriding the rate, and tests updated to cover the flow. Numeric coercions were added where needed.

Changes

Cohort / File(s) Summary
Tests
__tests__/InvoicesPage.test.tsx
Updated mocks and assertions for role/exact-text matching; added e2e-style test to select client, prefill rate, set dates, generate invoice, and assert POST body contains hourlyRate: 100.
Invoice POST API
app/api/invoices/route.ts
POST handler now accepts hourlyRate, validates numeric >= 0 (falls back to client rate), computes totals using resolved rate, constructs typed invoiceData and persists hourlyRate; returns 400 on invalid rates.
Invoice GET / Amounts
app/api/invoices/[id]/route.ts
Coerced invoice.hourlyRate with Number() when computing entry amount.
Dashboard calculations
app/api/dashboard/route.ts
Coerced entry.client.hourlyRate via Number() across earnings aggregations and reductions.
Frontend pages
app/invoices/page.tsx, app/timer/page.tsx
Added hourlyRate field to invoice form, client selection now sets clientId and hourlyRate (stringified); changed client/invoice fetches to /api/...; timer page updated to read clientsData.clients shape.
Prisma schema
prisma/schema.prisma
Converted Client.hourlyRate Float→Decimal(10,2) with default 0; added Client.updatedAt @updatedAt``; added Invoice.hourlyRate Decimal(10,2) @default`(0)`.
Migrations
prisma/migrations/.../20260330120000_add_invoice_hourly_rate/migration.sql, .../20260330140000_update_client_hourlyrate_and_updatedAt/migration.sql, .../20260330173921_update_client_hourlyrate_and_updated_at/migration.sql
Three DB migrations: add Invoice.hourlyRate column; convert Client.hourlyRate to NUMERIC(10,2), set NOT NULL/default/backfill and add updatedAt; then remove DB default from updatedAt.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as InvoicesPage
    participant API as /api/invoices (POST)
    participant DB as Database

    User->>UI: Select client
    UI->>UI: Prefill hourlyRate from client data
    User->>UI: Optionally override hourlyRate
    User->>UI: Click "Generate Invoice"
    UI->>API: POST { clientId, hourlyRate, startDate, endDate }
    API->>API: Resolve rate (use provided hourlyRate or fall back to client.hourlyRate)
    API->>API: Validate rate (Number, finite, >=0)
    alt validation fails
        API-->>UI: 400 Error (invalid hourlyRate)
    else validation succeeds
        API->>DB: Create invoice with rounded hourlyRate, hours, amount
        DB-->>API: Created invoice
        API-->>UI: Success
        UI->>UI: Reset form (clear hourlyRate)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feature/invoice paid status #18 — touches invoices tests and mock fetch behavior for PATCHing invoice endpoints (related mocking/assertion changes in tests).

Poem

🐇 I hopped through fields of decimals and rates,
Prefilled a number before the invoice gates.
A tiny POST, a careful Number() sigh—
Now invoices carry the rate that passed by.
🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'store hourly rate per client' accurately captures the main objective: adding persistent hourly rate storage at the invoice level while updating the Client schema with Decimal precision and updatedAt tracking.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wl-44

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
app/invoices/page.tsx (2)

96-96: ⚠️ Potential issue | 🟡 Minor

Inconsistent API fetch URL pattern.

This fetch call uses a relative path api/clients without the leading /, while other calls in this file (e.g., line 122, 138) use absolute paths like /api/invoices. This inconsistency can cause issues when base paths differ. Issue #15 in .github/ISSUES/15-normalize-api-fetch-urls.md already tracks this debt.

Proposed fix
-      const res = await fetch('api/clients');
+      const res = await fetch('/api/clients');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/invoices/page.tsx` at line 96, The fetch call in page.tsx uses a relative
URL 'api/clients' which is inconsistent with other calls using absolute paths;
update the fetch invocation that assigns to res (res = await
fetch('api/clients')) to use an absolute path '/api/clients' so it matches the
other calls (e.g., the fetches for '/api/invoices') and prevents base-path
issues as tracked by Issue `#15`.

111-111: ⚠️ Potential issue | 🟡 Minor

Same inconsistent URL pattern for invoices fetch.

Same issue as the clients fetch - should use /api/invoices for consistency.

Proposed fix
-      const res = await fetch('api/invoices');
+      const res = await fetch('/api/invoices');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/invoices/page.tsx` at line 111, The fetch call in page.tsx uses a
relative path 'api/invoices' which is inconsistent with other calls; update the
fetch invocation (the line creating const res via fetch) to use an absolute path
'/api/invoices' so it matches the clients fetch pattern and avoids routing
issues.
app/api/invoices/route.ts (2)

64-67: ⚠️ Potential issue | 🟠 Major

Validate date inputs before querying.

Line 64–Line 67 assumes valid dates. Invalid or reversed ranges should return 400 early to avoid invalid DB filters and confusing outcomes.

🔧 Proposed fix
     const from = new Date(startDate);
     const to = new Date(endDate);
+    if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
+      return Response.json({ error: 'Invalid date range' }, { status: 400 });
+    }
+    if (from > to) {
+      return Response.json({ error: 'startDate must be <= endDate' }, { status: 400 });
+    }
     to.setHours(23, 59, 59, 999);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/invoices/route.ts` around lines 64 - 67, Validate the incoming
startDate and endDate before constructing Date objects and querying: parse and
ensure both startDate and endDate produce valid Dates (not NaN) and that
startDate <= endDate; if either date is invalid or the range is reversed, return
a 400 response early. Update the logic around the creation of from and to (the
variables from, to and the inputs startDate, endDate) to perform these checks
and short-circuit with a clear 400 error message instead of proceeding to
setHours and querying the DB.

40-44: ⚠️ Potential issue | 🟠 Major

Handle malformed JSON inside the guarded error path.

Line 40 parses req.json() before try; malformed JSON bypasses your API error handling. Move body parsing/required-field checks into try and return 400 for parse errors.

🔧 Proposed fix
-  const { clientId, startDate, endDate, hourlyRate } = await req.json();
-
-  if (!clientId || !startDate || !endDate) {
-    return Response.json({ error: 'Missing required fields' }, { status: 400 });
-  }
-
   try {
+    const { clientId, startDate, endDate, hourlyRate } = await req.json();
+    if (!clientId || !startDate || !endDate) {
+      return Response.json({ error: 'Missing required fields' }, { status: 400 });
+    }
     const user = await prisma.user.upsert({
       where: { email: session.user.email },
       update: {},
@@
-  } catch (error) {
+  } catch (error) {
+    if (error instanceof SyntaxError) {
+      return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
+    }
     console.error('Error creating invoice:', error);
     return Response.json({ error: 'Internal server error' }, { status: 500 });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/invoices/route.ts` around lines 40 - 44, Move the request body
parsing and required-field validation into the try block so JSON parse errors
are caught and return a 400; specifically, stop calling await req.json() at the
top-level and instead call it inside the try that wraps the handler, then
destructure clientId, startDate, endDate, hourlyRate from that parsed body and
perform the existing required-field check (the block that currently returns
Response.json({ error: 'Missing required fields' }, { status: 400 })); ensure
any JSON parse exceptions are caught and mapped to a 400 response before other
error handling.
🧹 Nitpick comments (3)
.github/ISSUES/22-harden-jest-setup.md (1)

9-9: Make “isolated environment” explicit in CI criteria.

Line 9 is a bit ambiguous; consider naming the concrete CI setting/command (e.g., worker/process isolation strategy) so completion is testable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/ISSUES/22-harden-jest-setup.md at line 9, Replace the vague line
"Ensure CI runs tests with isolated environment." with a concrete CI test
command and isolation strategy so it's testable—for example, specify the exact
test runner flag or command to use (e.g., run tests in CI using "jest
--runInBand" or "jest --maxWorkers=50%" or invoke "npm test -- --runInBand"),
and mention the CI job/step that should execute it (the CI test step that runs
the test command), so the requirement names the command/flag and the job that
enforces worker/process isolation.
app/invoices/page.tsx (1)

240-247: Consider adding required attribute to the hourly rate input.

The hourly rate input lacks the required attribute, unlike the other form fields (client, start date, end date). This could allow form submission with an empty hourly rate. If the backend expects a valid hourlyRate, this could cause errors or incorrect invoice calculations.

Proposed fix
             <input
               type="number"
               value={formData.hourlyRate}
               onChange={(e) => setFormData({ ...formData, hourlyRate: e.target.value })}
               className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-900"
               min="0"
               step="0.01"
+              required
             />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/invoices/page.tsx` around lines 240 - 247, Add the required attribute to
the hourly rate input element so the form won't submit with an empty rate;
update the <input> rendered for formData.hourlyRate (the element using
onChange={(e) => setFormData({ ...formData, hourlyRate: e.target.value })}) to
include required, and confirm any client-side validation/submit logic treats
hourlyRate as present (and, if needed, converts the string to a number before
sending to the backend).
.github/ISSUES/09-invoice-features.md (1)

1-14: Consider splitting this broad issue into smaller, focused issues.

This issue covers multiple distinct features (numbering, currency, templates, emailing) that could each be substantial implementations. Consider breaking into separate issues for better tracking and incremental delivery. However, the current structure is acceptable if the team prefers to track related features together.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/ISSUES/09-invoice-features.md around lines 1 - 14, The issue is too
broad—split "Improve invoice features (unique numbers, currency, templates,
emailing)" into smaller, trackable issues corresponding to each acceptance
criterion (e.g., "Invoice numbering scheme", "Currency support & formatting",
"Invoice templates & PDF export", "Server-side email sending with retry/audit",
"Tests for generation & email") or create these as linked sub-tasks under the
main issue; for each new issue include the specific acceptance criteria,
configurable options (prefix/counter/timestamp for numbering), UI/UX scope
(currency selector, template selection), backend responsibilities (email
provider, retry policy, audit logging), and test scope (integration/E2E) so work
can be implemented and reviewed incrementally using the existing Title and
Acceptance criteria as identifiers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/create_issues.sh:
- Line 1: The shebang in .github/create_issues.sh is malformed ("#/usr/bin/env
bash"); update the interpreter directive to a valid shebang by replacing that
line with the correct form (#!/usr/bin/env bash) so the script runs with bash as
intended and is recognized by the kernel.
- Line 14: Fix the test expression spacing bug: in the conditional that compares
the loop variable f to README.md (using ISSUES_DIR and f), add spaces after the
opening bracket and before the closing bracket so the shell builtin test is used
correctly (i.e., change the malformed ["$f" = "$ISSUES_DIR/README.md"] form to
the properly spaced [ "$f" = "$ISSUES_DIR/README.md" ] and keep the trailing &&
continue).

In @.github/ISSUES/README.md:
- Line 3: The README sentence uses "gh" CLI but writes the product name as
"GitHub" incorrectly capitalized; update the user-facing text in the sentence
"This folder contains issue drafts used by `.github/create_issues.sh` to create
GitHub issues via the `gh` CLI." to use "GitHub" (capital H) consistently so it
reads "...to create GitHub issues via the `gh` CLI." ensuring the product name
is correctly capitalized wherever referenced.

In `@app/api/invoices/`[id]/route.ts:
- Line 55: The calculation uses the client's current rate
(invoice.client.hourlyRate) instead of the persisted invoice rate, causing
historical invoices to show incorrect amounts; update the multiplication to use
invoice.hourlyRate so the amount is computed as hours *
Number(invoice.hourlyRate), ensuring you reference the existing invoice object
and preserve numeric conversion/typing the same way as before.

In `@app/api/invoices/route.ts`:
- Around line 90-93: Replace the permissive coercion used to compute rate (the
ternary that reads hourlyRate and falls back to client.hourlyRate) with strict
validation: if an override hourlyRate is provided, parse it to a number and
ensure Number.isFinite(parsed) and parsed >= 0, otherwise return a 400
bad-request; only then set rate to the parsed override, and if no valid override
use Number(client.hourlyRate) (also ensure client.hourlyRate is
finite/non-negative before persisting or billing). Target the rate assignment
and request-validation logic in the invoice route handler that references
hourlyRate and client.hourlyRate.

In `@prisma/schema.prisma`:
- Around line 29-31: Add a proper migration to reconcile the Prisma schema
changes for the Client model: create a migration that converts the existing
hourlyRate column from DOUBLE PRECISION to NUMERIC(10,2) (rounding or casting
existing values), sets its default to 0, and adds the updatedAt TIMESTAMP(3) NOT
NULL column with DEFAULT CURRENT_TIMESTAMP to implement `@updatedAt` behavior;
update the migration SQL to perform the ALTER TABLE for "Client" altering
"hourlyRate" TYPE NUMERIC(10,2) and SET DEFAULT 0 and adding the "updatedAt"
column so the database matches the Prisma model (Client.hourlyRate and
Client.updatedAt) before deploying.

---

Outside diff comments:
In `@app/api/invoices/route.ts`:
- Around line 64-67: Validate the incoming startDate and endDate before
constructing Date objects and querying: parse and ensure both startDate and
endDate produce valid Dates (not NaN) and that startDate <= endDate; if either
date is invalid or the range is reversed, return a 400 response early. Update
the logic around the creation of from and to (the variables from, to and the
inputs startDate, endDate) to perform these checks and short-circuit with a
clear 400 error message instead of proceeding to setHours and querying the DB.
- Around line 40-44: Move the request body parsing and required-field validation
into the try block so JSON parse errors are caught and return a 400;
specifically, stop calling await req.json() at the top-level and instead call it
inside the try that wraps the handler, then destructure clientId, startDate,
endDate, hourlyRate from that parsed body and perform the existing
required-field check (the block that currently returns Response.json({ error:
'Missing required fields' }, { status: 400 })); ensure any JSON parse exceptions
are caught and mapped to a 400 response before other error handling.

In `@app/invoices/page.tsx`:
- Line 96: The fetch call in page.tsx uses a relative URL 'api/clients' which is
inconsistent with other calls using absolute paths; update the fetch invocation
that assigns to res (res = await fetch('api/clients')) to use an absolute path
'/api/clients' so it matches the other calls (e.g., the fetches for
'/api/invoices') and prevents base-path issues as tracked by Issue `#15`.
- Line 111: The fetch call in page.tsx uses a relative path 'api/invoices' which
is inconsistent with other calls; update the fetch invocation (the line creating
const res via fetch) to use an absolute path '/api/invoices' so it matches the
clients fetch pattern and avoids routing issues.

---

Nitpick comments:
In @.github/ISSUES/09-invoice-features.md:
- Around line 1-14: The issue is too broad—split "Improve invoice features
(unique numbers, currency, templates, emailing)" into smaller, trackable issues
corresponding to each acceptance criterion (e.g., "Invoice numbering scheme",
"Currency support & formatting", "Invoice templates & PDF export", "Server-side
email sending with retry/audit", "Tests for generation & email") or create these
as linked sub-tasks under the main issue; for each new issue include the
specific acceptance criteria, configurable options (prefix/counter/timestamp for
numbering), UI/UX scope (currency selector, template selection), backend
responsibilities (email provider, retry policy, audit logging), and test scope
(integration/E2E) so work can be implemented and reviewed incrementally using
the existing Title and Acceptance criteria as identifiers.

In @.github/ISSUES/22-harden-jest-setup.md:
- Line 9: Replace the vague line "Ensure CI runs tests with isolated
environment." with a concrete CI test command and isolation strategy so it's
testable—for example, specify the exact test runner flag or command to use
(e.g., run tests in CI using "jest --runInBand" or "jest --maxWorkers=50%" or
invoke "npm test -- --runInBand"), and mention the CI job/step that should
execute it (the CI test step that runs the test command), so the requirement
names the command/flag and the job that enforces worker/process isolation.

In `@app/invoices/page.tsx`:
- Around line 240-247: Add the required attribute to the hourly rate input
element so the form won't submit with an empty rate; update the <input> rendered
for formData.hourlyRate (the element using onChange={(e) => setFormData({
...formData, hourlyRate: e.target.value })}) to include required, and confirm
any client-side validation/submit logic treats hourlyRate as present (and, if
needed, converts the string to a number before sending to the backend).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 873ed70e-1311-482f-a657-c81498edb3ef

📥 Commits

Reviewing files that changed from the base of the PR and between dc72b07 and 75567b6.

📒 Files selected for processing (33)
  • .github/ISSUES/01-unify-env.md
  • .github/ISSUES/02-harden-nextauth.md
  • .github/ISSUES/03-rate-limiting.md
  • .github/ISSUES/04-centralized-validation.md
  • .github/ISSUES/05-standardize-getserversession.md
  • .github/ISSUES/06-prisma-connection-safety.md
  • .github/ISSUES/07-persist-timer.md
  • .github/ISSUES/08-manual-time-entries.md
  • .github/ISSUES/09-invoice-features.md
  • .github/ISSUES/10-timezone-handling.md
  • .github/ISSUES/11-accessibility-improvements.md
  • .github/ISSUES/12-offline-ux-optimistic.md
  • .github/ISSUES/13-fix-prisma-ownership.md
  • .github/ISSUES/14-centralize-types.md
  • .github/ISSUES/15-normalize-api-fetch-urls.md
  • .github/ISSUES/16-structured-logging.md
  • .github/ISSUES/17-disable-prisma-query-logging.md
  • .github/ISSUES/18-add-lint-precommit-hooks.md
  • .github/ISSUES/19-integration-api-tests.md
  • .github/ISSUES/20-replace-fetch-with-msw.md
  • .github/ISSUES/21-add-e2e-tests.md
  • .github/ISSUES/22-harden-jest-setup.md
  • .github/ISSUES/23-expand-api-route-tests.md
  • .github/ISSUES/README.md
  • .github/create_issues.sh
  • __tests__/InvoicesPage.test.tsx
  • app/api/dashboard/route.ts
  • app/api/invoices/[id]/route.ts
  • app/api/invoices/route.ts
  • app/invoices/page.tsx
  • app/timer/page.tsx
  • prisma/migrations/20260330120000_add_invoice_hourly_rate/migration.sql
  • prisma/schema.prisma

Comment thread .github/create_issues.sh Outdated
Comment thread .github/create_issues.sh Outdated
Comment thread .github/ISSUES/README.md Outdated
Comment thread app/api/invoices/[id]/route.ts Outdated
Comment thread app/api/invoices/route.ts Outdated
Comment thread prisma/schema.prisma
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/invoices/page.tsx (3)

207-211: ⚠️ Potential issue | 🟠 Major

Add optional chaining on clients to prevent the pipeline TypeError.

The pipeline failure Cannot read properties of undefined (reading 'map') occurs because clientsData?.clients.map only guards against clientsData being nullish, not clientsData.clients. If the API response structure differs (e.g., error response), accessing .map on undefined throws.

🔧 Proposed fix
-              {clientsData?.clients.map((client: Client) => (
+              {clientsData?.clients?.map((client: Client) => (
                 <option key={client.id} value={client.id}>
                   {client.name} (${client.hourlyRate}/hr)
                 </option>
               ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/invoices/page.tsx` around lines 207 - 211, The JSX currently calls
clientsData?.clients.map which can throw if clientsData.clients is undefined;
update the mapping to safely handle a missing clients array by using optional
chaining or a fallback array (e.g., clientsData?.clients?.map(...) or
(clientsData?.clients ?? []).map(...)) where the option elements are rendered
(referencing clientsData, clients, map and the Client mapping that produces the
<option> elements) so the pipeline no longer errors when clients is undefined.

283-284: ⚠️ Potential issue | 🟠 Major

Same optional chaining issue with invoicesData?.invoices.

Accessing .length on undefined will throw if invoices property is missing from the response.

🔧 Proposed fix
-              {invoicesData?.invoices.length > 0 ? (
+              {(invoicesData?.invoices?.length ?? 0) > 0 ? (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/invoices/page.tsx` around lines 283 - 284, The JSX uses
invoicesData?.invoices.length which can throw if invoices is undefined; update
the conditional and mapping to guard the invoices array (e.g., check
invoicesData?.invoices?.length > 0 or use a fallback empty array) and use
invoicesData?.invoices?.map when mapping; locate the conditional around
invoicesData and the map call that iterates Invoice to apply the null-safe
checks or fallback so .length and .map are never called on undefined.

7-11: ⚠️ Potential issue | 🟡 Minor

Type mismatch: hourlyRate is string from API, not number.

Per the relevant code snippets, Prisma's Decimal type serializes to a string when passed through JSON.stringify(). The API returns hourlyRate as "50.00" (string), not 50 (number). While string interpolation and String() calls still work, this mismatch can cause subtle issues with numeric comparisons or arithmetic operations.

🔧 Proposed fix
 interface Client {
   id: string;
   name: string;
-  hourlyRate: number;
+  hourlyRate: string;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/invoices/page.tsx` around lines 7 - 11, The Client interface declares
hourlyRate as a number but the API/Prisma returns it as a string (e.g. "50.00"),
so update the interface declaration (interface Client) to hourlyRate: string and
locate all places that perform numeric operations on Client.hourlyRate and
convert explicitly (e.g., parseFloat(client.hourlyRate) or use Decimal parsing)
before comparisons or arithmetic; ensure any JSON serialization/typing assumes
the string form to avoid runtime type errors.
🧹 Nitpick comments (1)
prisma/migrations/20260330140000_update_client_hourlyrate_and_updatedAt/migration.sql (1)

16-20: Backfill statement is a no-op after adding NOT NULL DEFAULT column.

Line 17 adds updatedAt with NOT NULL DEFAULT CURRENT_TIMESTAMP, which means all rows immediately get a non-null value. The subsequent WHERE "updatedAt" IS NULL condition on line 20 will never match any rows.

This is harmless but creates confusion about the migration's intent. Consider either:

  • Removing the redundant UPDATE, or
  • Changing line 17 to allow NULLs initially, then backfill, then add NOT NULL constraint
♻️ Cleaner approach if backfill is intended
--- If you want to backfill from createdAt instead of CURRENT_TIMESTAMP:
-- 5) Add updatedAt column (allow NULLs initially)
-ALTER TABLE "Client" ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
+ALTER TABLE "Client" ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3);

-- 6) Backfill from createdAt
-UPDATE "Client" SET "updatedAt" = COALESCE("updatedAt", "createdAt") WHERE "updatedAt" IS NULL;
+UPDATE "Client" SET "updatedAt" = "createdAt" WHERE "updatedAt" IS NULL;
+
+-- 7) Add NOT NULL constraint
+ALTER TABLE "Client" ALTER COLUMN "updatedAt" SET NOT NULL;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@prisma/migrations/20260330140000_update_client_hourlyrate_and_updatedAt/migration.sql`
around lines 16 - 20, The UPDATE backfill is redundant because ALTER TABLE
"Client" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
already assigns non-null values; either remove the UPDATE statement entirely, or
if you intend to preserve existing createdAt values, change the migration to: 1)
ADD COLUMN "updatedAt" TIMESTAMP(3) NULL (no DEFAULT), 2) UPDATE "Client" SET
"updatedAt" = COALESCE("updatedAt", "createdAt") to backfill from createdAt,
then 3) ALTER TABLE "Client" ALTER COLUMN "updatedAt" SET NOT NULL and
(optionally) ADD DEFAULT CURRENT_TIMESTAMP — locate the ALTER TABLE and UPDATE
statements referencing the "Client" table and "updatedAt" column and apply one
of these two approaches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/invoices/page.tsx`:
- Around line 194-202: The client selection onChange handler uses
clientsData?.clients.find(...) which can throw if clients is undefined; change
the lookup to safely handle missing clients (e.g., use (clientsData?.clients ??
[]).find(...) or clientsData?.clients?.find(...)) before creating client and
then call setFormData to update clientId and hourlyRate (keep the existing
setFormData logic and Client type check).

---

Outside diff comments:
In `@app/invoices/page.tsx`:
- Around line 207-211: The JSX currently calls clientsData?.clients.map which
can throw if clientsData.clients is undefined; update the mapping to safely
handle a missing clients array by using optional chaining or a fallback array
(e.g., clientsData?.clients?.map(...) or (clientsData?.clients ?? []).map(...))
where the option elements are rendered (referencing clientsData, clients, map
and the Client mapping that produces the <option> elements) so the pipeline no
longer errors when clients is undefined.
- Around line 283-284: The JSX uses invoicesData?.invoices.length which can
throw if invoices is undefined; update the conditional and mapping to guard the
invoices array (e.g., check invoicesData?.invoices?.length > 0 or use a fallback
empty array) and use invoicesData?.invoices?.map when mapping; locate the
conditional around invoicesData and the map call that iterates Invoice to apply
the null-safe checks or fallback so .length and .map are never called on
undefined.
- Around line 7-11: The Client interface declares hourlyRate as a number but the
API/Prisma returns it as a string (e.g. "50.00"), so update the interface
declaration (interface Client) to hourlyRate: string and locate all places that
perform numeric operations on Client.hourlyRate and convert explicitly (e.g.,
parseFloat(client.hourlyRate) or use Decimal parsing) before comparisons or
arithmetic; ensure any JSON serialization/typing assumes the string form to
avoid runtime type errors.

---

Nitpick comments:
In
`@prisma/migrations/20260330140000_update_client_hourlyrate_and_updatedAt/migration.sql`:
- Around line 16-20: The UPDATE backfill is redundant because ALTER TABLE
"Client" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
already assigns non-null values; either remove the UPDATE statement entirely, or
if you intend to preserve existing createdAt values, change the migration to: 1)
ADD COLUMN "updatedAt" TIMESTAMP(3) NULL (no DEFAULT), 2) UPDATE "Client" SET
"updatedAt" = COALESCE("updatedAt", "createdAt") to backfill from createdAt,
then 3) ALTER TABLE "Client" ALTER COLUMN "updatedAt" SET NOT NULL and
(optionally) ADD DEFAULT CURRENT_TIMESTAMP — locate the ALTER TABLE and UPDATE
statements referencing the "Client" table and "updatedAt" column and apply one
of these two approaches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b931376-8429-4daa-bc76-e65d9f9d637d

📥 Commits

Reviewing files that changed from the base of the PR and between 75567b6 and 1e0b9f5.

📒 Files selected for processing (5)
  • app/api/invoices/[id]/route.ts
  • app/api/invoices/route.ts
  • app/invoices/page.tsx
  • prisma/migrations/20260330140000_update_client_hourlyrate_and_updatedAt/migration.sql
  • prisma/migrations/20260330173921_update_client_hourlyrate_and_updated_at/migration.sql
✅ Files skipped from review due to trivial changes (1)
  • app/api/invoices/[id]/route.ts

Comment thread app/invoices/page.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/InvoicesPage.test.tsx`:
- Around line 113-120: The test currently guards filling date inputs with an if
which hides missing elements; change to fail fast by asserting the expected
inputs exist before using them: add an assertion on dateInputs (e.g.,
expect(dateInputs.length).toBeGreaterThanOrEqual(2) or toBe(2)) right after
querying, then proceed to call fireEvent.change on dateInputs[0] and
dateInputs[1]; reference variables/mymbols: dateInputs, fireEvent.change, and
the existing today variable so the test clearly fails when the required date
inputs are not present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1afdb2d-98ba-46fb-809d-0e7a8a82c1ff

📥 Commits

Reviewing files that changed from the base of the PR and between 1e0b9f5 and 0f41252.

📒 Files selected for processing (2)
  • __tests__/InvoicesPage.test.tsx
  • app/invoices/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/invoices/page.tsx

Comment on lines +113 to +120
// fill required date inputs (native validation prevents submit otherwise)
const { container } = rendered;
const dateInputs = container.querySelectorAll('input[type="date"]') as NodeListOf<HTMLInputElement>;
const today = new Date().toISOString().slice(0, 10);
if (dateInputs.length >= 2) {
fireEvent.change(dateInputs[0], { target: { value: today } });
fireEvent.change(dateInputs[1], { target: { value: today } });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fail fast when required date inputs are missing.

The conditional guard can defer failures to later assertions, making the cause less clear. Assert the expected inputs exist before filling them.

💡 Suggested test-hardening diff
-    if (dateInputs.length >= 2) {
-      fireEvent.change(dateInputs[0], { target: { value: today } });
-      fireEvent.change(dateInputs[1], { target: { value: today } });
-    }
+    expect(dateInputs).toHaveLength(2);
+    fireEvent.change(dateInputs[0], { target: { value: today } });
+    fireEvent.change(dateInputs[1], { target: { value: today } });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// fill required date inputs (native validation prevents submit otherwise)
const { container } = rendered;
const dateInputs = container.querySelectorAll('input[type="date"]') as NodeListOf<HTMLInputElement>;
const today = new Date().toISOString().slice(0, 10);
if (dateInputs.length >= 2) {
fireEvent.change(dateInputs[0], { target: { value: today } });
fireEvent.change(dateInputs[1], { target: { value: today } });
}
// fill required date inputs (native validation prevents submit otherwise)
const { container } = rendered;
const dateInputs = container.querySelectorAll('input[type="date"]') as NodeListOf<HTMLInputElement>;
const today = new Date().toISOString().slice(0, 10);
expect(dateInputs).toHaveLength(2);
fireEvent.change(dateInputs[0], { target: { value: today } });
fireEvent.change(dateInputs[1], { target: { value: today } });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/InvoicesPage.test.tsx` around lines 113 - 120, The test currently
guards filling date inputs with an if which hides missing elements; change to
fail fast by asserting the expected inputs exist before using them: add an
assertion on dateInputs (e.g.,
expect(dateInputs.length).toBeGreaterThanOrEqual(2) or toBe(2)) right after
querying, then proceed to call fireEvent.change on dateInputs[0] and
dateInputs[1]; reference variables/mymbols: dateInputs, fireEvent.change, and
the existing today variable so the test clearly fails when the required date
inputs are not present.

@grmbyrn grmbyrn merged commit 6ed96e2 into main Mar 30, 2026
4 checks passed
@grmbyrn grmbyrn deleted the wl-44 branch March 30, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant