219 lines
6.3 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|