mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
fixing multi company bug
This commit is contained in:
parent
2468d3600b
commit
9118f9202b
3 changed files with 276 additions and 7 deletions
|
|
@ -0,0 +1,265 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// ── Hoisted mocks ─────────────────────────────────────────────────────────────
|
||||
const { mockGetServerSession, mockDbSelect, mockNotFound, mockRedirect } =
|
||||
vi.hoisted(() => ({
|
||||
mockGetServerSession: vi.fn(),
|
||||
mockDbSelect: vi.fn(),
|
||||
mockNotFound: vi.fn(() => {
|
||||
throw new Error("NEXT_NOT_FOUND");
|
||||
}),
|
||||
mockRedirect: vi.fn(() => {
|
||||
throw new Error("NEXT_REDIRECT");
|
||||
}),
|
||||
}));
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
vi.mock("next-auth", () => ({ getServerSession: mockGetServerSession }));
|
||||
vi.mock("@/app/api/auth/[...nextauth]/authOptions", () => ({ AuthOptions: {} }));
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: mockRedirect,
|
||||
notFound: mockNotFound,
|
||||
}));
|
||||
vi.mock("next/link", () => ({ default: vi.fn(() => null) }));
|
||||
|
||||
// ── Drizzle ORM ───────────────────────────────────────────────────────────────
|
||||
vi.mock("drizzle-orm", () => ({
|
||||
eq: vi.fn((a: unknown, b: unknown) => ({ $eq: [a, b] })),
|
||||
inArray: vi.fn((col: unknown, vals: unknown) => ({ $inArray: [col, vals] })),
|
||||
and: vi.fn((...args: unknown[]) => ({ $and: args })),
|
||||
desc: vi.fn((col: unknown) => ({ $desc: col })),
|
||||
sql: vi.fn(),
|
||||
}));
|
||||
vi.mock("drizzle-orm/pg-core", () => ({
|
||||
alias: vi.fn((table: unknown, _name: string) => table),
|
||||
}));
|
||||
|
||||
// ── DB ────────────────────────────────────────────────────────────────────────
|
||||
vi.mock("@/app/db/db", () => ({
|
||||
db: { get select() { return mockDbSelect; } },
|
||||
}));
|
||||
|
||||
// ── Schemas ───────────────────────────────────────────────────────────────────
|
||||
vi.mock("@/app/db/schema/crm/hubspot_deal_table", () => ({
|
||||
hubspotDealData: { dealId: {}, companyId: {}, coordinator: {}, designer: {} },
|
||||
}));
|
||||
vi.mock("@/app/db/schema/crm/hubspot_user_table", () => ({
|
||||
hubspotUsers: { hubspotOwnerId: {}, firstName: {}, lastName: {} },
|
||||
}));
|
||||
vi.mock("@/app/db/schema/uploaded_files", () => ({
|
||||
uploadedFiles: { hubsotDealId: {}, fileType: {}, measureName: {}, uprn: {} },
|
||||
}));
|
||||
vi.mock("@/app/db/schema/portfolio_organisation", () => ({
|
||||
portfolioOrganisation: { portfolioId: {}, organisationId: {} },
|
||||
}));
|
||||
vi.mock("@/app/db/schema/organisation", () => ({
|
||||
organisation: { id: {}, hubspotCompanyId: {} },
|
||||
}));
|
||||
vi.mock("@/app/db/schema/portfolio", () => ({
|
||||
portfolioCapabilities: { portfolioId: {}, userId: {}, capability: {} },
|
||||
portfolioUsers: { portfolioId: {}, userId: {}, role: {} },
|
||||
}));
|
||||
vi.mock("@/app/db/schema/approvals", () => ({
|
||||
dealMeasureApprovals: { hubspotDealId: {}, isApproved: {}, measureName: {} },
|
||||
}));
|
||||
vi.mock("@/app/db/schema/removal_requests", () => ({
|
||||
propertyRemovalRequests: {
|
||||
portfolioId: {},
|
||||
hubspotDealId: {},
|
||||
type: {},
|
||||
status: {},
|
||||
requestedAt: {},
|
||||
},
|
||||
}));
|
||||
vi.mock("@/app/db/schema/users", () => ({
|
||||
user: { id: {}, email: {} },
|
||||
}));
|
||||
|
||||
// ── Transforms & lib ──────────────────────────────────────────────────────────
|
||||
vi.mock("../transforms", () => ({
|
||||
classifyDeals: vi.fn((deals: unknown[]) =>
|
||||
deals.map((d) => ({ ...(d as object), displayStage: "Test Stage" })),
|
||||
),
|
||||
}));
|
||||
vi.mock("@/app/lib/measureDocumentRequirements", () => ({
|
||||
getRequiredDocs: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
// ── DealPage component ────────────────────────────────────────────────────────
|
||||
vi.mock("./DealPage", () => ({ default: vi.fn(() => null) }));
|
||||
|
||||
// ── Subject under test ────────────────────────────────────────────────────────
|
||||
import DealDetailPage from "./page";
|
||||
|
||||
// ── Chain helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Generic chain: .limit(n) resolves to limitResult; awaiting without .limit
|
||||
// resolves to directResult (thenable).
|
||||
function makeSelectChain(
|
||||
limitResult: unknown[] = [],
|
||||
directResult: unknown[] = [],
|
||||
) {
|
||||
const self: Record<string, unknown> = {};
|
||||
self.then = (
|
||||
resolve: (v: unknown) => unknown,
|
||||
reject: (e: unknown) => unknown,
|
||||
) => Promise.resolve(directResult).then(resolve, reject);
|
||||
self.from = vi.fn(() => self);
|
||||
self.innerJoin = vi.fn(() => self);
|
||||
self.leftJoin = vi.fn(() => self);
|
||||
self.where = vi.fn(() => self);
|
||||
self.orderBy = vi.fn(() => self);
|
||||
self.limit = vi.fn(() => Promise.resolve(limitResult));
|
||||
return self;
|
||||
}
|
||||
|
||||
// Deal chain: inspects the WHERE argument's JSON to decide what to return.
|
||||
// Returns dealRow only when the condition includes $inArray, i.e. after fix.
|
||||
// Before fix, eq() produces $eq and the deal is not found (empty result).
|
||||
function makeDealChain(dealRow: Record<string, unknown>) {
|
||||
let conditionUsesInArray = false;
|
||||
const self: Record<string, unknown> = {};
|
||||
self.then = (
|
||||
resolve: (v: unknown) => unknown,
|
||||
reject: (e: unknown) => unknown,
|
||||
) => Promise.resolve([]).then(resolve, reject);
|
||||
self.from = vi.fn(() => self);
|
||||
self.leftJoin = vi.fn(() => self);
|
||||
self.where = vi.fn((condition: unknown) => {
|
||||
conditionUsesInArray = JSON.stringify(condition).includes('"$inArray"');
|
||||
return self;
|
||||
});
|
||||
self.orderBy = vi.fn(() => self);
|
||||
self.limit = vi.fn(() =>
|
||||
Promise.resolve(conditionUsesInArray ? [dealRow] : []),
|
||||
);
|
||||
return self;
|
||||
}
|
||||
|
||||
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const DEAL_COMPANY = "86970043613";
|
||||
const OTHER_COMPANIES = ["262091354351", "86797211838"];
|
||||
const DEAL_ID = "489569618109";
|
||||
const PORTFOLIO_ID = "737";
|
||||
|
||||
const mockDealRow = {
|
||||
deal: {
|
||||
id: "1",
|
||||
dealId: DEAL_ID,
|
||||
dealname: "Test Property",
|
||||
dealstage: "test",
|
||||
companyId: DEAL_COMPANY,
|
||||
projectCode: null,
|
||||
landlordPropertyId: null,
|
||||
uprn: null,
|
||||
outcome: null,
|
||||
outcomeNotes: null,
|
||||
majorConditionIssueDescription: null,
|
||||
majorConditionIssuePhotos: null,
|
||||
majorConditionIssuePhotosS3: null,
|
||||
coordinationStatus: null,
|
||||
designStatus: null,
|
||||
pashubLink: null,
|
||||
sharepointLink: null,
|
||||
dampmouldGrowth: null,
|
||||
damnpMouldAndRepairComments: null,
|
||||
preSap: null,
|
||||
mtpCompletionDate: null,
|
||||
mtpReModelCompletionDate: null,
|
||||
ioeV3CompletionDate: null,
|
||||
proposedMeasures: null,
|
||||
approvedPackage: null,
|
||||
designCompletionDate: null,
|
||||
actualMeasuresInstalled: null,
|
||||
installer: null,
|
||||
installerHandover: null,
|
||||
lodgementStatus: null,
|
||||
measuresLodgementDate: null,
|
||||
lodgementDate: null,
|
||||
confirmedSurveyDate: null,
|
||||
confirmedSurveyTime: null,
|
||||
surveyedDate: null,
|
||||
dealType: null,
|
||||
eiScore: null,
|
||||
eiScorePotential: null,
|
||||
epcSapScore: null,
|
||||
epcSapScorePotential: null,
|
||||
surveyType: null,
|
||||
measuresForPibiOrdered: null,
|
||||
pibiOrderDate: null,
|
||||
pibiCompletedDate: null,
|
||||
propertyHaltedDate: null,
|
||||
propertyHaltedReason: null,
|
||||
technicalApprovedMeasuresForInstall: null,
|
||||
domnaSurveyType: null,
|
||||
domnaSurveyDate: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
coordinator: null,
|
||||
designer: null,
|
||||
};
|
||||
|
||||
function setupDb() {
|
||||
mockGetServerSession.mockResolvedValue({
|
||||
user: { email: "test@domna.homes" },
|
||||
});
|
||||
|
||||
// 1. Org lookup
|
||||
// limitResult = [wrong company] — what .limit(1) returns (broken path)
|
||||
// directResult = all three companies — what await-without-limit returns (fixed path)
|
||||
mockDbSelect.mockImplementationOnce(() =>
|
||||
makeSelectChain(
|
||||
[{ hubspotCompanyId: OTHER_COMPANIES[0] }],
|
||||
[
|
||||
{ hubspotCompanyId: DEAL_COMPANY },
|
||||
{ hubspotCompanyId: OTHER_COMPANIES[0] },
|
||||
{ hubspotCompanyId: OTHER_COMPANIES[1] },
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 2. Deal lookup — returns deal only when inArray appears in the where clause
|
||||
mockDbSelect.mockImplementationOnce(() => makeDealChain(mockDealRow));
|
||||
|
||||
// 3. User lookup
|
||||
mockDbSelect.mockImplementationOnce(() => makeSelectChain([{ id: 1n }]));
|
||||
|
||||
// 4. Cap rows (no .limit — awaited via thenable)
|
||||
mockDbSelect.mockImplementationOnce(() => makeSelectChain([], []));
|
||||
|
||||
// 5. Role row
|
||||
mockDbSelect.mockImplementationOnce(() =>
|
||||
makeSelectChain([{ role: "read" }]),
|
||||
);
|
||||
|
||||
// 6. Approval rows (no .limit)
|
||||
mockDbSelect.mockImplementationOnce(() => makeSelectChain([], []));
|
||||
|
||||
// 7. Removal rows (has .orderBy + .limit)
|
||||
mockDbSelect.mockImplementationOnce(() => makeSelectChain([]));
|
||||
|
||||
// 8. Phase-1 docs (no .limit)
|
||||
mockDbSelect.mockImplementationOnce(() => makeSelectChain([], []));
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("DealDetailPage — multi-organisation portfolio", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders deal when its company is not the first linked organisation", async () => {
|
||||
setupDb();
|
||||
await expect(
|
||||
DealDetailPage({
|
||||
params: Promise.resolve({ slug: PORTFOLIO_ID, dealId: DEAL_ID }),
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
expect(mockNotFound).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -109,22 +109,23 @@ export default async function DealDetailPage(props: {
|
|||
redirect("/");
|
||||
}
|
||||
|
||||
const link = await db
|
||||
const links = await db
|
||||
.select({ hubspotCompanyId: organisation.hubspotCompanyId })
|
||||
.from(portfolioOrganisation)
|
||||
.innerJoin(
|
||||
organisation,
|
||||
eq(portfolioOrganisation.organisationId, organisation.id),
|
||||
)
|
||||
.where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId)))
|
||||
.limit(1);
|
||||
.where(eq(portfolioOrganisation.portfolioId, BigInt(portfolioId)));
|
||||
|
||||
if (!link.length || !link[0].hubspotCompanyId) {
|
||||
const companyIds = links
|
||||
.map((l) => l.hubspotCompanyId)
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
if (companyIds.length === 0) {
|
||||
redirect(`/portfolio/${portfolioId}/your-projects/live`);
|
||||
}
|
||||
|
||||
const companyId = link[0].hubspotCompanyId;
|
||||
|
||||
const rawDeals = await db
|
||||
.select({
|
||||
deal: hubspotDealData,
|
||||
|
|
@ -142,7 +143,7 @@ export default async function DealDetailPage(props: {
|
|||
)
|
||||
.where(
|
||||
and(
|
||||
eq(hubspotDealData.companyId, companyId),
|
||||
inArray(hubspotDealData.companyId, companyIds),
|
||||
eq(hubspotDealData.dealId, dealId),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { defineConfig } from "vitest/config";
|
|||
import path from "node:path";
|
||||
|
||||
export default defineConfig({
|
||||
esbuild: {
|
||||
jsx: "automatic",
|
||||
},
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue