mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
test(freight-api): cover poa delegation and fayda identity gates
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
|
||||
import { CompaniesService } from "./companies.service";
|
||||
import { CompanyNationality, CompanyStatus } from "./entities/company.entity";
|
||||
import { ProfileType } from "./entities/company-profile.entity";
|
||||
import { POA_DELEGATION_FILE_KEY } from "../file-upload-settings/poa-delegation.constants";
|
||||
|
||||
/**
|
||||
* A person's identity is proved through Fayda: name, email, phone and address
|
||||
* come from the verified payload, not typed. Fayda's userinfo carries no
|
||||
* national ID number, so none is collected or derived here.
|
||||
*
|
||||
* - Ethiopian company: the owner (and its PoA, once named) is verified through
|
||||
* Fayda, and their details can't be edited afterwards.
|
||||
* - Foreign company: Fayda is an Ethiopian national ID, so the owner instead
|
||||
* supplies a typed passport number — required on its own, whether or not the
|
||||
* owner also completes a (purely optional) Fayda verification.
|
||||
*
|
||||
* The owner is NOT the general manager — GM is a separate, plain typed role
|
||||
* the portal offers a "same as owner" copy for, but it is never itself
|
||||
* Fayda-verified or gated on.
|
||||
*/
|
||||
|
||||
interface Ctx {
|
||||
attributes: Record<string, unknown>;
|
||||
files: { id: string; code: string; reviewStatus?: string | null }[];
|
||||
profileTypes: ProfileType[];
|
||||
status: CompanyStatus;
|
||||
nationality: CompanyNationality;
|
||||
verification: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const OWNER_VERIFIED = {
|
||||
ownerFaydaSub: "owner-sub",
|
||||
ownerFaydaVerifiedAt: "2026-07-01T00:00:00.000Z",
|
||||
ownerName: "Abebe Bikila",
|
||||
};
|
||||
|
||||
const POA_VERIFIED = {
|
||||
poaFaydaSub: "poa-sub",
|
||||
poaFaydaVerifiedAt: "2026-07-02T00:00:00.000Z",
|
||||
poaName: "Tirunesh Dibaba",
|
||||
poaEmail: "tirunesh@example.com",
|
||||
poaPhone: "+251911000000",
|
||||
};
|
||||
|
||||
const paper = () => ({
|
||||
id: "file-1",
|
||||
code: POA_DELEGATION_FILE_KEY,
|
||||
reviewStatus: null,
|
||||
});
|
||||
|
||||
function makeService(overrides: Partial<Ctx> = {}) {
|
||||
const ctx: Ctx = {
|
||||
attributes: {},
|
||||
files: [],
|
||||
profileTypes: [ProfileType.importer],
|
||||
status: CompanyStatus.Pending,
|
||||
nationality: CompanyNationality.Ethiopian,
|
||||
verification: {
|
||||
purpose: "VERIFY",
|
||||
verified: true,
|
||||
sub: "new-sub",
|
||||
fullName: "Haile Gebrselassie",
|
||||
email: "haile@example.com",
|
||||
phoneNumber: "+251922000000",
|
||||
address: "Addis Ababa",
|
||||
birthdate: "1973-04-18",
|
||||
gender: "Male",
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const company = () => ({
|
||||
id: "company-1",
|
||||
status: ctx.status,
|
||||
nationality: ctx.nationality,
|
||||
attributes: ctx.attributes,
|
||||
companyProfiles: ctx.profileTypes.map((type, i) => ({
|
||||
id: `profile-${i}`,
|
||||
type,
|
||||
})),
|
||||
type: "customer",
|
||||
});
|
||||
|
||||
const deps = {
|
||||
companiesRepo: {
|
||||
findById: jest.fn(async () => company()),
|
||||
update: jest.fn(async (_id: string, patch: Record<string, unknown>) => {
|
||||
if (patch.attributes)
|
||||
ctx.attributes = patch.attributes as Record<string, unknown>;
|
||||
return company();
|
||||
}),
|
||||
findByTin: jest.fn(async () => null),
|
||||
},
|
||||
companyProfilesRepo: {
|
||||
findByCompanyId: jest.fn(async () =>
|
||||
ctx.profileTypes.map((type, i) => ({ id: `profile-${i}`, type })),
|
||||
),
|
||||
findByType: jest.fn(async (_id: string, type: ProfileType) =>
|
||||
ctx.profileTypes.includes(type) ? { id: "existing", type } : null,
|
||||
),
|
||||
create: jest.fn(async (row: Record<string, unknown>) => ({
|
||||
id: "new",
|
||||
...row,
|
||||
})),
|
||||
},
|
||||
changeRequestRepo: {
|
||||
findPendingByCompanyId: jest.fn(async () => null),
|
||||
findByCompanyId: jest.fn(async () => []),
|
||||
create: jest.fn(async (row: Record<string, unknown>) => ({
|
||||
id: "cr-1",
|
||||
...row,
|
||||
})),
|
||||
update: jest.fn(async () => ({ id: "cr-1" })),
|
||||
},
|
||||
profilesRepo: {
|
||||
findByCompanyId: jest.fn(async () => []),
|
||||
findByUserId: jest.fn(async () => ({
|
||||
id: "external-1",
|
||||
companyId: "company-1",
|
||||
company: company(),
|
||||
onboardingCompleted: false,
|
||||
})),
|
||||
},
|
||||
filesService: {
|
||||
findByResource: jest.fn(async () => ctx.files),
|
||||
findById: jest.fn(async () => null),
|
||||
remove: jest.fn(async () => undefined),
|
||||
},
|
||||
companyNotifier: { changeRequestSubmitted: jest.fn() },
|
||||
verifayda: {
|
||||
completeVerification: jest.fn(async () => ctx.verification),
|
||||
},
|
||||
};
|
||||
|
||||
const service = new CompaniesService(
|
||||
deps.companiesRepo as never,
|
||||
deps.companyProfilesRepo as never,
|
||||
deps.changeRequestRepo as never,
|
||||
deps.profilesRepo as never,
|
||||
{} as never,
|
||||
deps.filesService as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
deps.companyNotifier as never,
|
||||
{} as never,
|
||||
deps.verifayda as never,
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(service, "getCompanyInfoByUserId")
|
||||
.mockImplementation(
|
||||
async () =>
|
||||
({ profile: { id: "external-1" }, company: company() }) as never,
|
||||
);
|
||||
|
||||
return { service, ctx, deps, company };
|
||||
}
|
||||
|
||||
describe("Fayda identity verification binds a person to the company", () => {
|
||||
it("writes the verified identity", async () => {
|
||||
const { service, ctx } = makeService();
|
||||
|
||||
const state = await service.completeIdentityVerification("user-1", {
|
||||
subject: "owner",
|
||||
code: "c",
|
||||
state: "s",
|
||||
});
|
||||
|
||||
expect(ctx.attributes.ownerFaydaSub).toBe("new-sub");
|
||||
expect(ctx.attributes.ownerName).toBe("Haile Gebrselassie");
|
||||
expect(state.owner.verified).toBe(true);
|
||||
});
|
||||
|
||||
it("fills every PoA detail from the payload, address included", async () => {
|
||||
const { service, ctx } = makeService();
|
||||
|
||||
await service.completeIdentityVerification("user-1", {
|
||||
subject: "poa",
|
||||
code: "c",
|
||||
state: "s",
|
||||
});
|
||||
|
||||
expect(ctx.attributes.poaName).toBe("Haile Gebrselassie");
|
||||
expect(ctx.attributes.poaEmail).toBe("haile@example.com");
|
||||
expect(ctx.attributes.poaPhone).toBe("+251922000000");
|
||||
expect(ctx.attributes.poaAddress).toBe("Addis Ababa");
|
||||
});
|
||||
|
||||
it("verifies successfully even though Fayda returns no national ID number", async () => {
|
||||
// Fayda's userinfo carries no FAN/FIN claim at all — this must be the
|
||||
// normal, successful path, not an error.
|
||||
const { service } = makeService({
|
||||
verification: {
|
||||
purpose: "VERIFY",
|
||||
verified: true,
|
||||
sub: "x",
|
||||
fullName: "No Fan Here",
|
||||
},
|
||||
});
|
||||
|
||||
const state = await service.completeIdentityVerification("user-1", {
|
||||
subject: "owner",
|
||||
code: "c",
|
||||
state: "s",
|
||||
});
|
||||
|
||||
expect(state.owner.verified).toBe(true);
|
||||
});
|
||||
|
||||
it("refuses to make one identity both owner and PoA", async () => {
|
||||
const { service } = makeService({
|
||||
attributes: { ownerFaydaSub: "same-person" },
|
||||
verification: {
|
||||
purpose: "VERIFY",
|
||||
verified: true,
|
||||
sub: "same-person",
|
||||
fullName: "Abebe Bikila",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.completeIdentityVerification("user-1", {
|
||||
subject: "poa",
|
||||
code: "c",
|
||||
state: "s",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("stages the change for review on an approved company", async () => {
|
||||
// Swapping the person who can act for a live company is exactly what the
|
||||
// backoffice review exists for, so it must not rewrite the row directly.
|
||||
const { service, ctx, deps } = makeService({
|
||||
status: CompanyStatus.Active,
|
||||
});
|
||||
|
||||
await service.completeIdentityVerification("user-1", {
|
||||
subject: "poa",
|
||||
code: "c",
|
||||
state: "s",
|
||||
});
|
||||
|
||||
expect(deps.changeRequestRepo.create).toHaveBeenCalled();
|
||||
expect(ctx.attributes.poaFaydaSub).toBeUndefined();
|
||||
});
|
||||
|
||||
it("refuses to rename a verified person by hand", async () => {
|
||||
const { service } = makeService({
|
||||
attributes: { ...OWNER_VERIFIED, ...POA_VERIFIED },
|
||||
files: [paper()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.updateProfile("user-1", { poaName: "Someone Else" } as never),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("never locks or gates the general manager — it is not the verified subject", async () => {
|
||||
// GM is a plain typed role; the portal offers a "same as owner" copy, but
|
||||
// the backend must not treat it as identity-owned or require it verified.
|
||||
const { service } = makeService({
|
||||
attributes: { ...OWNER_VERIFIED },
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.updateProfile("user-1", {
|
||||
generalManagerName: "Someone Else",
|
||||
generalManagerEmail: "someone@example.com",
|
||||
generalManagerPhone: "+251911223344",
|
||||
} as never),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ethiopian companies verify with Fayda; foreign companies verify identity by passport", () => {
|
||||
// The company is applying for the forwarder role, so it must not already
|
||||
// hold it — createCompanyProfileForUser short-circuits on an existing profile
|
||||
// and would never reach the gate.
|
||||
const applyingForFf = {
|
||||
profileTypes: [ProfileType.importer],
|
||||
attributes: { ...POA_VERIFIED },
|
||||
files: [paper()],
|
||||
};
|
||||
|
||||
it("blocks the forwarder role while the owner is unverified", async () => {
|
||||
const { service } = makeService(applyingForFf);
|
||||
|
||||
await expect(
|
||||
service.createCompanyProfileForUser(
|
||||
"user-1",
|
||||
ProfileType.freightForwarder,
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("blocks the forwarder role while the PoA is unverified", async () => {
|
||||
const { service } = makeService({
|
||||
profileTypes: [ProfileType.importer],
|
||||
attributes: {
|
||||
...OWNER_VERIFIED,
|
||||
poaName: "Tirunesh Dibaba",
|
||||
poaEmail: "t@example.com",
|
||||
poaPhone: "+251911000000",
|
||||
},
|
||||
files: [paper()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createCompanyProfileForUser(
|
||||
"user-1",
|
||||
ProfileType.freightForwarder,
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("grants the forwarder role once owner and PoA are both verified", async () => {
|
||||
const { service } = makeService({
|
||||
profileTypes: [ProfileType.importer],
|
||||
attributes: { ...OWNER_VERIFIED, ...POA_VERIFIED },
|
||||
files: [paper()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createCompanyProfileForUser(
|
||||
"user-1",
|
||||
ProfileType.freightForwarder,
|
||||
),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("never asks a foreign company for Fayda, verified or not", async () => {
|
||||
const { service } = makeService({
|
||||
nationality: CompanyNationality.Foreign,
|
||||
});
|
||||
|
||||
const state = await service.completeIdentityVerification("user-1", {
|
||||
subject: "owner",
|
||||
code: "c",
|
||||
state: "s",
|
||||
});
|
||||
|
||||
// Still lets the owner verify — a foreign owner verifying is allowed, just
|
||||
// never required — but the passport is the thing that actually gates it.
|
||||
expect(state.owner.verified).toBe(true);
|
||||
expect(state.faydaRequired).toBe(false);
|
||||
expect(state.passportRequired).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks the forwarder role for a foreign company with no owner passport", async () => {
|
||||
const { service } = makeService({
|
||||
profileTypes: [ProfileType.importer],
|
||||
nationality: CompanyNationality.Foreign,
|
||||
attributes: {
|
||||
poaName: "Jean Dupont",
|
||||
poaEmail: "jean@example.com",
|
||||
poaPhone: "+33100000000",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createCompanyProfileForUser(
|
||||
"user-1",
|
||||
ProfileType.freightForwarder,
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("grants the forwarder role to a foreign company with an owner passport and no Fayda at all", async () => {
|
||||
const { service } = makeService({
|
||||
profileTypes: [ProfileType.importer],
|
||||
nationality: CompanyNationality.Foreign,
|
||||
attributes: {
|
||||
ownerPassportNumber: "P1234567",
|
||||
poaName: "Jean Dupont",
|
||||
poaEmail: "jean@example.com",
|
||||
poaPhone: "+33100000000",
|
||||
},
|
||||
files: [paper()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createCompanyProfileForUser(
|
||||
"user-1",
|
||||
ProfileType.freightForwarder,
|
||||
),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("still requires the passport for a foreign owner who chose to verify with Fayda too", async () => {
|
||||
// Verifying is optional for a foreign owner, but it does not waive the
|
||||
// passport requirement — the two are independent credentials.
|
||||
const { service } = makeService({
|
||||
profileTypes: [ProfileType.importer],
|
||||
nationality: CompanyNationality.Foreign,
|
||||
attributes: {
|
||||
...OWNER_VERIFIED,
|
||||
poaName: "Jean Dupont",
|
||||
poaEmail: "jean@example.com",
|
||||
poaPhone: "+33100000000",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createCompanyProfileForUser(
|
||||
"user-1",
|
||||
ProfileType.freightForwarder,
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
|
||||
import { CompaniesService } from "./companies.service";
|
||||
import { CompanyStatus } from "./entities/company.entity";
|
||||
import { ProfileType } from "./entities/company-profile.entity";
|
||||
import { POA_DELEGATION_FILE_KEY } from "../file-upload-settings/poa-delegation.constants";
|
||||
|
||||
/**
|
||||
* EDRFREIGHT-358: a company that names a Power of Attorney must have the DARS
|
||||
* delegation paper on file. The rule used to live only in the onboarding
|
||||
* wizard's completion check, so every other write that could break the pairing
|
||||
* — saving PoA details, deleting the paper, picking up the forwarder role —
|
||||
* went unguarded. These cover those writes.
|
||||
*/
|
||||
|
||||
interface Ctx {
|
||||
attributes: Record<string, unknown>;
|
||||
files: { id: string; code: string; reviewStatus?: string | null }[];
|
||||
profileTypes: ProfileType[];
|
||||
status: CompanyStatus;
|
||||
pendingSnapshot: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const POA = { poaName: "Abebe", poaEmail: "a@b.com", poaPhone: "+251911000000" };
|
||||
|
||||
/**
|
||||
* The forwarder role is gated on Fayda-verified identities as well as on the
|
||||
* delegation paper. These tests are about the paper, so they run against a
|
||||
* company whose identities are already verified — the identity rule itself is
|
||||
* covered in companies.fayda-identity.spec.ts.
|
||||
*/
|
||||
const VERIFIED_IDENTITIES = {
|
||||
ownerFaydaSub: "owner-sub",
|
||||
poaFaydaSub: "poa-sub",
|
||||
};
|
||||
|
||||
function makeService(overrides: Partial<Ctx> = {}) {
|
||||
const ctx: Ctx = {
|
||||
attributes: {},
|
||||
files: [],
|
||||
profileTypes: [ProfileType.importer],
|
||||
status: CompanyStatus.Pending,
|
||||
pendingSnapshot: null,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const company = () => ({
|
||||
id: "company-1",
|
||||
status: ctx.status,
|
||||
attributes: ctx.attributes,
|
||||
companyProfiles: ctx.profileTypes.map((type, i) => ({
|
||||
id: `profile-${i}`,
|
||||
type,
|
||||
})),
|
||||
type: "customer",
|
||||
});
|
||||
|
||||
const deps = {
|
||||
companiesRepo: {
|
||||
findById: jest.fn(async () => company()),
|
||||
update: jest.fn(async (_id: string, patch: Record<string, unknown>) => {
|
||||
ctx.attributes = (patch.attributes ??
|
||||
ctx.attributes) as Record<string, unknown>;
|
||||
return company();
|
||||
}),
|
||||
findByTin: jest.fn(async () => null),
|
||||
},
|
||||
companyProfilesRepo: {
|
||||
findByCompanyId: jest.fn(async () =>
|
||||
ctx.profileTypes.map((type, i) => ({ id: `profile-${i}`, type })),
|
||||
),
|
||||
findByType: jest.fn(async (_id: string, type: ProfileType) =>
|
||||
ctx.profileTypes.includes(type) ? { id: "existing", type } : null,
|
||||
),
|
||||
create: jest.fn(async (row: Record<string, unknown>) => ({
|
||||
id: "new",
|
||||
...row,
|
||||
})),
|
||||
},
|
||||
changeRequestRepo: {
|
||||
findPendingByCompanyId: jest.fn(async () =>
|
||||
ctx.pendingSnapshot ? { id: "cr-1", snapshot: ctx.pendingSnapshot } : null,
|
||||
),
|
||||
findByCompanyId: jest.fn(async () => []),
|
||||
create: jest.fn(async (row: Record<string, unknown>) => ({
|
||||
id: "cr-1",
|
||||
...row,
|
||||
})),
|
||||
update: jest.fn(async () => ({ id: "cr-1" })),
|
||||
},
|
||||
profilesRepo: {
|
||||
findByCompanyId: jest.fn(async () => []),
|
||||
findByUserId: jest.fn(async () => ({
|
||||
id: "external-1",
|
||||
companyId: "company-1",
|
||||
company: company(),
|
||||
onboardingCompleted: false,
|
||||
})),
|
||||
},
|
||||
filesService: {
|
||||
findByResource: jest.fn(async () => ctx.files),
|
||||
findById: jest.fn(async (id: string) =>
|
||||
ctx.files.find((f) => f.id === id)
|
||||
? {
|
||||
...ctx.files.find((f) => f.id === id),
|
||||
resource: "companies",
|
||||
resourceId: "company-1",
|
||||
name: "dars.pdf",
|
||||
}
|
||||
: null,
|
||||
),
|
||||
remove: jest.fn(async () => undefined),
|
||||
},
|
||||
companyNotifier: { changeRequestSubmitted: jest.fn() },
|
||||
};
|
||||
|
||||
const service = new CompaniesService(
|
||||
deps.companiesRepo as never,
|
||||
deps.companyProfilesRepo as never,
|
||||
deps.changeRequestRepo as never,
|
||||
deps.profilesRepo as never,
|
||||
{} as never,
|
||||
deps.filesService as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
deps.companyNotifier as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
// getCompanyInfoByUserId does its own lookups; the stubs above are enough for
|
||||
// the PoA paths, so short-circuit it rather than mock the whole graph.
|
||||
jest
|
||||
.spyOn(service, "getCompanyInfoByUserId")
|
||||
.mockImplementation(
|
||||
async () =>
|
||||
({ profile: { id: "external-1" }, company: company() }) as never,
|
||||
);
|
||||
|
||||
return { service, ctx, deps };
|
||||
}
|
||||
|
||||
const paper = (reviewStatus: string | null = null) => ({
|
||||
id: "file-1",
|
||||
code: POA_DELEGATION_FILE_KEY,
|
||||
reviewStatus,
|
||||
});
|
||||
|
||||
describe("PoA delegation paper is enforced wherever PoA state changes", () => {
|
||||
it("rejects PoA details saved with no paper on file", async () => {
|
||||
const { service } = makeService();
|
||||
|
||||
await expect(
|
||||
service.updateProfile("user-1", POA as never),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("accepts PoA details once the paper is on file", async () => {
|
||||
const { service } = makeService({ files: [paper()] });
|
||||
|
||||
await expect(
|
||||
service.updateProfile("user-1", POA as never),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("rejects a paper the reviewer sent back for correction", async () => {
|
||||
const { service } = makeService({ files: [paper("change_requested")] });
|
||||
|
||||
await expect(
|
||||
service.updateProfile("user-1", POA as never),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("leaves edits that don't touch the PoA alone", async () => {
|
||||
// A company carrying legacy details must not be locked out of every other
|
||||
// field until it produces a paper.
|
||||
const { service } = makeService({ attributes: { ...POA }, files: [] });
|
||||
|
||||
await expect(
|
||||
service.updateProfile("user-1", { companyEmail: "x@y.com" } as never),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("refuses to remove the paper while the PoA is still named", async () => {
|
||||
const { service } = makeService({
|
||||
attributes: { ...POA },
|
||||
files: [paper()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.removePoaDelegationLetter("user-1", "file-1"),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("allows removing the paper once the PoA has been cleared", async () => {
|
||||
const { service } = makeService({ attributes: {}, files: [paper()] });
|
||||
|
||||
await expect(
|
||||
service.removePoaDelegationLetter("user-1", "file-1"),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("judges the removal against a staged clear, not the live row", async () => {
|
||||
// An Active company's edits are staged for review rather than written, so
|
||||
// the live attributes still carry the PoA the customer just cleared.
|
||||
const { service } = makeService({
|
||||
status: CompanyStatus.Active,
|
||||
attributes: { ...POA },
|
||||
pendingSnapshot: { poaName: "", poaEmail: "", poaPhone: "" },
|
||||
files: [paper()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.removePoaDelegationLetter("user-1", "file-1"),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("refuses the forwarder role to a company with no PoA", async () => {
|
||||
const { service } = makeService({ attributes: { ...VERIFIED_IDENTITIES } });
|
||||
|
||||
await expect(
|
||||
service.createCompanyProfileForUser(
|
||||
"user-1",
|
||||
ProfileType.freightForwarder,
|
||||
),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it("grants the forwarder role once PoA details and paper are both in place", async () => {
|
||||
const { service } = makeService({
|
||||
attributes: { ...POA, ...VERIFIED_IDENTITIES },
|
||||
files: [paper()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createCompanyProfileForUser(
|
||||
"user-1",
|
||||
ProfileType.freightForwarder,
|
||||
),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user