Files
edr-platform/e2e/freight/cypress/e2e/flows/onboarding_switch_back.cy.ts
Nathnael 604710bd25 feat(companies): attach an eTrade business to each company profile
A TIN holds many business licences split by activity — export of coffee,
freight forwarding, import of vehicles — but the company picked one for
its whole record, so every operational role shared it. Each profile now
names the business it actually operates as.

Stored on `company_profiles.etrade_business` as a snapshot (licence
number, trade name, activity, renewal) rather than a bare licence number,
so the portal and backoffice can show it without an eTrade round-trip —
that API is slow, serves a broken TLS chain and is regularly down. Not
unique: one business may legitimately back several roles.

The licence number is a client input, so it is never stored as sent —
`ETradeService.findBusinessOption` looks it up under the company's own
TIN and persists eTrade's record, which makes another company's licence
simply unfindable.

Choosing one is required wherever the customer adds a role with a TIN
already on file. The onboarding wizard is the exception by necessity: it
picks roles on its first step, before a TIN exists, so there is nothing
to choose from yet. There it is enforced through
`getOnboardingRequirements` instead — an unattached role is reported
outstanding and blocks submission — and the picker sits on the documents
step beside that role's licence upload.

Lifted entirely for a co-operative or investment-licence company: eTrade
holds no record for its TIN, so the requirement would be unsatisfiable.
2026-08-27 09:29:15 +00:00

172 lines
6.7 KiB
TypeScript

/**
* Giving the investment-licence route back.
*
* A company that ticked the box by mistake, or that has since been registered
* with the trade registry, switches from Settings → Company. It is a
* re-application rather than a settings edit, and this spec is about proving
* that literally: the typed registration is cleared (nothing on file was ever
* checked against a licence), the company returns to pending, onboarding
* reopens on the company step — and only after a real eTrade lookup does the
* backoffice stop flagging it.
*
* Each test is one leg of a single journey, in order, retries off. The
* portal ↔ backoffice hops re-evaluate the spec bundle, so every leg resolves
* the company from the database rather than from module state.
*/
import {
approveFirstProfile,
companyByEmail,
completeInvestorOnboarding,
etradeTin,
expectProfileActive,
fillAria,
latestJourney,
openCustomer,
signupIdentity,
wizardClick,
SIGNUP_PASSWORD,
chooseRoleBusiness,
} from "./onboarding-utils";
const stamp = Date.now();
const who = signupIdentity("switchback", stamp);
describe("onboarding — switching back to eTrade registration", { retries: 0 }, () => {
it("onboards on an investment licence", () => {
completeInvestorOnboarding(who, stamp, {
companyName: `E2E Switchback Trading ${stamp}`,
});
companyByEmail(who.email).then((company) => {
expect((company.attributes ?? {})["investorLicence"]).to.eq(true);
expect(company.region, "typed address").to.eq("Addis Ababa");
});
});
it("is approved by the backoffice", () => {
cy.loginBackoffice("chief@edr.local");
latestJourney("switchback").then((company) => {
openCustomer(company.name);
cy.contains("Manual entry · investment licence").should("be.visible");
approveFirstProfile();
expectProfileActive(company.name);
});
});
it("switches back from settings, which clears the typed registration", () => {
latestJourney("switchback").then((company) => {
cy.loginPortal(company.email, SIGNUP_PASSWORD);
});
cy.visitPortal("/settings");
cy.contains("Registration source", { timeout: 20000 }).should("be.visible");
cy.contains("button", "Switch to eTrade registration").click();
// The confirmation has to state the cost outright — this is the screen
// that decides whether the customer knows they are re-applying.
cy.contains("Switch to eTrade registration?").should("be.visible");
cy.contains("The registration details you typed are cleared").should(
"be.visible",
);
cy.contains("Your company goes back to pending").should("be.visible");
cy.contains("button", "Switch and re-apply").click();
cy.contains("Registration source", { timeout: 20000 }).should("not.exist");
latestJourney("switchback").then((company) => {
expect((company.attributes ?? {})["investorLicence"]).to.be.undefined;
expect(company.status).to.eq("pending");
expect(company.onboarding_completed).to.eq(false);
// The wizard treats a populated registration as a lookup that already
// passed, so leaving any of it behind would walk the customer straight
// past the eTrade step this switch exists to reach.
expect(company.region, "typed address cleared").to.be.null;
expect(company.licence_number).to.be.null;
expect(company.etrade_phone).to.be.null;
expect(company.onboarding_step, "resume target").to.eq("company");
});
});
it("re-runs onboarding through eTrade and resubmits", () => {
latestJourney("switchback").then((company) => {
cy.loginPortal(company.email, SIGNUP_PASSWORD);
});
cy.visitPortal("/portal");
// Reopened on the company step — not on the furthest step reached before.
cy.contains("Confirm your VAT number", { timeout: 30000 }).should(
"be.visible",
);
// The typed registration section is gone: this is an ordinary company now.
cy.contains("Nothing on file at eTrade for this TIN").should("not.exist");
// Wait for the profile to rehydrate before typing. The company still holds
// the TIN it typed under the flag (the switch clears the registration, not
// the tax number), and RHF re-seeds the field when the refetch lands — so a
// TIN typed into an empty-looking field is silently replaced by the stored
// one, and the lookup then runs against the wrong number.
cy.get('.mantine-Modal-content [aria-label="VAT Number"]')
.should("not.have.value", "");
fillAria('[aria-label^="TIN Number"]', etradeTin(stamp));
cy.get('.mantine-Modal-content [aria-label^="TIN Number"]').should(
"have.value",
etradeTin(stamp),
);
cy.contains("Verified with eTrade", { timeout: 20000 }).should("be.visible");
cy.wait(500);
wizardClick("Continue");
// Owner, representation and contact are already satisfied server-side —
// the switch keeps everything except the registration — so each step only
// needs advancing.
cy.contains("Company Owner", { timeout: 20000 }).should("be.visible");
wizardClick("Continue");
cy.contains("The owner acts for the company", { timeout: 20000 }).should(
"be.visible",
);
wizardClick("Continue");
cy.contains("Contact Person", { timeout: 20000 }).should("be.visible");
wizardClick("Continue");
// Documents and the licence were uploaded before the switch and survive
// it, so the step is already satisfied.
cy.contains("Upload Importer Business license file(s)", {
timeout: 20000,
}).should("be.visible");
chooseRoleBusiness();
wizardClick("Submit for review");
cy.contains("You're all set", { timeout: 30000 }).should("be.visible");
latestJourney("switchback").then((company) => {
expect(company.onboarding_completed).to.eq(true);
expect((company.attributes ?? {})["investorLicence"]).to.be.undefined;
// eTrade answered this time, and its licence is on file.
expect(company.licence_number).to.eq("LIC-E2E-0001");
});
});
it("the backoffice no longer flags it", () => {
cy.loginBackoffice("chief@edr.local");
latestJourney("switchback").then((company) => {
openCustomer(company.name);
cy.contains("eTrade trade licence").should("be.visible");
cy.contains("Manual entry").should("not.exist");
cy.contains("Registration entered by hand").should("not.exist");
});
});
it("offers nothing to switch to a customer who came through eTrade", () => {
// The seeded demo customer onboarded the ordinary way, so the card must
// not be on its settings page at all.
cy.loginPortal();
cy.visitPortal("/settings");
cy.contains("Operational Services", { timeout: 20000 }).should("be.visible");
cy.contains("Registration source").should("not.exist");
});
});
export {};