fix: gm email

This commit is contained in:
Nathnael
2026-08-09 07:57:45 +00:00
parent e03ccf0f69
commit 182787e143
3 changed files with 130 additions and 3 deletions

View File

@@ -354,6 +354,93 @@ describe("Fayda identity verification binds a person to the company", () => {
).resolves.toBeDefined();
});
// Fayda's email and phone claims are optional and routinely absent. The owner
// is who the company is reached through and the step renders no input for
// their contact details, so the onboarding account — already OTP-proven —
// stands in rather than leaving the company unreachable.
describe("account contact details stand in for absent Fayda claims", () => {
const noContactClaims = {
purpose: "VERIFY",
verified: true,
sub: "new-sub",
fullName: "Haile Gebrselassie",
address: "Addis Ababa",
};
const account = {
email: "account@example.com",
phoneNumber: "+251911777777",
};
it("falls back to the account for an owner Fayda gave no email or phone", async () => {
const { service, ctx } = makeService({ verification: noContactClaims });
await service.completeIdentityVerification(
"user-1",
{ subject: "owner", code: "c", state: "s" },
account,
);
expect(ctx.attributes.ownerEmail).toBe("account@example.com");
expect(ctx.attributes.ownerPhone).toBe("+251911777777");
});
it("prefers the Fayda claim over the account when there is one", async () => {
const { service, ctx } = makeService();
await service.completeIdentityVerification(
"user-1",
{ subject: "owner", code: "c", state: "s" },
account,
);
expect(ctx.attributes.ownerEmail).toBe("haile@example.com");
expect(ctx.attributes.ownerPhone).toBe("+251922000000");
});
it("leaves the PoA alone — the account is not that person", async () => {
const { service, ctx } = makeService({ verification: noContactClaims });
await service.completeIdentityVerification(
"user-1",
{ subject: "poa", code: "c", state: "s" },
account,
);
expect(ctx.attributes.poaEmail).toBeUndefined();
expect(ctx.attributes.poaPhone).toBeUndefined();
});
// Owners verified before the fallback existed hold blank contacts. Copying
// those blanks onto the GM makes generalManagerEmail required by onboarding
// with no field anywhere to satisfy it.
it("fills the GM copy from the account when the stored owner has no contacts", async () => {
const { service, ctx } = makeService({
attributes: { ...OWNER_VERIFIED },
});
await service.setGmSameAsOwner("user-1", account);
expect(ctx.attributes.gmEmail).toBe("account@example.com");
expect(ctx.attributes.generalManagerEmail).toBe("account@example.com");
expect(ctx.attributes.generalManagerPhone).toBe("+251911777777");
});
it("keeps the stored owner contacts when the GM copy has them", async () => {
const { service, ctx } = makeService({
attributes: {
...OWNER_VERIFIED,
ownerEmail: "abebe@example.com",
ownerPhone: "+251911000111",
},
});
await service.setGmSameAsOwner("user-1", account);
expect(ctx.attributes.generalManagerEmail).toBe("abebe@example.com");
expect(ctx.attributes.generalManagerPhone).toBe("+251911000111");
});
});
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.

View File

@@ -139,10 +139,16 @@ export function stepPayload(
};
}
case "personnel":
// `|| undefined`, never "": the DTO's `@IsOptional()` only skips null and
// undefined, so an empty string is validated and 400s with
// "generalManagerEmail must be an email". An Ethiopian company never types
// these — the GM comes from the Fayda verification (or the "same as owner"
// declaration), so the form fields are legitimately blank and would fail a
// step that has no input to fix.
return {
generalManagerName: d.generalManagerName,
generalManagerEmail: d.generalManagerEmail,
generalManagerPhone: d.generalManagerPhone,
generalManagerName: d.generalManagerName || undefined,
generalManagerEmail: d.generalManagerEmail || undefined,
generalManagerPhone: d.generalManagerPhone || undefined,
};
case "contact":
return {

View File

@@ -123,6 +123,40 @@ describe("stepPayload (company)", () => {
});
});
describe("stepPayload (personnel)", () => {
// An Ethiopian company never types the GM — Fayda (or "same as owner") owns
// those fields — so the form holds "". `@IsOptional()` on the DTO skips only
// null/undefined, so an empty string is validated and comes back as
// "generalManagerEmail must be an email", on a step that renders no input.
it("omits blank GM fields instead of sending empty strings", () => {
const payload = stepPayload(
"personnel",
values({
generalManagerName: "",
generalManagerEmail: "",
generalManagerPhone: "",
}),
);
expect(payload.generalManagerName).toBeUndefined();
expect(payload.generalManagerEmail).toBeUndefined();
expect(payload.generalManagerPhone).toBeUndefined();
});
it("still sends typed GM details (foreign company)", () => {
const payload = stepPayload(
"personnel",
values({
generalManagerName: "Abebe Bikila",
generalManagerEmail: "gm@example.com",
generalManagerPhone: "+251911223344",
}),
);
expect(payload.generalManagerName).toBe("Abebe Bikila");
expect(payload.generalManagerEmail).toBe("gm@example.com");
expect(payload.generalManagerPhone).toBe("+251911223344");
});
});
describe("firstPresent", () => {
it("skips empty strings rather than stopping at them", () => {
expect(firstPresent("", " ", "second@example.com")).toBe(