Offer Letter and Renewal System
Current implementation: v0.4.0 Last updated: 2026-07-14
Offer Letter and Renewal System is an internal offer-letter approval and renewal workflow system built with Next.js App Router + Prisma + Postgres.
Specs
docs/specs/ERS-v0.1-spec.mddocs/specs/ERS-v0.2-build-pack.mddocs/specs/ERS-v0.3-runnable-pack.mddocs/USER-MANUAL.md
Stack
- Next.js (App Router, TypeScript)
- Prisma + PostgreSQL
- Local filesystem uploads (
UPLOAD_BASE_DIR) - JWT cookie auth (email + password)
- Resend-backed durable email outbox
- PDF signature stamping with
pdf-lib - OpenAI-assisted offer extraction, contract variation review, and signature placement suggestions
- Server-rendered senior-management compensation analytics with table and chart views
Environment
Set values in .env (and in /etc/ers-prod.env if you run via ers-prod.service):
DATABASE_URLAUTH_JWT_SECRETAUTH_COOKIE_NAMEUPLOAD_BASE_DIRUPLOAD_MAX_MBRESEND_API_KEYEMAIL_FROMNEXT_PUBLIC_APP_URL(preferred public app base URL, e.g.https://review.r32a.com)NEXTAUTH_URLorAPP_URL(supported public URL fallbacks)OPENAI_API_KEY(optional, for offer-letter extraction and AI contract review)OPENAI_MODEL(optional, defaultgpt-4.1-mini)INTERNAL_API_SECRET
UPLOAD_BASE_DIR must point to a persistent directory such as /var/app/ers_uploads.
Do not use /tmp or any other temporary directory in production, or uploaded offer letters and CEO signatures can disappear after service restarts.
Install
npm install
npm run prisma:generate
Database Setup
Apply migrations in production without running the demo seed:
npx prisma migrate deploy
For an empty local development database only:
npm run prisma:migrate
npm run db:seed
npm run db:seed is destructive: it deletes existing application data and creates demo
@ers.local users and sample cases. Never run it against production.
Run
npm run dev
Open http://localhost:3000.
Core URLs
/login/dashboard(CEO-only management analytics landing page)/hr/renewals/manager/requests/ceo/renewals/ceo/signature/admin/settings/admin/system-health/admin/audit/admin/users/help/notifications/api/version(public build identity; no secrets)
Navigation and case review
- Desktop uses grouped role navigation; mobile uses an accessible navigation drawer.
- Every authenticated page has a breadcrumb and a route-specific page header.
- Case lists can be filtered by employee, status, grade, and department. Opening and returning from a case preserves the filtered list URL.
- Long case pages provide sticky section navigation for overview, evidence, contract AI, decisions, manager input, and documents.
- The user menu contains role-specific Help, About ERS, and Sign out.
Release versioning
ERS uses Semantic Versioning. package.json is the single editable version source. Before local
development and production builds, scripts/generate-build-info.mjs writes
lib/generated/build-info.ts with the version, Git SHA, build timestamp, environment, and dirty
worktree state. The resulting identity appears on login, in the authenticated sidebar, in About
ERS, on CEO System Health, and at /api/version.
For a release:
npm version <major|minor|patch> --no-git-tag-version
npm run build
Update CHANGELOG.md in the same change. Release versions cannot be edited from Admin Settings.
Create the Git commit and tag only from a reviewed clean worktree.
Supported Case Types
Renewal: requires offer letter, performance snapshot, and manager justification before CEO approval.New Hire: requires offer letter only; manager justification and performance snapshot are not required.
Offer Letter Autofill
- HR can upload an offer-letter PDF on
/hr/renewals/newand clickExtract & Prefill. - If
OPENAI_API_KEYis configured, extraction uses OpenAI structured parsing with heuristic fallback. - Extraction prepares an editable review; it does not create an employee until HR submits the completed profile.
- Employee name, department, role title, current grade, applicable grade, contract dates, salary,
currency, and the offer PDF are required. Placeholder values such as
Unknown DepartmentandUNASSIGNEDare rejected. - Foreign contractors without an internal grade must use an explicit category such as
FOREIGN CONTRACTOR - NO INTERNAL GRADE. - Interview and performance evaluation forms are rejected when uploaded as
OFFER_LETTER. - Uploading
OFFER_LETTERin a case detail also auto-maps extracted fields toRenewalCase.
Performance Snapshot
Renewal cases require a real performance snapshot before CEO approval. The snapshot contains:
- review period (required)
- overall score (optional)
- HR notes (optional)
- manager comments (optional)
New-hire cases use the internal __NEW_HIRE__ marker and do not require performance or manager
inputs. A submitted manager justification does not make a renewal ready by itself; the latest offer
letter and performance snapshot must also be present.
Before manager outreach or CEO review, ERS rechecks the employee profile, offer PDF, dates, positive salary values, three-letter currency code, and applicable grade.
CEO Decision Brief
Each CEO case review places a decision brief directly after the case overview. It shows:
- proposed monthly and annualized salary, salary movement, and contract term
- same-grade percentile, P25, median, P75, range, and confidence based on active peer records
- narrower same-grade plus department and same-grade plus exact-role cohorts when available
- salary differences from the grade median and performance position among scored grade peers
- manager recommendation, grade movement, required evidence, and AI contract variation risk
- an advisory
Aligned,Needs Review,Caution, orIncompleteposture with explicit reasons
The reviewed employee is excluded from the peer population. Cohorts below five peers are marked insufficient and are not classified on the curve. The posture is decision support only and never approves or rejects a case automatically.
Senior Management Analytics
CEO users land on /dashboard; HR users land on /hr/renewals. The CEO-only dashboard provides:
- Executive Overview, Grade Curve, Increases & Outliers, and Budget & Risk tabs
- filters for dates, case type, department, normalized grade, manager, status, curve position, and increase
- one effective compensation record per active employee as of the selected date
- current, projected, committed, pending, annualized, 12-month, and finite contract-term impact
- configurable large-increase and high-percentile alerts
- table and SVG box-plot chart modes on the Grade Curve tab
- clickable proposed-salary dots with employee details and renewal links
- CEO-only, audited, spreadsheet-safe CSV export from
/api/analytics/export
Curve bands use the target grade and midrank percentiles. P25-P75 is ON_CURVE; values outside
that range are classified below or above. Peer groups under five are not classified, and groups of
five to nine are marked limited. Equivalent grade labels such as 13, 13 Executive, and
Grade 13 are normalized for grouping without merging separate G scales such as G5.
The dashboard defaults to a combined portfolio view, and the Case Type filter can isolate
Renewal or New Hire offers so each curve can be reviewed separately.
Salary coverage is shown as confirmed, inferred, missing, or conversion-failed. Future-effective
approved compensation is excluded from the current curve. Open-ended 9999 contract dates are
excluded from finite contract-term totals and remain included in the 12-month view.
AI Contract Review
HR and CEO can run AI contract review against the latest offer letter. The review stores:
- suggested signature page and PDF coordinates with confidence and rationale
- comparison against a previous signed contract baseline
- variation risk, clause differences, missing clauses, warnings, and recommendations
- model, prompt version, status, errors, and actor audit information
The PDF signer can load a suggestion into its normal placement queue. AI output is advisory; the CEO reviews placement and contract differences before signing.
Email Dispatch
Outbound emails are queued in EmailOutbox. Dispatch endpoint:
POST /api/internal/email/dispatch- Header:
x-internal-secret: <INTERNAL_API_SECRET>
Recommended VPS cron (every minute):
* * * * * curl -X POST https://your-domain/api/internal/email/dispatch -H "x-internal-secret: your-secret"
Notes
- HR/Manager cannot modify locked cases (
locked_atset by CEO decision). - Renewal-case CEO approval requires latest offer letter, performance snapshot, and manager justification.
- New-hire CEO approval requires latest offer letter only.
- Signature stamping is enabled via
/api/renewals/:id/sign-pdfwith multi-placement payload. - Signing is allowed only when case status is
APPROVED. - After signing, case status becomes
SIGNED(closed/read-only) and HR users are notified via outbox email. - If the employee or candidate declines after CEO signature, CEO can mark the signed case
DECLINED; ERS voids the linked approved compensation, removes it from default analytics, and keeps the signed document and audit trail. - If a latest document row exists but its stored file is missing, HR and CEO can re-upload the latest missing file from the case detail page to restore downloads or signing.
/ceo/signatureshows the current signature preview and allows the CEO to re-upload the PNG if the stored file is missing.- HR and CEO can delete wrong supporting documents or an eligible pre-approval case. Approved, rejected, signed, and compensation-linked cases remain protected.
- Confirmed salary data corrections must record before/after values in
AuditLog. Corrections to a renewal's current salary do not alter its approved compensation record or signed document. - Voided compensation rows are retained for audit but excluded from salary curves and peer stats.
Verification
npm run test:analytics
npm run typecheck
npm run lint
npm run build
npx prisma migrate status
npm run lint currently reports only the existing no-img-element optimization warnings in the
PDF signer and CEO signature preview.