From 0b8b9c39ab8064f966b0a428efbcf80d5eb8546f Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 18 Aug 2026 09:00:21 +0000 Subject: [PATCH] fix(companies): clear the typed registration when the manual-entry box is un-ticked Going back in the wizard and un-ticking co-operative or investment licence used to write the flag and nothing else. The registration the customer had typed stayed on the company row, so `hasRegistrationDetails` still read as a passed eTrade lookup, resume dropped them at their furthest step rather than the company one, and the application could be finished on unverified data with no flag left on it for the backoffice to show. That transition now costs what the settings switch costs: the eTrade-sourced columns and the manager captured beside them are cleared, and onboarding drops back to the company step so the TIN actually goes through eTrade. Both the reset payload and the attribute strip are now shared with `revertToRegularCompany`, which did this correctly already. --- .../companies.investor-licence.spec.ts | 64 +++++++++++++++ .../modules/companies/companies.service.ts | 77 +++++++++++++++---- 2 files changed, 124 insertions(+), 17 deletions(-) diff --git a/apps/edr-freight-api/src/modules/companies/companies.investor-licence.spec.ts b/apps/edr-freight-api/src/modules/companies/companies.investor-licence.spec.ts index 4a17d9594..9623945d1 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.investor-licence.spec.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.investor-licence.spec.ts @@ -138,6 +138,70 @@ describe("the foreign investment-licence route", () => { }); }); + it("clears the typed registration when the box is un-ticked on the way back", async () => { + const { service, companiesRepo, profilesRepo } = makeService({ + id: "company-1", + nationality: CompanyNationality.Foreign, + attributes: { investorLicence: true, etradeManagerName: "Typed Name" }, + region: "Addis Ababa", + licenceNumber: "TYPED-1", + }); + + await start(service, CompanyNationality.Foreign, false, false); + + const [, updates] = companiesRepo.update.mock.calls[0] as unknown as [ + string, + Record, + ]; + // The wizard sends both flags; the typed manager does not survive. + expect(updates.attributes).toEqual({ + cooperative: false, + investorLicence: false, + }); + expect(updates.licenceNumber).toBeNull(); + expect(updates.region).toBeNull(); + // Resume must land back on the company step, or the customer never reaches + // the eTrade lookup they just opted back into. + expect(profilesRepo.update).toHaveBeenCalledWith("external-1", { + onboardingStep: "company", + }); + }); + + it("does the same for a co-operative that stops being one", async () => { + const { service, companiesRepo } = makeService({ + id: "company-1", + nationality: CompanyNationality.Ethiopian, + attributes: { cooperative: true }, + region: "Oromia", + }); + + await start(service, CompanyNationality.Ethiopian, false, false); + + const [, updates] = companiesRepo.update.mock.calls[0] as unknown as [ + string, + Record, + ]; + expect(updates.region).toBeNull(); + }); + + it("leaves the registration alone while the flag stays on", async () => { + const { service, companiesRepo, profilesRepo } = makeService({ + id: "company-1", + nationality: CompanyNationality.Foreign, + attributes: { investorLicence: true }, + region: "Addis Ababa", + }); + + await start(service, CompanyNationality.Foreign, false, true); + + const [, updates] = companiesRepo.update.mock.calls[0] as unknown as [ + string, + Record, + ]; + expect(updates).not.toHaveProperty("region"); + expect(profilesRepo.update).not.toHaveBeenCalled(); + }); + it("refuses to switch a company that never took the investment-licence route", async () => { const { service } = makeService({ id: "company-1", attributes: {} }); await expect(service.revertToRegularCompany("user-1")).rejects.toBeInstanceOf( diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index f9090f288..6dae48f24 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -424,9 +424,28 @@ export class CompaniesService { : {}), }; } + // Going back and un-ticking the box is the same act as the settings + // switch, so it has to cost the same: the registration the customer typed + // goes, and onboarding drops back to the company step. Without this the + // draft keeps the typed values, `hasRegistrationDetails` reads as a passed + // lookup, resume lands past the company step entirely — and the company + // finishes onboarding on unverified data with no flag left to say so. + const backToEtrade = + usesManualRegistration(current) && !isCoop && !isInvestor; + if (backToEtrade) { + Object.assign(updates, CompaniesService.CLEARED_REGISTRATION); + updates.attributes = this.withoutTypedEtradeManager( + updates.attributes ?? current?.attributes, + ); + } if (Object.keys(updates).length > 0) { await this.companiesRepo.update(companyId, updates); } + if (backToEtrade) { + await this.profilesRepo.update(existing.id, { + onboardingStep: "company", + }); + } return this.getCompanyInfoByUserId(identity.userId); } @@ -513,6 +532,45 @@ export class CompaniesService { } } + /** + * The registration block as it must look when nobody has verified it. + * + * Used wherever a company stops being one eTrade cannot answer for: whatever + * sits in these columns was the customer's own statement, and the wizard + * treats a populated registration as a lookup that already passed + * (`hasRegistrationDetails`). Leaving it behind would hand the company an + * eTrade-verified record eTrade never supplied — and, once the flag is gone, + * a backoffice screen that says so. + */ + private static readonly CLEARED_REGISTRATION: Partial = { + licenceNumber: null, + statusDescription: null, + dateRegistered: null, + renewedFrom: null, + renewalDate: null, + renewedTo: null, + region: null, + zone: null, + woreda: null, + kebele: null, + houseNo: null, + etradePhone: null, + }; + + /** + * The company's own `attributes`, minus the manager captured alongside a + * typed registration. It never came from a licence, so it must not outlive + * the registration it belonged to. + */ + private withoutTypedEtradeManager( + attributes: Record | null | undefined, + ): Record { + const next = { ...(attributes ?? {}) }; + delete next.etradeManagerName; + delete next.etradeManagerPhone; + return next; + } + /** * An investment licence belongs to a foreign company and to nothing else. * @@ -2457,28 +2515,13 @@ export class CompaniesService { ); } - const attributes = { ...(company.attributes ?? {}) }; + const attributes = this.withoutTypedEtradeManager(company.attributes); delete attributes[INVESTOR_LICENCE_KEY]; - // The manager captured alongside the (typed) registration goes with it — - // it never came from a licence, so it must not survive as one. - delete attributes.etradeManagerName; - delete attributes.etradeManagerPhone; await this.companiesRepo.update(companyId, { + ...CompaniesService.CLEARED_REGISTRATION, attributes, status: CompanyStatus.Pending, - licenceNumber: null, - statusDescription: null, - dateRegistered: null, - renewedFrom: null, - renewalDate: null, - renewedTo: null, - region: null, - zone: null, - woreda: null, - kebele: null, - houseNo: null, - etradePhone: null, }); await this.profilesRepo.update(profile.id, { onboardingCompleted: false,