Files
optima/api/src/api/sales/opportunities/create.ts
T
2026-04-07 23:56:31 +00:00

131 lines
4.3 KiB
TypeScript

import { createRoute } from "../../../modules/api-utils/createRoute";
import { opportunities } from "../../../managers/opportunities";
import { apiResponse } from "../../../modules/api-utils/apiResponse";
import { ContentfulStatusCode } from "hono/utils/http-status";
import { authMiddleware } from "../../middleware/authorization";
import GenericError from "../../../Errors/GenericError";
import { z } from "zod";
import { cwMembers } from "../../../managers/cwMembers";
import { prisma } from "../../../constants";
import {
createWorkflowActivity,
OptimaType,
} from "../../../workflows/wf.opportunity";
const createSchema = z.object({
name: z.string().min(1),
expectedCloseDate: z
.string()
.min(1)
.transform((v) => new Date(v).toISOString().replace(/\.\d{3}Z$/, "Z")),
notes: z.string().optional(),
interest: z.enum(["HOT", "WARM", "COLD"]).nullable().optional(),
rating: z.object({ id: z.number() }).optional(),
type: z.object({ id: z.number() }).optional(),
stage: z.object({ id: z.number() }).optional(),
status: z.object({ id: z.number() }).optional(),
priority: z.object({ id: z.number() }).optional(),
campaign: z.object({ id: z.number() }).optional(),
primarySalesRep: z.object({ id: z.number() }),
secondarySalesRep: z.object({ id: z.number() }).nullable().optional(),
company: z.object({ id: z.number() }),
contact: z.object({ id: z.number() }),
site: z.object({ id: z.number() }).nullable().optional(),
source: z.string().nullable().optional(),
customerPO: z.string().nullable().optional(),
locationId: z.number().optional(),
businessUnitId: z.number().optional(),
});
/* POST /v1/sales/opportunities */
export default createRoute(
"post",
["/opportunities"],
async (c) => {
const body = await c.req.json();
const data = createSchema.parse(body);
const { interest, ...cwCreateData } = data;
try {
const item = await opportunities.createItem(cwCreateData);
if (interest !== undefined) {
await prisma.opportunity.update({
where: { uid: item.id },
data: { interest },
});
item.interest = interest;
}
// Create a workflow activity for the new opportunity
try {
const user = c.get("user");
let cwMemberId: number | null = null;
if (user.cwIdentifier) {
const cwMember = await cwMembers.fetch(user.cwIdentifier);
cwMemberId = cwMember.cwMemberId;
}
if (cwMemberId) {
await createWorkflowActivity({
name: `[Workflow] Opportunity created — ${item.name}`,
opportunityCwId: item.cwOpportunityId,
companyCwId: item.companyCwId,
assignToCwMemberId: cwMemberId,
notes: "Opportunity created.",
optimaType: OptimaType.OpportunityCreated,
});
}
} catch (activityErr) {
console.error(
"[Opportunity Create] Failed to create workflow activity:",
activityErr
);
// Don't fail the opportunity creation if the activity fails
}
const response = apiResponse.created(
"Opportunity created successfully!",
item.toJson()
);
return c.json(response, response.status as ContentfulStatusCode);
} catch (err) {
const isAxios =
err != null && typeof err === "object" && "isAxiosError" in err;
if (isAxios) {
const axiosErr = err as any;
const cwStatus: number = axiosErr.response?.status ?? 502;
const cwData = axiosErr.response?.data;
const cwMessage: string =
cwData?.message ?? "Failed to create the opportunity in ConnectWise";
const cwErrors: unknown[] | undefined = Array.isArray(cwData?.errors)
? cwData.errors
: undefined;
return c.json(
{
status: cwStatus,
message: cwMessage,
error: "ConnectWiseCreateError",
successful: false,
errors: cwErrors,
meta: { timestamp: Date.now() },
},
cwStatus as ContentfulStatusCode
);
}
throw new GenericError({
status: 500,
name: "OpportunityCreateError",
message: "Failed to create opportunity",
cause: err instanceof Error ? err.message : String(err),
});
}
},
authMiddleware({ permissions: ["sales.opportunity.create"] })
);