import { CatalogItem } from "../../generated/prisma/client"; import { prisma } from "../constants"; import { catalogCw } from "../modules/cw-utils/procurement/catalog"; import { CatalogItem as CWCatalogItem } from "../modules/cw-utils/procurement/catalog.types"; import GenericError from "../Errors/GenericError"; /** * Catalog Item Controller * * This class encapsulates a catalog item entity and provides domain methods * for accessing, refreshing, and serializing catalog item data. It bridges * the internal database representation with ConnectWise catalog data. */ export class CatalogItemController { public readonly id: string; public name: string; public description: string | null; public customerDescription: string | null; public internalNotes: string | null; public readonly cwCatalogId: number; public readonly identifier: string | null; public manufacturer: string | null; public manufactureCwId: number | null; public partNumber: string | null; public vendorName: string | null; public vendorSku: string | null; public vendorCwId: number | null; public price: number; public cost: number; public inactive: boolean; public salesTaxable: boolean; public onHand: number; public cwLastUpdated: Date | null; private _linkedItems: CatalogItemController[]; public readonly createdAt: Date; public updatedAt: Date; constructor( itemData: CatalogItem & { linkedItems?: CatalogItem[]; }, ) { this.id = itemData.id; this.name = itemData.name; this.description = itemData.description; this.customerDescription = itemData.customerDescription; this.internalNotes = itemData.internalNotes; this.cwCatalogId = itemData.cwCatalogId; this.identifier = itemData.identifier; this.manufacturer = itemData.manufacturer; this.manufactureCwId = itemData.manufactureCwId; this.partNumber = itemData.partNumber; this.vendorName = itemData.vendorName; this.vendorSku = itemData.vendorSku; this.vendorCwId = itemData.vendorCwId; this.price = itemData.price; this.cost = itemData.cost; this.inactive = itemData.inactive; this.salesTaxable = itemData.salesTaxable; this.onHand = itemData.onHand; this.cwLastUpdated = itemData.cwLastUpdated; this.createdAt = itemData.createdAt; this.updatedAt = itemData.updatedAt; this._linkedItems = (itemData.linkedItems ?? []).map( (linked) => new CatalogItemController(linked), ); } /** * Refresh Inventory * * Fetches the latest on-hand inventory count from ConnectWise * and updates both the controller state and the database. * * @returns {Promise} - The updated controller */ public async refreshInventory(): Promise { const onHand = await catalogCw.fetchInventoryOnHand(this.cwCatalogId); if (onHand !== this.onHand) { await prisma.catalogItem.update({ where: { id: this.id }, data: { onHand }, }); this.onHand = onHand; } return this; } /** * Fetch Linked Items * * Returns the linked catalog items as an array of controllers. * * @returns {CatalogItemController[]} - Array of linked item controllers */ public getLinkedItems(): CatalogItemController[] { return this._linkedItems; } /** * Link Item * * Links another catalog item to this item. The relationship is bidirectional * via the Prisma implicit many-to-many. * * @param targetId - The internal ID of the catalog item to link * @returns {Promise} - The updated controller */ public async linkItem(targetId: string): Promise { if (targetId === this.id) { throw new GenericError({ message: "Cannot link a catalog item to itself", name: "InvalidLinkTarget", cause: `Item '${this.id}' cannot be linked to itself`, status: 400, }); } const target = await prisma.catalogItem.findFirst({ where: { id: targetId }, }); if (!target) { throw new GenericError({ message: "Target catalog item not found", name: "CatalogItemNotFound", cause: `No catalog item exists with ID '${targetId}'`, status: 404, }); } const updated = await prisma.catalogItem.update({ where: { id: this.id }, data: { linkedItems: { connect: { id: targetId } }, }, include: { linkedItems: true }, }); this._linkedItems = (updated.linkedItems ?? []).map( (linked) => new CatalogItemController(linked), ); return this; } /** * Unlink Item * * Removes the link between this catalog item and another. * * @param targetId - The internal ID of the catalog item to unlink * @returns {Promise} - The updated controller */ public async unlinkItem(targetId: string): Promise { const updated = await prisma.catalogItem.update({ where: { id: this.id }, data: { linkedItems: { disconnect: { id: targetId } }, }, include: { linkedItems: true }, }); this._linkedItems = (updated.linkedItems ?? []).map( (linked) => new CatalogItemController(linked), ); return this; } /** * To JSON * * Serializes the catalog item into a safe, API-friendly object. * * @param opts - Options to control output * @returns - A JSON-safe representation of the catalog item */ public toJson(opts?: { includeLinkedItems?: boolean }): Record { return { id: this.id, cwCatalogId: this.cwCatalogId, identifier: this.identifier, name: this.name, description: this.description, customerDescription: this.customerDescription, internalNotes: this.internalNotes, manufacturer: this.manufacturer, manufactureCwId: this.manufactureCwId, partNumber: this.partNumber, vendorName: this.vendorName, vendorSku: this.vendorSku, vendorCwId: this.vendorCwId, price: this.price, cost: this.cost, inactive: this.inactive, salesTaxable: this.salesTaxable, onHand: this.onHand, cwLastUpdated: this.cwLastUpdated, linkedItems: opts?.includeLinkedItems ? this._linkedItems.map((item) => item.toJson()) : undefined, createdAt: this.createdAt, updatedAt: this.updatedAt, }; } }