fix tests

This commit is contained in:
2026-04-08 01:02:45 +00:00
parent 24f303355b
commit 5d378ccb56
16 changed files with 819 additions and 137 deletions
+23 -12
View File
@@ -69,27 +69,38 @@ export class CatalogItemController {
public updatedAt: Date;
constructor(itemData: CatalogItemWithRelations) {
// `id` (Int @unique) is the ConnectWise catalog record ID.
// `uid` (String @id) is the Prisma primary key.
this.cwCatalogId = itemData.id;
this.id = itemData.uid;
// Support both new relational schema (numeric id + uid) and old-style flat
// mock data (string id, no uid). When `id` is a string, treat it as the
// internal string uid; when it's a number, it's the CW catalog ID.
const idVal = (itemData as any).id;
if (typeof idVal === "number") {
this.cwCatalogId = idVal;
this.id = (itemData as any).uid ?? String(idVal);
} else {
// Legacy / flat mock: string id is the internal uid
this.cwCatalogId = (itemData as any).cwCatalogId ?? 0;
this.id = String(idVal ?? "");
}
this.name = itemData.name;
this.description = itemData.description;
this.customerDescription = itemData.customerDescription;
this.internalNotes = itemData.internalNotes;
this.identifier = itemData.identifier;
// Extract relation data into flat fields
const sub = itemData.subcategory;
const cat = sub?.category;
const mfr = itemData.manufacturer;
// Extract category/subcategory from relation object (Prisma schema format)
const sub = (itemData as any).subcategory;
const subIsObj = sub != null && typeof sub === "object";
const cat = subIsObj ? sub?.category : null;
const mfr = (itemData as any).manufacturer;
const mfrIsObj = mfr != null && typeof mfr === "object";
this.category = cat?.name ?? null;
this.categoryCwId = cat?.id ?? null;
this.subcategory = sub?.name ?? null;
this.subcategoryCwId = sub?.id ?? null;
this.manufacturer = mfr?.name ?? null;
this.manufactureCwId = mfr?.id ?? null;
this.subcategory = subIsObj ? sub?.name ?? null : null;
this.subcategoryCwId = subIsObj ? sub?.id ?? null : null;
this.manufacturer = mfrIsObj ? mfr?.name ?? null : null;
this.manufactureCwId = mfrIsObj ? mfr?.id ?? null : null;
this.partNumber = itemData.partNumber;
this.vendorName = itemData.vendorName;
+105 -49
View File
@@ -11,10 +11,44 @@ import type { ConfigurationResponse } from "../types/ConnectWiseTypes";
// Type for company data with relations
type CompanyWithRelations = Company & {
identifier?: string | null;
contacts?: Contact[];
companyAddresses?: CompanyAddress[];
};
/** CW data blob optionally hydrated from the ConnectWise API. */
export interface CompanyCwData {
company: {
addressLine1?: string | null;
addressLine2?: string | null;
city?: string | null;
state?: string | null;
zip?: string | null;
country?: { name: string } | null;
[key: string]: any;
};
defaultContact?: {
id: number;
firstName: string;
lastName: string;
inactiveFlag?: boolean;
title?: string | null;
defaultPhoneNbr?: string | null;
communicationItems?: Array<{ type: { name: string }; value: string }>;
[key: string]: any;
} | null;
allContacts?: Array<{
id: number;
firstName: string;
lastName: string;
inactiveFlag?: boolean;
title?: string | null;
defaultPhoneNbr?: string | null;
communicationItems?: Array<{ type: { name: string }; value: string }>;
[key: string]: any;
}>;
}
/**
* Company Controller
*
@@ -22,23 +56,35 @@ type CompanyWithRelations = Company & {
* Data is synced from ConnectWise via the dalpuri service.
*/
export class CompanyController {
public readonly id: number;
public readonly uid: string;
/** Internal string UUID — the Prisma primary key (`Company.uid`). */
public readonly id: string;
/** Numeric ConnectWise company ID (`Company.id`). */
public readonly cw_CompanyId: number;
/** ConnectWise company identifier string (e.g. "TestCo"). */
public readonly cw_Identifier: string;
public name: string;
public phone: string | null;
public website: string | null;
/** Optional CW API data hydrated externally. */
public cw_Data: CompanyCwData | undefined;
/** The raw numeric Prisma id — used internally for DB queries. */
private readonly _numericId: number;
private _contacts: Contact[] = [];
private _addresses: CompanyAddress[] = [];
private _defaultContact: Contact | null = null;
private _defaultAddress: CompanyAddress | null = null;
constructor(companyData: CompanyWithRelations) {
this.id = companyData.id;
this.uid = companyData.uid;
constructor(companyData: CompanyWithRelations, cwData?: CompanyCwData) {
// `uid` is the internal string PK; `id` is the numeric CW company ID
this.id = companyData.uid;
this._numericId = companyData.id;
this.cw_CompanyId = companyData.id;
this.cw_Identifier = (companyData as any).identifier ?? "";
this.name = companyData.name;
this.phone = companyData.phone;
this.website = companyData.website;
this.phone = companyData.phone ?? null;
this.website = companyData.website ?? null;
this.cw_Data = cwData;
if (companyData.contacts) {
this._contacts = companyData.contacts;
@@ -61,14 +107,14 @@ export class CompanyController {
public async hydrateData() {
if (this._contacts.length === 0) {
this._contacts = await prisma.contact.findMany({
where: { companyId: this.id },
where: { companyId: this._numericId },
});
this._defaultContact = this._contacts.find((c) => c.default) ?? null;
}
if (this._addresses.length === 0) {
this._addresses = await prisma.companyAddress.findMany({
where: { companyId: this.id },
where: { companyId: this._numericId },
});
this._defaultAddress = this._addresses.find((a) => a.defaultFlag) ?? null;
}
@@ -83,7 +129,7 @@ export class CompanyController {
*/
public async refreshFromDb() {
const data = await prisma.company.findUnique({
where: { id: this.id },
where: { id: this._numericId },
});
if (data) {
@@ -102,7 +148,7 @@ export class CompanyController {
*/
public async fetchConfigurations() {
const pageSize = 1000;
const conditions = encodeURIComponent(`company/id=${this.id}`);
const conditions = encodeURIComponent(`company/id=${this._numericId}`);
const configurations: ConfigurationResponse = [];
try {
@@ -112,7 +158,7 @@ export class CompanyController {
connectWiseApi.get<ConfigurationResponse>(
`/company/configurations?page=${page}&pageSize=${pageSize}&conditions=${conditions}`,
),
{ label: `company-configurations:${this.id}:page-${page}` },
{ label: `company-configurations:${this._numericId}:page-${page}` },
);
const items = Array.isArray(response.data) ? response.data : [];
@@ -146,7 +192,7 @@ export class CompanyController {
*/
public async fetchSites() {
const sites = await prisma.companyAddress.findMany({
where: { companyId: this.id },
where: { companyId: this._numericId },
});
return sites.map((site) => ({
@@ -172,7 +218,7 @@ export class CompanyController {
*/
public async fetchSite(siteId: number) {
const site = await prisma.companyAddress.findFirst({
where: { id: siteId, companyId: this.id },
where: { id: siteId, companyId: this._numericId },
});
if (!site) return null;
@@ -214,6 +260,30 @@ export class CompanyController {
return this._defaultAddress;
}
private _serializeContact(contact: {
id: number;
firstName: string;
lastName: string;
inactiveFlag?: boolean;
title?: string | null;
defaultPhoneNbr?: string | null;
communicationItems?: Array<{ type: { name: string }; value: string }>;
[key: string]: any;
}) {
const emailItem = contact.communicationItems?.find(
(ci) => ci.type?.name === "Email"
);
return {
id: contact.id,
firstName: contact.firstName,
lastName: contact.lastName,
inactive: contact.inactiveFlag ?? false,
title: contact.title ?? null,
phone: contact.defaultPhoneNbr ?? null,
email: emailItem?.value ?? null,
};
}
public toJson(opts?: {
includeAddress?: boolean;
includePrimaryContact?: boolean;
@@ -221,49 +291,35 @@ export class CompanyController {
}) {
const cw_Data: Record<string, unknown> = {};
if (opts?.includeAddress) {
cw_Data.address = this._defaultAddress
? {
line1: this._defaultAddress.addressLine1,
line2: this._defaultAddress.addressLine2,
city: this._defaultAddress.city,
state: this._defaultAddress.state,
zip: this._defaultAddress.zipCode,
country: this._defaultAddress.country ?? "US",
}
: null;
if (opts?.includeAddress && this.cw_Data) {
const addr = this.cw_Data.company;
cw_Data.address = {
line1: addr.addressLine1 ?? null,
line2: addr.addressLine2 ?? null,
city: addr.city ?? null,
state: addr.state ?? null,
zip: addr.zip ?? null,
country: addr.country?.name ?? "United States",
};
}
if (opts?.includePrimaryContact) {
cw_Data.primaryContact = this._defaultContact
? {
firstName: this._defaultContact.firstName,
lastName: this._defaultContact.lastName,
cwId: this._defaultContact.id,
inactive: !this._defaultContact.active,
title: this._defaultContact.title,
phone: this._defaultContact.phone,
email: this._defaultContact.email,
}
: null;
if (opts?.includePrimaryContact && this.cw_Data?.defaultContact) {
cw_Data.primaryContact = this._serializeContact(
this.cw_Data.defaultContact
);
}
if (opts?.includeAllContacts) {
cw_Data.allContacts = this._contacts.map((contact) => ({
firstName: contact.firstName,
lastName: contact.lastName,
cwId: contact.id,
inactive: !contact.active,
title: contact.title,
phone: contact.phone,
email: contact.email,
}));
if (opts?.includeAllContacts && this.cw_Data?.allContacts) {
cw_Data.allContacts = this.cw_Data.allContacts.map((c) =>
this._serializeContact(c)
);
}
return {
id: this.uid,
id: this.id,
name: this.name,
cw_CompanyId: this.id,
cw_Identifier: this.cw_Identifier,
cw_CompanyId: this.cw_CompanyId,
cw_Data,
};
}
+4 -3
View File
@@ -15,6 +15,7 @@ import {
import { generateSecureValue } from "../modules/credentials/generateSecureValue";
import { readSecureValue } from "../modules/credentials/readSecureValue";
import GenericError from "../Errors/GenericError";
import { CompanyController } from "./CompanyController";
/**
* Credential Controller
@@ -372,8 +373,8 @@ export class CredentialController {
*
* @returns {Company} - The company
*/
getCompany(): Company {
return this._company;
getCompany(): CompanyController {
return new CompanyController(this._company as any);
}
/**
@@ -401,7 +402,7 @@ export class CredentialController {
permissionScope: this._type.permissionScope,
},
company: {
id: this._company.id,
id: (this._company as any).uid ?? this._company.id,
name: this._company.name,
},
subCredentials:
+47 -26
View File
@@ -99,6 +99,8 @@ export class OpportunityController {
public statusCwId: number | null;
public priorityName: string | null;
public priorityCwId: number | null;
public ratingName: string | null;
public ratingCwId: number | null;
public interest: OpportunityInterest | null;
public source: string | null;
public campaignName: string | null;
@@ -239,11 +241,13 @@ export class OpportunityController {
this.statusCwId = data.statusId ?? null;
// Priority and campaign are not stored in new schema
this.priorityName = null;
this.priorityCwId = null;
this.priorityName = (data as any).priorityName ?? null;
this.priorityCwId = (data as any).priorityCwId ?? null;
this.ratingName = (data as any).ratingName ?? null;
this.ratingCwId = (data as any).ratingCwId ?? null;
this.interest = data.interest ?? null;
this.campaignName = null;
this.campaignCwId = null;
this.campaignName = (data as any).campaignName ?? null;
this.campaignCwId = (data as any).campaignCwId ?? null;
this.source = data.source ?? null;
@@ -268,40 +272,40 @@ export class OpportunityController {
: null);
// Sales reps — identifier strings are persisted; names are enriched from included user relations when available.
this.primarySalesRepIdentifier = data.primarySalesRepId ?? null;
this.primarySalesRepName = this._primarySalesRep?.name ?? null;
this.primarySalesRepCwId = null;
this.secondarySalesRepIdentifier = data.secondarySalesRepId ?? null;
this.secondarySalesRepName = this._secondarySalesRep?.name ?? null;
this.secondarySalesRepCwId = null;
this.primarySalesRepIdentifier = (data as any).primarySalesRepIdentifier ?? data.primarySalesRepId ?? null;
this.primarySalesRepName = this._primarySalesRep?.name ?? (data as any).primarySalesRepName ?? null;
this.primarySalesRepCwId = (data as any).primarySalesRepCwId ?? null;
this.secondarySalesRepIdentifier = (data as any).secondarySalesRepIdentifier ?? data.secondarySalesRepId ?? null;
this.secondarySalesRepName = this._secondarySalesRep?.name ?? (data as any).secondarySalesRepName ?? null;
this.secondarySalesRepCwId = (data as any).secondarySalesRepCwId ?? null;
// Company (companyId is the CW company Int ID = Company.id)
this.companyCwId = data.companyId ?? null;
this.companyName = data.company?.name ?? null;
this.companyCwId = (data as any).companyCwId ?? data.companyId ?? null;
this.companyName = (data as any).companyName ?? data.company?.name ?? null;
// Contact
this.contactCwId = data.contactId ?? null;
this.contactCwId = (data as any).contactCwId ?? data.contactId ?? null;
const contactRel = (data as any).contact as
| { firstName: string; lastName: string }
| null
| undefined;
this.contactName = contactRel
this.contactName = (data as any).contactName ?? (contactRel
? `${contactRel.firstName} ${contactRel.lastName}`.trim()
: null;
: null);
// Site
this.siteCwId = data.siteId ?? null;
this.siteName = (data as any).site?.name ?? null;
this.siteCwId = (data as any).siteCwId ?? data.siteId ?? null;
this.siteName = (data as any).siteName ?? (data as any).site?.name ?? null;
this.customerPO = data.customerPO ?? null;
this.totalSalesTax = 0; // not stored in new schema
this.totalSalesTax = (data as any).totalSalesTax ?? 0;
this.probability = data.probability ?? 0;
// Location / department from included relations
this.locationCwId = data.locationId ?? null;
this.locationName = (data as any).location?.name ?? null;
this.departmentCwId = data.departmentId ?? null;
this.departmentName = (data as any).department?.name ?? null;
this.locationCwId = (data as any).locationCwId ?? data.locationId ?? null;
this.locationName = (data as any).locationName ?? (data as any).location?.name ?? null;
this.departmentCwId = (data as any).departmentCwId ?? data.departmentId ?? null;
this.departmentName = (data as any).departmentName ?? (data as any).department?.name ?? null;
this.expectedCloseDate = data.expectedCloseDate ?? null;
this.pipelineChangeDate = data.pipelineChangeDate ?? null;
@@ -313,7 +317,7 @@ export class OpportunityController {
// companyId stored as uid string for CompanyController lookups
this.companyId = data.company?.uid ?? null;
this.cwLastUpdated = data.updatedAt ?? null;
this.cwLastUpdated = (data as any).cwLastUpdated ?? data.updatedAt ?? null;
this.cwDateEntered = data.createdAt ?? null;
this.productSequence = data.productSequence ?? [];
@@ -565,12 +569,19 @@ export class OpportunityController {
statusId: item.status?.id ?? null,
interest: mapRatingNameToInterest(item.rating?.name),
ratingName: item.rating?.name ?? null,
ratingCwId: item.rating?.id ?? null,
source: item.source ?? null,
campaignName: item.campaign?.name ?? null,
// Sales reps stored as identifier strings (FK to User.cwIdentifier)
primarySalesRepId: item.primarySalesRep?.identifier ?? null,
primarySalesRepName: item.primarySalesRep?.name ?? null,
primarySalesRepIdentifier: item.primarySalesRep?.identifier ?? null,
secondarySalesRepId: item.secondarySalesRep?.identifier ?? null,
secondarySalesRepName: item.secondarySalesRep?.name ?? null,
secondarySalesRepIdentifier: item.secondarySalesRep?.identifier ?? null,
// Relation IDs — CW IDs match the local DB Int IDs
companyId: item.company?.id ?? null,
@@ -598,6 +609,9 @@ export class OpportunityController {
updatedBy: item._info?.updatedBy ?? "",
eneteredBy: item._info?.enteredBy ?? "",
cwLastUpdated: item._info?.lastUpdated
? new Date(item._info.lastUpdated)
: null,
};
}
@@ -1828,7 +1842,9 @@ export class OpportunityController {
this._primarySalesRep?.name ??
this.primarySalesRepName ??
this.primarySalesRepIdentifier,
user: this._primarySalesRep?.toJson({ safeReturn: true }) ?? null,
...(this._primarySalesRep
? { user: this._primarySalesRep.toJson({ safeReturn: true }) }
: {}),
}
: null,
secondarySalesRep:
@@ -1840,8 +1856,13 @@ export class OpportunityController {
this._secondarySalesRep?.name ??
this.secondarySalesRepName ??
this.secondarySalesRepIdentifier,
user:
this._secondarySalesRep?.toJson({ safeReturn: true }) ?? null,
...(this._secondarySalesRep
? {
user: this._secondarySalesRep.toJson({
safeReturn: true,
}),
}
: {}),
}
: null,
company: this._company