Files
optima/api/src/services/cw.opportunityService.ts
T

119 lines
3.4 KiB
TypeScript

/**
* @module cw.opportunityService
*
* ConnectWise Opportunity Service
* ================================
*
* Methods for ConnectWise integrations that the opportunity workflow
* calls. Some are still stubs (marked with console.warn); others are
* fully implemented against the CW REST API.
*/
import { opportunityCw } from "../modules/cw-utils/opportunities/opportunities";
import { connectWiseApi } from "../constants";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface TimeEntryInput {
/** CW activity ID to charge the time entry to. */
activityId: number;
/** CW member ID of the user submitting time. */
cwMemberId: number;
/** ISO-8601 datetime when work started. */
timeStart: string;
/** ISO-8601 datetime when work ended. */
timeEnd: string;
notes: string;
}
export interface TimeEntryResult {
success: boolean;
cwTimeEntryId: number | null;
message: string;
}
export interface StatusSyncInput {
opportunityId: number;
statusCwId: number;
}
export interface StatusSyncResult {
success: boolean;
message: string;
}
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
/**
* Submit a time entry to ConnectWise for an opportunity activity.
*
* Called automatically whenever `timeStart` and `timeEnd` are provided
* on a workflow action.
*/
export async function submitTimeEntry(
input: TimeEntryInput,
): Promise<TimeEntryResult> {
try {
const stripMs = (d: string) => d.replace(/\.\d{3}Z$/, "Z");
const response = await connectWiseApi.post("/time/entries", {
member: { id: input.cwMemberId },
chargeToType: "Activity",
chargeToId: input.activityId,
timeStart: stripMs(input.timeStart),
timeEnd: stripMs(input.timeEnd),
notes: input.notes,
});
return {
success: true,
cwTimeEntryId: response.data?.id ?? null,
message: `Time entry ${response.data?.id} created for activity ${input.activityId}.`,
};
} catch (error: any) {
console.error(
`[cw.opportunityService] submitTimeEntry FAILED — activityId=${input.activityId}, cwMemberId=${input.cwMemberId}`,
error?.response?.data ?? error,
);
return {
success: false,
cwTimeEntryId: null,
message: `Failed to submit time entry: ${error?.message ?? "Unknown error"}`,
};
}
}
/**
* Sync an opportunity's status to ConnectWise.
*
* Called whenever the workflow transitions an opportunity to a new
* status, ensuring the CW record stays in sync.
*/
export async function syncOpportunityStatus(
input: StatusSyncInput,
): Promise<StatusSyncResult> {
try {
await opportunityCw.update(input.opportunityId, {
status: { id: input.statusCwId },
});
return {
success: true,
message: `Opportunity ${input.opportunityId} status synced to CW status ID ${input.statusCwId}.`,
};
} catch (error: any) {
console.error(
`[cw.opportunityService] syncOpportunityStatus FAILED — opportunityId=${input.opportunityId}, statusCwId=${input.statusCwId}`,
error?.response?.data ?? error,
);
return {
success: false,
message: `Failed to sync opportunity status: ${error?.message ?? "Unknown error"}`,
};
}
}