diff --git a/src/server/utils/data/add-district-to-opp.ts b/src/server/utils/data/add-district-to-opp.ts index bd6b0fa9..c0248b2c 100644 --- a/src/server/utils/data/add-district-to-opp.ts +++ b/src/server/utils/data/add-district-to-opp.ts @@ -1,4 +1,5 @@ import { OpportunityType } from "need4deed-sdk"; +import Postcode from "../../../data/entity/location/postcode.entity"; import Opportunity from "../../../data/entity/opportunity/opportunity.entity"; import { getDistrictFromPostcode } from "../../../data/utils/get-district"; @@ -12,29 +13,37 @@ export function getDistrictToOpportunityHandler() { if (opportunity.districtId) { return opportunity; } - if (opportunity.type !== OpportunityType.ACCOMPANYING) { - const district = opportunity.deal?.dealDistrict?.[0]?.district; + + // ACCOMPANYING: derive from the appointment's own postcode (be#895). + // `deal.postcode` mirrors the agent's own postcode — an unrelated + // concept — so it isn't used here. + if (opportunity.type === OpportunityType.ACCOMPANYING) { + const postcode = + opportunity.accompanying?.postcode ?? + (opportunity.accompanying?.postcodeId + ? new Postcode({ id: opportunity.accompanying.postcodeId }) + : undefined); + const district = postcode + ? await getDistrictFromPostcode(postcode) + : null; if (district) { opportunity.district = district; updates.push(opportunity); + return opportunity; } - return opportunity; - } - // accompanying: use appointment postcode from the form - const district = await getDistrictFromPostcode( - opportunity.deal?.postcode, - ); - if (district) { - opportunity.district = district; - updates.push(opportunity); - return opportunity; } - // fallback to agent + + // REGULAR/EVENTS use the opportunity's own agent's district (be#895). + // Also the fallback for an ACCOMPANYING opportunity whose appointment + // postcode didn't resolve to a district. `agent.districtId` is an + // already-loaded FK column wherever `agent` is loaded at all, no extra + // relation needed. if (opportunity.agent?.districtId) { opportunity.districtId = opportunity.agent.districtId; updates.push(opportunity); return opportunity; } + const districtFromAgent = await getDistrictFromPostcode( opportunity.agent?.address?.postcode, ); diff --git a/src/test/server/utils/data/add-district-to-opp.test.ts b/src/test/server/utils/data/add-district-to-opp.test.ts index dec4ce66..dccdc518 100644 --- a/src/test/server/utils/data/add-district-to-opp.test.ts +++ b/src/test/server/utils/data/add-district-to-opp.test.ts @@ -25,6 +25,7 @@ function makeOpportunity( district: undefined, agent: undefined, deal: undefined, + accompanying: undefined, ...overrides, } as Opportunity; } @@ -43,69 +44,131 @@ describe("getDistrictToOpportunityHandler", () => { }); describe("addDistrictToOpportunity", () => { - describe("Non-ACCOMPANYING Type", () => { - it("assigns district from deal location and returns early", async () => { + it("does nothing if opportunity.districtId is already set", async () => { + const handler = getDistrictToOpportunityHandler(); + const opportunity = makeOpportunity({ + type: OpportunityType.REGULAR, + districtId: 3, + agent: { districtId: 99 } as any, + }); + + const result = await handler.addDistrictToOpportunity(opportunity); + + expect(result.districtId).toBe(3); + expect(handler.updates).toHaveLength(0); + expect(mockedGetDistrictFromPostcode).not.toHaveBeenCalled(); + }); + + describe("REGULAR / EVENTS", () => { + it("uses the opportunity's own agent's districtId", async () => { const handler = getDistrictToOpportunityHandler(); - const district = makeDistrict({ id: 5 }); const opportunity = makeOpportunity({ type: OpportunityType.REGULAR, - deal: { - dealDistrict: [{ district }], - } as any, + agent: { districtId: 5 } as any, }); const result = await handler.addDistrictToOpportunity(opportunity); - expect(result.district).toBe(district); + expect(result.districtId).toBe(5); expect(handler.updates).toContain(opportunity); expect(mockedGetDistrictFromPostcode).not.toHaveBeenCalled(); }); + + it("falls back to the agent's own address postcode when agent.districtId is missing", async () => { + const handler = getDistrictToOpportunityHandler(); + const district = makeDistrict({ id: 202 }); + const opportunity = makeOpportunity({ + type: OpportunityType.EVENTS, + agent: { address: { postcode: "54321" } } as any, + }); + mockedGetDistrictFromPostcode.mockResolvedValue(district as any); + + const result = await handler.addDistrictToOpportunity(opportunity); + + expect(mockedGetDistrictFromPostcode).toHaveBeenCalledWith("54321"); + expect(result.district).toBe(district); + expect(handler.updates).toContain(opportunity); + }); + + it("does not add to updates if no agent info is available", async () => { + const handler = getDistrictToOpportunityHandler(); + const opportunity = makeOpportunity({ + type: OpportunityType.REGULAR, + agent: undefined, + }); + mockedGetDistrictFromPostcode.mockResolvedValue(undefined!); + + const result = await handler.addDistrictToOpportunity(opportunity); + + expect(result.district).toBeUndefined(); + expect(result.districtId).toBeUndefined(); + expect(handler.updates).toHaveLength(0); + }); }); - describe("ACCOMPANYING Type Resolution Priority", () => { - it("Priority 1: assigns district from deal postcode", async () => { + describe("ACCOMPANYING resolution priority", () => { + it("Priority 1: resolves from the accompanying's own postcode relation", async () => { const handler = getDistrictToOpportunityHandler(); const district = makeDistrict({ id: 101 }); + const postcode = { id: 7, value: "12345" }; const opportunity = makeOpportunity({ type: OpportunityType.ACCOMPANYING, - deal: { postcode: "12345" } as any, + accompanying: { postcode } as any, }); mockedGetDistrictFromPostcode.mockResolvedValue(district as any); const result = await handler.addDistrictToOpportunity(opportunity); - expect(mockedGetDistrictFromPostcode).toHaveBeenCalledWith("12345"); + expect(mockedGetDistrictFromPostcode).toHaveBeenCalledWith(postcode); expect(result.district).toBe(district); expect(handler.updates).toContain(opportunity); }); - it("Priority 2: falls back to agent.districtId if deal postcode lookup fails", async () => { + it("resolves from accompanying.postcodeId when the postcode relation isn't loaded", async () => { const handler = getDistrictToOpportunityHandler(); + const district = makeDistrict({ id: 303 }); const opportunity = makeOpportunity({ type: OpportunityType.ACCOMPANYING, - deal: { postcode: "INVALID" } as any, + accompanying: { postcodeId: 42 } as any, + }); + mockedGetDistrictFromPostcode.mockResolvedValue(district as any); + + const result = await handler.addDistrictToOpportunity(opportunity); + + expect(mockedGetDistrictFromPostcode).toHaveBeenCalledWith( + expect.objectContaining({ id: 42 }), + ); + expect(result.district).toBe(district); + expect(handler.updates).toContain(opportunity); + }); + + it("Priority 2: falls back to agent.districtId if the appointment postcode lookup fails", async () => { + const handler = getDistrictToOpportunityHandler(); + const opportunity = makeOpportunity({ + type: OpportunityType.ACCOMPANYING, + accompanying: { postcode: { id: 7 } } as any, agent: { districtId: 99 } as any, }); mockedGetDistrictFromPostcode.mockResolvedValue(undefined!); const result = await handler.addDistrictToOpportunity(opportunity); + expect(mockedGetDistrictFromPostcode).toHaveBeenCalledTimes(1); expect(result.districtId).toBe(99); expect(handler.updates).toContain(opportunity); }); - it("Priority 3: falls back to agent postcode if agent.districtId is missing", async () => { + it("Priority 3: falls back to the agent's address postcode if agent.districtId is also missing", async () => { const handler = getDistrictToOpportunityHandler(); const district = makeDistrict({ id: 202 }); const opportunity = makeOpportunity({ type: OpportunityType.ACCOMPANYING, - deal: { postcode: "INVALID" } as any, - agent: { - address: { postcode: "54321" }, - } as any, + accompanying: { postcode: { id: 7 } } as any, + agent: { address: { postcode: "54321" } } as any, }); - // First call (deal postcode) returns null, second call (agent postcode) returns district + // First call (appointment postcode) returns nothing, second call + // (agent's own postcode) resolves. mockedGetDistrictFromPostcode .mockResolvedValueOnce(undefined!) .mockResolvedValueOnce(district as any); @@ -116,36 +179,35 @@ describe("getDistrictToOpportunityHandler", () => { expect(result.district).toBe(district); expect(handler.updates).toContain(opportunity); }); - }); - describe("Edge Cases", () => { - it("does not add to updates if no district info is found anywhere", async () => { + it("handles a missing accompanying object gracefully", async () => { const handler = getDistrictToOpportunityHandler(); const opportunity = makeOpportunity({ type: OpportunityType.ACCOMPANYING, - deal: undefined, - agent: undefined, + accompanying: undefined, + agent: { districtId: 77 } as any, }); - mockedGetDistrictFromPostcode.mockResolvedValue(undefined!); const result = await handler.addDistrictToOpportunity(opportunity); - expect(result.district).toBeUndefined(); - expect(handler.updates).toHaveLength(0); + expect(result.districtId).toBe(77); + expect(handler.updates).toHaveLength(1); + expect(mockedGetDistrictFromPostcode).not.toHaveBeenCalled(); }); - it("handles missing deal object gracefully for ACCOMPANYING type", async () => { + it("does not add to updates if no district info is found anywhere", async () => { const handler = getDistrictToOpportunityHandler(); const opportunity = makeOpportunity({ type: OpportunityType.ACCOMPANYING, - agent: { districtId: 77 } as any, + accompanying: undefined, + agent: undefined, }); mockedGetDistrictFromPostcode.mockResolvedValue(undefined!); - await handler.addDistrictToOpportunity(opportunity); + const result = await handler.addDistrictToOpportunity(opportunity); - expect(opportunity.districtId).toBe(77); - expect(handler.updates).toHaveLength(1); + expect(result.district).toBeUndefined(); + expect(handler.updates).toHaveLength(0); }); }); }); @@ -157,11 +219,11 @@ describe("getDistrictToOpportunityHandler", () => { const o1 = makeOpportunity({ type: OpportunityType.ACCOMPANYING, - deal: { postcode: "1" } as any, + accompanying: { postcode: { id: 1 } } as any, }); const o2 = makeOpportunity({ type: OpportunityType.ACCOMPANYING, - deal: { postcode: "2" } as any, + accompanying: { postcode: { id: 2 } } as any, }); await handler.addDistrictToOpportunity(o1);