/** * @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 { 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 { 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"}`, }; } }