Added Connectwise Compnay Syncing

This commit is contained in:
2026-01-26 17:09:18 -06:00
parent 4524c0258a
commit 7748e6171b
19 changed files with 1783 additions and 9 deletions
+14
View File
@@ -4,6 +4,7 @@ import { Prisma, PrismaClient } from "../generated/prisma/client";
import * as msal from "@azure/msal-node";
import { Server } from "socket.io";
import { Server as Engine } from "@socket.io/bun-engine";
import axios from "axios";
const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaPg({ connectionString });
@@ -55,3 +56,16 @@ const engine = new Engine();
io.bind(engine);
export { io, engine };
// Connectwise API Client
const connectWiseApi = axios.create({
baseURL: `https://ttscw.totaltech.net/v4_6_release/apis/3.0/`,
headers: {
Authorization: `Basic ${process.env.CW_BASIC_TOKEN}`,
clientId: `${process.env.CW_CLIENT_ID}`,
"Content-Type": "application/json",
},
});
export { connectWiseApi };
+10
View File
@@ -1,5 +1,15 @@
import { refresh } from "./api/auth";
import app from "./api/server";
import { engine, PORT } from "./constants";
import { refreshCompanies } from "./modules/cw-utils/refreshCompanies";
import { events, setupEventDebugger } from "./modules/globalEvents";
// Setup global event debugger in non-production environments
if (Bun.env.NODE_ENV == "development") setupEventDebugger();
// Refresh the internal list of companies every minute
await refreshCompanies();
setInterval(() => refreshCompanies, 60 * 1000);
Bun.serve({
port: PORT,
+27
View File
@@ -0,0 +1,27 @@
import { Collection } from "@discordjs/collection";
import { connectWiseApi } from "../../constants";
import { Company } from "../../types/ConnectWiseTypes";
export const fetchAllCwCompanies = async (): Promise<
Collection<string, Company>
> => {
let allCompanies = new Collection<string, Company>();
const pageCount = 1000;
const count = (await connectWiseApi.get("/company/companies/count")).data
.count;
const totalPages = Math.ceil(count / pageCount);
for (let page = 0; page < totalPages; page++) {
const response = await connectWiseApi.get(
`/company/companies?page=${page + 1}&pageSize=${pageCount}`,
);
const companies = response.data;
for (const company of companies) {
allCompanies.set(company.id, company);
}
}
return allCompanies;
};
+40
View File
@@ -0,0 +1,40 @@
import { connectWiseApi, prisma } from "../../constants";
import { events } from "../globalEvents";
import { fetchAllCwCompanies } from "./fetchAllCompanies";
export const refreshCompanies = async () => {
events.emit("cw:companies:refreshed:check");
// Dynamically import to avoid circular dependency
const internalCompanyCount = await prisma.company.count();
const externalCompanyCount =
(await connectWiseApi.get("/company/companies/count")).data.count - 1;
if (internalCompanyCount !== externalCompanyCount) {
console.log(
`Company count mismatch detected. Internal: ${internalCompanyCount}, External: ${externalCompanyCount}. Refreshing...`,
);
const allCompanies = await fetchAllCwCompanies();
await Promise.all(
allCompanies.map(async (company) => {
return await prisma.company.upsert({
where: { cw_CompanyId: company.id },
create: {
cw_CompanyId: company.id,
cw_Identifier: company.identifier,
name: company.name,
},
update: {
name: company.name,
},
});
}),
);
events.emit("cw:companies:refreshed", {
internalCompaniesCount: internalCompanyCount,
externalCompaniesCount: externalCompanyCount,
});
}
};
+6 -1
View File
@@ -53,6 +53,11 @@ interface EventTypes {
err: Error;
role: RoleController;
}) => void;
"cw:companies:refreshed:check": () => void;
"cw:companies:refreshed": (data: {
internalCompaniesCount: number;
externalCompaniesCount: number;
}) => void;
}
export const events = new Eventra<EventTypes>();
@@ -61,4 +66,4 @@ export function setupEventDebugger() {
events.any((eventName, ...args) => {
console.log(`[ Event Debugger ] (${eventName})`);
});
}
}
+139
View File
@@ -0,0 +1,139 @@
export interface Company {
id: number;
identifier: string;
name: string;
status: CompanyStatus;
addressLine1: string;
city: string;
state: string;
zip: string;
phoneNumber: string;
faxNumber: string;
website: string;
territory: Territory;
market: Market;
accountNumber: string;
defaultContact: Contact;
dateAcquired: string;
annualRevenue: number;
numberOfEmployees: number;
leadFlag: boolean;
unsubscribeFlag: boolean;
vendorIdentifier: string;
taxIdentifier: string;
taxCode: TaxCode;
billingTerms: BasicEntity;
billToCompany: LinkedCompany;
billingSite: LinkedSite;
billingContact: Contact;
invoiceDeliveryMethod: BasicEntity;
invoiceToEmailAddress: string;
deletedFlag: boolean;
dateDeleted: string;
mobileGuid: string;
resellerIdentifier: string;
isVendorFlag: boolean;
types: TypeItem[];
site: LinkedSite;
_info: CompanyInfo;
customFields: CustomField[];
}
export interface CompanyStatus {
id: number;
name: string;
_info: {
status_href: string;
};
}
export interface Territory {
id: number;
name: string;
_info: {
location_href: string;
};
}
export interface Market {
id: number;
name: string;
_info: {
Market_href: string;
};
}
export interface TaxCode {
id: number;
name: string;
_info: {
taxCode_href: string;
};
}
export interface BasicEntity {
id: number;
name: string;
}
export interface LinkedCompany extends BasicEntity {
identifier: string;
_info: {
company_href: string;
};
}
export interface LinkedSite extends BasicEntity {
_info: {
site_href: string;
};
}
export interface Contact {
id: number;
name: string;
_info: {
contact_href: string;
};
}
export interface TypeItem {
id: number;
name: string;
_info: {
type_href: string;
};
}
export interface CompanyInfo {
lastUpdated: string;
updatedBy: string;
dateEntered: string;
enteredBy: string;
contacts_href: string;
agreements_href: string;
tickets_href: string;
opportunities_href: string;
activities_href: string;
projects_href: string;
configurations_href: string;
orders_href: string;
documents_href: string;
sites_href: string;
teams_href: string;
reports_href: string;
notes_href: string;
}
export interface CustomField {
id: number;
caption: string;
type: string;
entryMethod: string;
numberOfDecimals: number;
value: string | null;
connectWiseId: string;
rowNum: number;
userDefinedFieldRecId: number;
podId: string;
}