feat: add CW members, opportunity create/update, and integrator interceptor

This commit is contained in:
2026-03-07 18:15:17 -06:00
parent 0ce1eda606
commit c0a4d4f919
27 changed files with 2504 additions and 16 deletions
+4
View File
@@ -1,8 +1,10 @@
import { default as fetchAll } from "./opportunities/fetchAll";
import { default as createOpportunity } from "./opportunities/create";
import { default as fetchOpportunityTypes } from "./fetchOpportunityTypes";
import { default as count } from "./opportunities/count";
import { default as fetch } from "./opportunities/[id]/fetch";
import { default as refresh } from "./opportunities/[id]/refresh";
import { default as updateOpportunity } from "./opportunities/[id]/update";
import { default as products } from "./opportunities/[id]/products/fetchAll";
import { default as addProduct } from "./opportunities/[id]/products/add";
import { default as addSpecialOrderProduct } from "./opportunities/[id]/products/addSpecialOrder";
@@ -29,6 +31,7 @@ export {
laborOptions,
addSpecialOrderProduct,
count,
createOpportunity,
fetch,
fetchAll,
fetchOpportunityTypes,
@@ -48,4 +51,5 @@ export {
downloadQuote,
fetchDownloads,
refresh,
updateOpportunity,
};
@@ -0,0 +1,93 @@
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";
const updateSchema = z
.object({
name: z.string().min(1).optional(),
notes: z.string().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() }).optional(),
secondarySalesRep: z.object({ id: z.number() }).nullable().optional(),
company: z.object({ id: z.number() }).optional(),
contact: z.object({ id: z.number() }).nullable().optional(),
site: z.object({ id: z.number() }).nullable().optional(),
expectedCloseDate: z
.string()
.optional()
.transform((v) => (v ? new Date(v).toISOString() : v)),
customerPO: z.string().nullable().optional(),
source: z.string().nullable().optional(),
locationId: z.number().optional(),
businessUnitId: z.number().optional(),
})
.refine((d) => Object.values(d).some((v) => v !== undefined), {
message: "At least one field must be provided",
});
/* PATCH /v1/sales/opportunities/:identifier */
export default createRoute(
"patch",
["/opportunities/:identifier"],
async (c) => {
const identifier = c.req.param("identifier");
const body = await c.req.json();
const data = updateSchema.parse(body);
const item = await opportunities.fetchRecord(identifier);
try {
const updated = await item.updateOpportunity(data);
const response = apiResponse.successful(
"Opportunity updated successfully!",
updated.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 update the opportunity in ConnectWise";
const cwErrors: unknown[] | undefined = Array.isArray(cwData?.errors)
? cwData.errors
: undefined;
return c.json(
{
status: cwStatus,
message: cwMessage,
error: "ConnectWiseUpdateError",
successful: false,
errors: cwErrors,
meta: { timestamp: Date.now() },
},
cwStatus as ContentfulStatusCode,
);
}
throw new GenericError({
status: 500,
name: "OpportunitySaveError",
message: "Failed to save opportunity data",
cause: err instanceof Error ? err.message : String(err),
});
}
},
authMiddleware({ permissions: ["sales.opportunity.update"] }),
);
+86
View File
@@ -0,0 +1,86 @@
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";
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(),
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);
try {
const item = await opportunities.createItem(data);
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"] }),
);