Workflow-App/Workflow/app/api/shareholder-changes/parties/route.ts
sjcoetzeesj-coder 11d0178ab1 feat: commit Workflow app source; move CIPC credentials to env
Adds the full Next.js app (app/, components/, lib/, scripts/, assets/,
docs/, Dockerfile, compose, configs) to version control.

Security:
- CIPC BizPortal customer-code logins (Login 1 + Login 3) now read
  CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE /
  CIPC_LOGIN3_PASSWORD from .env.local instead of source.
- BizPortal personal login in apply-bizportal route reads
  BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD.
- Added .env.example as the committed template.

.gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright
storage_state.json (live CIPC cookies), scratch dirs, client PDFs and
debug screenshots.
2026-10-18 15:21:45 +02:00

118 lines
No EOL
4 KiB
TypeScript

// app/api/shareholder-changes/parties/route.ts
// GET /api/shareholder-changes/parties?company_id=<id>
//
// Returns the two lists the wizard offers when picking the parties to a share
// transfer. There is no shareholder register yet, so a shareholder is somebody
// already on file — either a person who is (also) a director, or a company:
//
// { items: {
// directors: [{ id, name, surname, email, id_no, linked }] // linked = director of company_id
// companies: [{ id, name, email, enterprise_no, authorised_shares }]
// } }
//
// The company being resolved is EXCLUDED from the company list: a company
// cannot be a shareholder in itself, and offering it invites a pointless
// round-trip through validation.
//
// Every director is returned, not only those linked to `company_id`. A
// shareholder of a private company need not serve on its board, and since
// there is no register of who holds what we cannot pre-filter reliably. The
// `linked` flag lets the UI surface the obvious candidates first without
// hiding anyone.
import { NextRequest, NextResponse } from "next/server";
import { fetchCollection, type PBRecord } from "@/lib/pb";
import { COL } from "@/lib/shareholder-changes";
interface DirectorRec extends PBRecord {
first_names?: string;
surname?: string;
email?: string | null;
id_no?: string | null;
telephone?: string | null;
}
interface CompanyRec extends PBRecord {
enterprise_name?: string;
enterprise_no?: string;
company_email?: string | null;
authorised_shares?: number | null;
}
interface JunctionRec extends PBRecord {
director?: string;
}
export async function GET(req: NextRequest) {
const url = new URL(req.url);
const companyId = url.searchParams.get("company_id") ?? "";
try {
// Which directors are on the board of the company being resolved. Used only
// as a "likely current shareholder" hint, so a failure here must not break
// the picker — degrade to "nobody flagged".
const linkedDirectorIds = new Set<string>();
if (companyId) {
try {
const junction = await fetchCollection<JunctionRec>(COL.companyDirectors, {
filter: `company="${companyId}"`,
perPage: 500,
});
for (const j of junction.items) {
if (j.director) linkedDirectorIds.add(j.director);
}
} catch (err) {
console.error(
"[shareholder-changes] could not load company directors:",
err instanceof Error ? err.message : err
);
}
}
const [directors, companies] = await Promise.all([
fetchCollection<DirectorRec>(COL.directors, { perPage: 500, sort: "surname" }),
fetchCollection<CompanyRec>(COL.companies, {
perPage: 500,
sort: "enterprise_name",
}),
]);
// A company with no name is a data-quality artefact, not a pickable party.
const companyItems = companies.items
.filter((c) => (c.enterprise_name ?? "").trim() !== "")
.filter((c) => c.id !== companyId)
.map((c) => ({
id: c.id,
name: (c.enterprise_name ?? "").trim(),
email: (c.company_email ?? "").trim() || null,
enterprise_no: c.enterprise_no ?? null,
authorised_shares: c.authorised_shares ?? null,
}));
const directorItems = directors.items
.filter((d) => [d.first_names, d.surname].some((v) => (v ?? "").trim() !== ""))
.map((d) => ({
id: d.id,
name: (d.first_names ?? "").trim(),
surname: (d.surname ?? "").trim(),
email: (d.email ?? "").trim() || null,
id_no: d.id_no ?? null,
// Carried into the signature block ("Cell - 0827985409"), matching how
// section 4 identifies its signers.
telephone: (d.telephone ?? "").trim() || null,
linked: linkedDirectorIds.has(d.id),
}));
return NextResponse.json({
items: {
directors: directorItems,
companies: companyItems,
},
});
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "Unexpected error" },
{ status: 500 }
);
}
}