fix tests
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user