Files
optima/src/controllers/CatalogItemController.ts
T

219 lines
6.3 KiB
TypeScript

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<CatalogItemController>} - The updated controller
*/
public async refreshInventory(): Promise<CatalogItemController> {
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<CatalogItemController>} - The updated controller
*/
public async linkItem(targetId: string): Promise<CatalogItemController> {
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<CatalogItemController>} - The updated controller
*/
public async unlinkItem(targetId: string): Promise<CatalogItemController> {
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<string, any> {
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,
};
}
}