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
170 changes: 170 additions & 0 deletions server/regulatory.graph-importer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { afterEach, describe, expect, it } from "vitest";
import { randomUUID } from "node:crypto";
import { eq, inArray } from "drizzle-orm";
import { appRouter } from "./routers";
import { getDb } from "./db";
import type { TrpcContext } from "./_core/context";
import { assertDeclarationFormalitiesSatisfied } from "./routers/regulatory";
import {
declarations,
declarationFormalities,
ogaPermits,
regulatoryFormalities,
stakeholderMandates,
stakeholderRegistrations,
} from "../drizzle/schema";

function caller(id: number, role: "user" | "customs_officer" = "user") {
const context: TrpcContext = {
user: {
id,
openId: `regulatory-graph-${id}`,
name: `Regulatory Graph ${id}`,
email: `regulatory-graph-${id}@example.test`,
loginMethod: "test",
role,
createdAt: new Date(),
updatedAt: new Date(),
lastSignedIn: new Date(),
},
req: { method: "GET", headers: {}, cookies: {} } as TrpcContext["req"],
res: { clearCookie: () => undefined, cookie: () => undefined } as unknown as TrpcContext["res"],
};
return appRouter.createCaller(context);
}

async function database() {
const db = await getDb();
if (!db) throw new Error("Postgres is required for regulatory graph tests.");
return db;
}

const created = {
declarations: [] as number[],
formalities: [] as number[],
permits: [] as number[],
registrations: [] as number[],
mandates: [] as number[],
};

afterEach(async () => {
const db = await database();
if (created.declarations.length) {
await db.delete(declarationFormalities)
.where(inArray(declarationFormalities.declarationId, created.declarations));
await db.delete(ogaPermits).where(inArray(ogaPermits.declarationId, created.declarations));
await db.delete(declarations).where(inArray(declarations.id, created.declarations));
}
if (created.formalities.length) {
await db.delete(regulatoryFormalities).where(inArray(regulatoryFormalities.id, created.formalities));
}
if (created.registrations.length) {
await db.delete(stakeholderRegistrations).where(inArray(stakeholderRegistrations.id, created.registrations));
}
if (created.mandates.length) {
await db.delete(stakeholderMandates).where(inArray(stakeholderMandates.id, created.mandates));
}
created.declarations.length = 0;
created.formalities.length = 0;
created.permits.length = 0;
created.registrations.length = 0;
created.mandates.length = 0;
});

describe.sequential("regulatory clearance graph importer identity", () => {
it("uses the declaration principal for principal, agent, and customs previews", async () => {
const db = await database();
const now = new Date();
const [declaration] = await db.insert(declarations).values({
declarationNumber: `GRAPH-${randomUUID().slice(0, 20)}`,
ucr: `GRAPH-UCR-${randomUUID()}`,
traderId: 9,
principalId: 1,
actingAgentId: 2,
declarationType: "import",
hsCode: "847130",
countryOfOrigin: "GH",
countryOfDestination: "NG",
numberOfPackages: 5,
createdAt: now,
submittedAt: now,
}).returning();
created.declarations.push(declaration.id);

const [formality] = await db.insert(regulatoryFormalities).values({
hsCodePrefix: "8471",
origin: "GH",
destination: "NG",
regime: "import",
agencyCode: "OGA-GRAPH",
agencyName: "Graph Agency",
permitType: "GRAPH-PERMIT",
requiredQuantity: "5",
legalInstrument: "Instrument GRAPH",
validFrom: new Date(now.getTime() - 60_000),
createdBy: 4,
}).returning();
created.formalities.push(formality.id);

const [permit] = await db.insert(ogaPermits).values({
declarationId: declaration.id,
agencyCode: "OGA-GRAPH",
agencyName: "Graph Agency",
permitType: "GRAPH-PERMIT",
status: "approved",
hsCode: "8471",
consigneeId: 1,
permittedQuantity: "5",
validFrom: new Date(now.getTime() - 60_000),
}).returning();
created.permits.push(permit.id);

const [mandate] = await db.insert(stakeholderMandates).values({
referenceNumber: `GRAPH-MANDATE-${randomUUID().slice(0, 12)}`,
principalUserId: 1,
agentUserId: 2,
validFrom: new Date(now.getTime() - 60_000),
validUntil: new Date(now.getTime() + 60_000),
}).returning();
created.mandates.push(mandate.id);

const [registration] = await db.insert(stakeholderRegistrations).values({
referenceNumber: `GRAPH-AGENT-${randomUUID().slice(0, 12)}`,
userId: 2,
stakeholderType: "freight_forwarder",
organizationName: "Graph Agent",
country: "GH",
licenseExpiresAt: new Date(now.getTime() + 60_000),
status: "approved",
approvedBy: 4,
approvedAt: now,
}).returning();
created.registrations.push(registration.id);

const input = {
declarationId: declaration.id,
hsCode: declaration.hsCode!,
origin: declaration.countryOfOrigin!,
destination: declaration.countryOfDestination!,
regime: declaration.declarationType,
quantity: "5",
};
for (const preview of [
caller(1).regulatory.clearanceGraph(input),
caller(2).regulatory.clearanceGraph(input),
caller(4, "customs_officer").regulatory.clearanceGraph(input),
]) {
const graph = await preview;
expect(graph.blocking).toBe(false);
expect(graph.obligations).toMatchObject([{
satisfied: true,
blocking: false,
satisfiedByPermitId: permit.id,
}]);
}

await expect(assertDeclarationFormalitiesSatisfied(declaration.id)).resolves.toBeUndefined();
await expect(caller(3).regulatory.clearanceGraph(input))
.rejects.toMatchObject({ code: "FORBIDDEN" });
});
});
36 changes: 26 additions & 10 deletions server/routers/regulatory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,16 +604,32 @@ export const regulatoryRouter = router({
asAt: z.coerce.date().optional(),
declarationId: z.number().int().positive().optional(),
}))
.query(async ({ ctx, input }) => clearanceGraph({
hsCode: input.hsCode,
origin: input.origin,
destination: input.destination,
regime: input.regime,
quantity: input.quantity,
importerId: ctx.user.id,
declarationId: input.declarationId,
at: asDate(input.asAt),
})),
.query(async ({ ctx, input }) => {
let importerId = ctx.user.id;
if (input.declarationId) {
const db = await requireRegulatoryDb();
const [declaration] = await db.select().from(declarations)
.where(eq(declarations.id, input.declarationId))
.limit(1);
if (!declaration) {
throw new TRPCError({ code: "NOT_FOUND", message: "Declaration not found." });
}
if (ctx.user.role !== "admin" && ctx.user.role !== "customs_officer") {
await requireDeclarationActor(declaration, ctx.user);
}
importerId = declaration.principalId ?? declaration.traderId;
}
return clearanceGraph({
hsCode: input.hsCode,
origin: input.origin,
destination: input.destination,
regime: input.regime,
quantity: input.quantity,
importerId,
declarationId: input.declarationId,
at: asDate(input.asAt),
});
}),

allocateQuota: protectedProcedure
.input(z.object({ quotaId: z.number().int().positive(), declarationId: z.number().int().positive(), quantity: z.string().regex(/^\d+(\.\d{1,3})?$/) }))
Expand Down