119 lines
3.4 KiB
TypeScript
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"}`,
|
|
};
|
|
}
|
|
}
|