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.
118 lines
No EOL
4 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
} |