From 55e96e425cf25f5a3b8e4d95e4bce4a52985316b Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Fri, 26 Jul 2024 02:03:36 +0545 Subject: [PATCH 01/30] feat: getting started with stock-option index file --- .../api/routes/company/stock-option/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/server/api/routes/company/stock-option/index.ts diff --git a/src/server/api/routes/company/stock-option/index.ts b/src/server/api/routes/company/stock-option/index.ts new file mode 100644 index 000000000..3bb7698d1 --- /dev/null +++ b/src/server/api/routes/company/stock-option/index.ts @@ -0,0 +1,14 @@ +import type { PublicAPI } from "@/server/api/hono"; +import create from "./create"; +import deleteOne from "./delete"; +import getMany from "./getMany"; +import getOne from "./getOne"; +import update from "./update"; + +export const registerStockOptionRoutes = (api: PublicAPI) => { + create(api); + getOne(api); + getMany(api); + update(api); + deleteOne(api); +}; From f0bbeb402ead0430048967c6c05c6cc8433d24a2 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:34:05 +0545 Subject: [PATCH 02/30] feat: add stock option service --- .../services/stock-option/add-option.ts | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/server/services/stock-option/add-option.ts diff --git a/src/server/services/stock-option/add-option.ts b/src/server/services/stock-option/add-option.ts new file mode 100644 index 000000000..987999a34 --- /dev/null +++ b/src/server/services/stock-option/add-option.ts @@ -0,0 +1,139 @@ +import { generatePublicId } from "@/common/id"; +import { + type TCreateOptionSchema, + TOptionSchema, +} from "@/server/api/schema/option"; +import { Audit } from "@/server/audit"; +import { db } from "@/server/db"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; + +interface TAddOption { + companyId: string; + requestIP: string; + userAgent: string; + data: TCreateOptionSchema; + memberId: string; + user: { + id: string; + name: string; + }; +} + +export const addOption = async (payload: TAddOption) => { + try { + const { data, user, memberId } = payload; + const documents = data.documents; + + const issuedOption = await db.$transaction(async (tx) => { + const _data = { + grantId: data.grantId, + quantity: data.quantity, + exercisePrice: data.exercisePrice, + type: data.type, + status: data.status, + cliffYears: data.cliffYears, + vestingYears: data.vestingYears, + issueDate: new Date(data.issueDate), + expirationDate: new Date(data.expirationDate), + vestingStartDate: new Date(data.vestingStartDate), + boardApprovalDate: new Date(data.boardApprovalDate), + rule144Date: new Date(data.rule144Date), + + stakeholderId: data.stakeholderId, + equityPlanId: data.equityPlanId, + companyId: payload.companyId, + }; + + const option = await tx.option.create({ data: _data }); + + // biome-ignore lint/suspicious/noExplicitAny: + let auditPromises: any = []; + + if (documents && documents.length > 0) { + const bulkDocuments = documents.map((doc) => ({ + companyId: payload.companyId, + uploaderId: memberId, + publicId: generatePublicId(), + name: doc.name, + bucketId: doc.bucketId, + optionId: option.id, + })); + + const docs = await tx.document.createManyAndReturn({ + data: bulkDocuments, + skipDuplicates: true, + }); + + auditPromises = docs.map((doc) => + Audit.create( + { + action: "document.created", + companyId: payload.companyId, + actor: { type: "user", id: user.id }, + context: { + userAgent: payload.userAgent, + requestIp: payload.requestIP, + }, + target: [{ type: "document", id: doc.id }], + summary: `${user.name} created a document : ${doc.name}`, + }, + tx, + ), + ); + } + + await Promise.all([ + ...auditPromises, + Audit.create( + { + action: "option.created", + companyId: payload.companyId, + actor: { type: "user", id: user.id }, + context: { + userAgent: payload.userAgent, + requestIp: payload.requestIP, + }, + target: [{ type: "option", id: option.id }], + summary: `${user.name} issued an option for stakeholder : ${option.stakeholderId}`, + }, + tx, + ), + ]); + + return option; + }); + + return { + success: true, + message: "🎉 Successfully issued an option.", + data: { + ...issuedOption, + issueDate: issuedOption.issueDate.toISOString(), + rule144Date: issuedOption.rule144Date.toISOString(), + vestingStartDate: issuedOption.vestingStartDate.toISOString(), + boardApprovalDate: issuedOption.boardApprovalDate.toISOString(), + expirationDate: issuedOption.expirationDate.toISOString(), + }, + }; + } catch (error) { + console.error(error); + if (error instanceof PrismaClientKnownRequestError) { + // Unique constraints error code in prisma + if (error.code === "P2002") { + return { + success: false, + code: "BAD_REQUEST", + message: "Please use unique grant Id", + }; + } + } + return { + success: false, + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Something went wrong. Please try again later or contact support.", + }; + } +}; From 10b1daa8987bd21d67b7cb679a9cbb9deda7f74d Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:34:59 +0545 Subject: [PATCH 03/30] feat: add update option service --- .../services/stock-option/update-option.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/server/services/stock-option/update-option.ts diff --git a/src/server/services/stock-option/update-option.ts b/src/server/services/stock-option/update-option.ts new file mode 100644 index 000000000..63f988282 --- /dev/null +++ b/src/server/services/stock-option/update-option.ts @@ -0,0 +1,101 @@ +import { + TOptionSchema, + type TUpdateOptionSchema, +} from "@/server/api/schema/option"; +import type { UpdateShareSchemaType } from "@/server/api/schema/shares"; +import { Audit } from "@/server/audit"; +import { db } from "@/server/db"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; + +export type TUpdateOption = { + optionId: string; + companyId: string; + requestIp: string; + userAgent: string; + user: { + id: string; + name: string; + }; + data: TUpdateOptionSchema; +}; + +export const updateOption = async (payload: TUpdateOption) => { + try { + const { optionId, companyId, user } = payload; + + const existingOption = await db.option.findUnique({ + where: { id: optionId }, + }); + + if (!existingOption) { + return { + success: false, + message: `Option with ID ${optionId} not be found`, + }; + } + + const optionData = { + ...existingOption, + ...payload.data, + }; + + const updatedOption = await db.$transaction(async (tx) => { + const updated = await tx.option.update({ + where: { id: optionId }, + //@ts-ignore + data: optionData, + }); + + const { id: _optionId } = updated; + + await Audit.create( + { + action: "option.updated", + companyId: companyId, + actor: { type: "user", id: user.id }, + context: { + userAgent: payload.userAgent, + requestIp: payload.requestIp, + }, + target: [{ type: "option", id: _optionId }], + summary: `${user.name} updated option with option ID : ${_optionId}`, + }, + tx, + ); + + return updated; + }); + return { + success: true, + message: "🎉 Successfully updated the option.", + data: { + ...updatedOption, + issueDate: updatedOption.issueDate.toISOString(), + rule144Date: updatedOption.rule144Date.toISOString(), + vestingStartDate: updatedOption.vestingStartDate.toISOString(), + boardApprovalDate: updatedOption.boardApprovalDate.toISOString(), + expirationDate: updatedOption.expirationDate.toISOString(), + }, + }; + } catch (error) { + console.error("updateOption", error); + if (error instanceof PrismaClientKnownRequestError) { + // Unique constraints error code in prisma + if (error.code === "P2002") { + return { + success: false, + code: "BAD_REQUEST", + message: "Please use unique grant Id", + }; + } + } + return { + success: false, + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Something went wrong. Please try again or contact support.", + }; + } +}; From 59c63d6c961baa0909ac2df0ea86789b9532e54d Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:35:34 +0545 Subject: [PATCH 04/30] feat: add delete option by Id service --- .../services/stock-option/delete-option.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/server/services/stock-option/delete-option.ts diff --git a/src/server/services/stock-option/delete-option.ts b/src/server/services/stock-option/delete-option.ts new file mode 100644 index 000000000..956a000ce --- /dev/null +++ b/src/server/services/stock-option/delete-option.ts @@ -0,0 +1,77 @@ +import { Audit } from "@/server/audit"; +import { db } from "@/server/db"; + +interface TDeleteOption { + optionId: string; + companyId: string; + userAgent: string; + requestIp: string; + user: { + id: string; + name: string; + }; +} + +export const deleteOption = async ({ + optionId, + companyId, + requestIp, + userAgent, + user, +}: TDeleteOption) => { + try { + const existingOption = await db.option.findUnique({ + where: { + id: optionId, + }, + }); + + if (!existingOption) { + return { + success: false, + code: "NOT_FOUND", + message: `Option with ID ${optionId} not found`, + }; + } + + const option = await db.$transaction(async (tx) => { + const deletedOption = await tx.option.delete({ + where: { + id: optionId, + }, + }); + + const { stakeholderId } = deletedOption; + + await Audit.create( + { + action: "option.deleted", + companyId, + actor: { type: "user", id: user.id }, + context: { + userAgent, + requestIp, + }, + target: [{ type: "option", id: deletedOption.id }], + summary: `${user.name} deleted the option for stakeholder ${stakeholderId}`, + }, + tx, + ); + + return deletedOption; + }); + return { + success: true, + message: "🎉 Successfully deleted the option", + option, + }; + } catch (error) { + console.error("Error Deleting the option: ", error); + return { + success: false, + code: "INTERNAL_SERVER_ERROR", + message: + "Error deleting the option, please try again or contact support.", + }; + } +}; From 94412a8f490fa8a9a799dab4f2382c291e3f680a Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:36:44 +0545 Subject: [PATCH 05/30] feat: add get options service --- .../services/stock-option/get-option.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/server/services/stock-option/get-option.ts diff --git a/src/server/services/stock-option/get-option.ts b/src/server/services/stock-option/get-option.ts new file mode 100644 index 000000000..21545c56d --- /dev/null +++ b/src/server/services/stock-option/get-option.ts @@ -0,0 +1,42 @@ +import { ProxyPrismaModel } from "@/server/api/pagination/prisma-proxy"; +import { db } from "@/server/db"; + +type GetPaginatedOptions = { + companyId: string; + take: number; + cursor?: string; + total?: number; +}; + +export const getPaginatedOptions = async (payload: GetPaginatedOptions) => { + const queryCriteria = { + where: { + companyId: payload.companyId, + }, + orderBy: { + createdAt: "desc", + }, + }; + + const paginationData = { + take: payload.take, + cursor: payload.cursor, + total: payload.total, + }; + + const prismaModel = ProxyPrismaModel(db.option); + + const { data, count, total, cursor } = await prismaModel.findManyPaginated( + queryCriteria, + paginationData, + ); + + return { + data, + meta: { + count, + total, + cursor, + }, + }; +}; From 463ffd4373d70976a8429cecdff742b4e5129590 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:38:11 +0545 Subject: [PATCH 06/30] feat: add create option route --- .../api/routes/company/stock-option/create.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/server/api/routes/company/stock-option/create.ts diff --git a/src/server/api/routes/company/stock-option/create.ts b/src/server/api/routes/company/stock-option/create.ts new file mode 100644 index 000000000..e39ef28cc --- /dev/null +++ b/src/server/api/routes/company/stock-option/create.ts @@ -0,0 +1,100 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { + ApiError, + type ErrorCodeType, + ErrorResponses, +} from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { OptionSchema, type TCreateOptionSchema } from "../../../schema/option"; +import type { TOptionSchema } from "./../../../schema/option"; + +import { CreateOptionSchema } from "@/server/api/schema/option"; +import { getHonoUserAgent, getIp } from "@/server/api/utils"; +import { addOption } from "@/server/services/stock-option/add-option"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; + +const ParamsSchema = z.object({ + id: z + .string() + .cuid() + .openapi({ + description: "Company ID", + param: { + name: "id", + in: "path", + }, + + example: "clycjihpy0002c5fzcyf4gjjc", + }), +}); + +const ResponseSchema = z.object({ + message: z.string(), + data: OptionSchema, +}); + +const route = createRoute({ + method: "post", + path: "/v1/companies/{id}/options", + summary: "Issue option", + description: "Issue stock option to a stakeholder in a company.", + tags: ["Options"], + request: { + params: ParamsSchema, + body: { + content: { + "application/json": { + schema: CreateOptionSchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Issue options to stakeholders", + }, + ...ErrorResponses, + }, +}); + +const create = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company, member, user } = await withCompanyAuth(c); + const body = await c.req.json(); + + const { success, message, data, code } = await addOption({ + requestIP: getIp(c.req), + userAgent: getHonoUserAgent(c.req), + data: body as TCreateOptionSchema, + companyId: company.id, + memberId: member.id, + user: { + id: user.id, + name: user.name || "", + }, + }); + + if (!success) { + throw new ApiError({ + code: code as ErrorCodeType, + message, + }); + } + + return c.json( + { + message: "Stock option added successfully.", + data: data as unknown as TOptionSchema, + }, + 200, + ); + }); +}; + +export default create; From b5ecd59fc2e43b3f81824399a247b603b4200952 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:38:35 +0545 Subject: [PATCH 07/30] feat: add delete option route --- .../api/routes/company/stock-option/delete.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/server/api/routes/company/stock-option/delete.ts diff --git a/src/server/api/routes/company/stock-option/delete.ts b/src/server/api/routes/company/stock-option/delete.ts new file mode 100644 index 000000000..a51ac2065 --- /dev/null +++ b/src/server/api/routes/company/stock-option/delete.ts @@ -0,0 +1,73 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { + ApiError, + type ErrorCodeType, + ErrorResponses, +} from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { getHonoUserAgent, getIp } from "@/server/api/utils"; +import { deleteOption } from "@/server/services/stock-option/delete-option"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; +import { RequestParamsSchema } from "./update"; + +const ResponseSchema = z + .object({ + message: z.string(), + }) + .openapi({ + description: "Delete a Option by ID", + }); + +const route = createRoute({ + method: "delete", + path: "/v1/companies/{id}/options/{optionId}", + summary: "Delete issued options", + description: "Delete a Option by ID", + tags: ["Options"], + request: { + params: RequestParamsSchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Delete a Option by ID", + }, + ...ErrorResponses, + }, +}); + +const deleteOne = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company, user } = await withCompanyAuth(c); + const { optionId: id } = c.req.param(); + + const { success, code, message } = await deleteOption({ + companyId: company.id, + requestIp: getIp(c.req), + userAgent: getHonoUserAgent(c.req), + optionId: id as string, + user: { id: user.id, name: user.name || "" }, + }); + + if (!success) { + throw new ApiError({ + code: code as ErrorCodeType, + message, + }); + } + + return c.json( + { + message: message, + }, + 200, + ); + }); +}; + +export default deleteOne; From b9595646b0bef239b3cce0e5865e3d5e618c63cf Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:38:59 +0545 Subject: [PATCH 08/30] feat: add getMany option route --- .../routes/company/stock-option/getMany.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/server/api/routes/company/stock-option/getMany.ts diff --git a/src/server/api/routes/company/stock-option/getMany.ts b/src/server/api/routes/company/stock-option/getMany.ts new file mode 100644 index 000000000..cb9d404ed --- /dev/null +++ b/src/server/api/routes/company/stock-option/getMany.ts @@ -0,0 +1,84 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { ErrorResponses } from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { OptionSchema } from "@/server/api/schema/option"; +import { + DEFAULT_PAGINATION_LIMIT, + PaginationQuerySchema, + PaginationResponseSchema, +} from "@/server/api/schema/pagination"; +import { getPaginatedOptions } from "@/server/services/stock-option/get-option"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; + +const ParamsSchema = z.object({ + id: z + .string() + .cuid() + .openapi({ + description: "Company ID", + param: { + name: "id", + in: "path", + }, + + example: "clxwbok580000i7nge8nm1ry0", + }), +}); + +const ResponseSchema = z + .object({ + data: z.array(OptionSchema), + meta: PaginationResponseSchema, + }) + .openapi({ + description: "Get Options by Company ID", + }); + +const route = createRoute({ + method: "get", + path: "/v1/companies/{id}/options", + summary: "Get list of issued options", + description: "Get list of issued options for a company", + tags: ["Options"], + request: { + params: ParamsSchema, + query: PaginationQuerySchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Retrieve the options for the company", + }, + ...ErrorResponses, + }, +}); + +const getMany = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company } = await withCompanyAuth(c); + + const { limit, cursor, total } = c.req.query(); + + const { data, meta } = await getPaginatedOptions({ + companyId: company.id, + take: Number(limit || DEFAULT_PAGINATION_LIMIT), + cursor, + total: Number(total), + }); + + return c.json( + { + data, + meta, + }, + 200, + ); + }); +}; + +export default getMany; From d81a193391e03d3d276b240923312bb70fbfb59c Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:39:13 +0545 Subject: [PATCH 09/30] feat: add getOne option route --- .../api/routes/company/stock-option/getOne.ts | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/server/api/routes/company/stock-option/getOne.ts diff --git a/src/server/api/routes/company/stock-option/getOne.ts b/src/server/api/routes/company/stock-option/getOne.ts new file mode 100644 index 000000000..a516a4da2 --- /dev/null +++ b/src/server/api/routes/company/stock-option/getOne.ts @@ -0,0 +1,96 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { ApiError, ErrorResponses } from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; +import { OptionSchema, type TOptionSchema } from "@/server/api/schema/option"; +import { db } from "@/server/db"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; + +const ParamsSchema = z.object({ + id: z + .string() + .cuid() + .openapi({ + description: "Company ID", + param: { + name: "id", + in: "path", + }, + + example: "clxwbok580000i7nge8nm1ry0", + }), + optionId: z + .string() + .cuid() + .openapi({ + description: "Option ID", + param: { + name: "optionId", + in: "path", + }, + + example: "clyd3i9sw000008ij619eabva", + }), +}); + +const ResponseSchema = z + .object({ + data: OptionSchema, + }) + .openapi({ + description: "Get a single stock option by ID", + }); + +const route = createRoute({ + method: "get", + path: "/v1/companies/{id}/options/{optionId}", + summary: "Get an issued option by ID", + description: "Get a single issued option record by ID", + tags: ["Options"], + request: { + params: ParamsSchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Retrieve the option for the company", + }, + ...ErrorResponses, + }, +}); + +const getOne = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company } = await withCompanyAuth(c); + + // id destructured to companyId and optionId destructured to id + const { optionId: id } = c.req.param(); + + const option = (await db.option.findUnique({ + where: { + id, + companyId: company.id, + }, + })) as unknown as TOptionSchema; + + if (!option) { + throw new ApiError({ + code: "NOT_FOUND", + message: "Option not found", + }); + } + + return c.json( + { + data: option, + }, + 200, + ); + }); +}; + +export default getOne; From 83004a5d31f9ef44473a3ae6cbfe1a18e8cafe53 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:39:49 +0545 Subject: [PATCH 10/30] feat: add update option route --- .../api/routes/company/stock-option/update.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/server/api/routes/company/stock-option/update.ts diff --git a/src/server/api/routes/company/stock-option/update.ts b/src/server/api/routes/company/stock-option/update.ts new file mode 100644 index 000000000..2bbfaa04f --- /dev/null +++ b/src/server/api/routes/company/stock-option/update.ts @@ -0,0 +1,126 @@ +import { withCompanyAuth } from "@/server/api/auth"; +import { + ApiError, + type ErrorCodeType, + ErrorResponses, +} from "@/server/api/error"; +import type { PublicAPI } from "@/server/api/hono"; + +import { + OptionSchema, + type TOptionSchema, + type TUpdateOptionSchema, + UpdateOptionSchema, +} from "@/server/api/schema/option"; +import { getHonoUserAgent, getIp } from "@/server/api/utils"; +import { updateOption } from "@/server/services/stock-option/update-option"; +import { createRoute, z } from "@hono/zod-openapi"; +import type { Context } from "hono"; + +export const RequestParamsSchema = z + .object({ + id: z + .string() + .cuid() + .openapi({ + description: "Company ID", + param: { + name: "id", + in: "path", + }, + + example: "clxwbok580000i7nge8nm1ry0", + }), + optionId: z + .string() + .cuid() + .openapi({ + description: "Option ID", + param: { + name: "optionId", + in: "path", + }, + + example: "clyd3i9sw000008ij619eabva", + }), + }) + .openapi({ + description: "Update a Option by ID", + }); + +const ResponseSchema = z + .object({ + message: z.string(), + data: OptionSchema, + }) + .openapi({ + description: "Update a Option by ID", + }); + +const route = createRoute({ + method: "put", + path: "/v1/companies/{id}/options/{optionId}", + summary: "Update issued options by ID", + description: "Update issued options by option ID", + tags: ["Options"], + request: { + params: RequestParamsSchema, + body: { + content: { + "application/json": { + schema: UpdateOptionSchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Update the option by ID", + }, + ...ErrorResponses, + }, +}); + +const getOne = (app: PublicAPI) => { + app.openapi(route, async (c: Context) => { + const { company, user } = await withCompanyAuth(c); + const { optionId } = c.req.param(); + const body = await c.req.json(); + + const payload = { + optionId: optionId as string, + companyId: company.id, + requestIp: getIp(c.req), + userAgent: getHonoUserAgent(c.req), + data: body as TUpdateOptionSchema, + user: { + id: user.id, + name: user.name as string, + }, + }; + + const { success, message, code, data } = await updateOption(payload); + + if (!success) { + throw new ApiError({ + code: code as ErrorCodeType, + message, + }); + } + + return c.json( + { + message: message, + data: data as unknown as TOptionSchema, + }, + 200, + ); + }); +}; + +export default getOne; From ad4791ec484a12be431ae9c02cc0d491e88408a6 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:40:28 +0545 Subject: [PATCH 11/30] feat: zod schemas for option endpoint --- src/server/api/schema/option.ts | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/server/api/schema/option.ts diff --git a/src/server/api/schema/option.ts b/src/server/api/schema/option.ts new file mode 100644 index 000000000..2ec1b04a9 --- /dev/null +++ b/src/server/api/schema/option.ts @@ -0,0 +1,156 @@ +import { OptionStatusEnum, OptionTypeEnum } from "@/prisma/enums"; +import { z } from "zod"; + +const OptionTypeArray = Object.values(OptionTypeEnum) as [ + OptionTypeEnum, + ...OptionTypeEnum[], +]; +const OptionStatusArray = Object.values(OptionStatusEnum) as [ + OptionStatusEnum, + ...OptionStatusEnum[], +]; + +export const OptionSchema = z + .object({ + id: z.string().optional().openapi({ + description: "Stock Option ID", + example: "abc123", + }), + + grantId: z.string().openapi({ + description: "Grant ID", + example: "grant123", + }), + + quantity: z.coerce.number().min(0).openapi({ + description: "Quantity of Stock Options", + example: 100, + }), + + exercisePrice: z.coerce.number().min(0).openapi({ + description: "Exercise Price per Stock Option", + example: 10.5, + }), + + type: z.enum(OptionTypeArray).openapi({ + description: "Type of Stock Option", + example: "ISO", + }), + + status: z.enum(OptionStatusArray).openapi({ + description: "Status of Stock Option", + example: "DRAFT", + }), + + cliffYears: z.coerce.number().min(0).openapi({ + description: "Cliff Years", + example: 1, + }), + + vestingYears: z.coerce.number().min(0).openapi({ + description: "Vesting Years", + example: 4, + }), + + issueDate: z.string().datetime().openapi({ + description: "Issue Date", + example: "2024-01-01", + }), + + expirationDate: z.string().datetime().openapi({ + description: "Expiration Date", + example: "2028-01-01", + }), + + vestingStartDate: z.string().datetime().openapi({ + description: "Vesting Start Date", + example: "2024-01-01", + }), + + boardApprovalDate: z.string().datetime().openapi({ + description: "Board Approval Date", + example: "2024-01-01", + }), + + rule144Date: z.string().datetime().openapi({ + description: "Rule 144 Date", + example: "2024-01-01", + }), + + stakeholderId: z.string().openapi({ + description: "Stakeholder ID", + example: "stakeholder123", + }), + + companyId: z.string().cuid().openapi({ + description: "Company ID", + example: "clyvb28ak0000f1ngcn2i0p2m", + }), + + equityPlanId: z.string().openapi({ + description: "Equity Plan ID", + example: "equityPlan123", + }), + + documents: z + .array( + z.object({ + bucketId: z.string(), + name: z.string(), + }), + ) + .optional() + .openapi({ + description: "Documents", + example: [ + { bucketId: "clyvb28ak0000f1ngcn2i0p2m", name: "Esign docs" }, + ], + }), + + createdAt: z.string().date().nullish().openapi({ + description: "Option Created at", + example: "2024-01-01T00:00:00.000Z", + }), + + updatedAt: z.string().date().nullish().openapi({ + description: "Option Updated at", + example: "2024-01-01T00:00:00.000Z", + }), + }) + .openapi({ + description: "Get a Single Option by the ID", + }); + +export const CreateOptionSchema = OptionSchema.omit({ + id: true, + createdAt: true, + updatedAt: true, + companyId: true, +}) + .strict() + .openapi({ + description: "Issue options to a stakeholder in a company.", + }); + +export const UpdateOptionSchema = OptionSchema.omit({ + id: true, + createdAt: true, + updatedAt: true, +}) + .partial() + .strict() + .refine( + (data) => { + return Object.values(data).some((value) => value !== undefined); + }, + { + message: "At least one field must be provided to update.", + }, + ) + .openapi({ + description: "Update an option by ID", + }); + +export type TOptionSchema = z.infer; +export type TCreateOptionSchema = z.infer; +export type TUpdateOptionSchema = z.infer; From 626071c0647593dc4371eace882bb831da7c661e Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:41:27 +0545 Subject: [PATCH 12/30] feat: export option routes --- src/server/api/routes/company/stock-option/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/routes/company/stock-option/index.ts b/src/server/api/routes/company/stock-option/index.ts index 3bb7698d1..75b3ea56b 100644 --- a/src/server/api/routes/company/stock-option/index.ts +++ b/src/server/api/routes/company/stock-option/index.ts @@ -5,7 +5,7 @@ import getMany from "./getMany"; import getOne from "./getOne"; import update from "./update"; -export const registerStockOptionRoutes = (api: PublicAPI) => { +export const registerOptionRoutes = (api: PublicAPI) => { create(api); getOne(api); getMany(api); From 4e4e928d845e4e5daae6a2c17f952f730678ee7c Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:42:22 +0545 Subject: [PATCH 13/30] fix: conflict in index --- src/server/api/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 7e919c248..65171e0fe 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -4,6 +4,8 @@ import { registerCompanyRoutes } from "./routes/company"; import { registerShareRoutes } from "./routes/share"; import { registerStakeholderRoutes } from "./routes/stakeholder"; + + const api = PublicAPI(); api.use("*", middlewareServices()); @@ -12,5 +14,6 @@ api.use("*", middlewareServices()); registerCompanyRoutes(api); registerShareRoutes(api); registerStakeholderRoutes(api); +// registerOptionRoutes(api); export default api; From 5f8cea42948a91921cdfdb743b031974659669ec Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 29 Jul 2024 02:43:34 +0545 Subject: [PATCH 14/30] feat: audit events for docs added --- src/server/audit/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/audit/schema.ts b/src/server/audit/schema.ts index d952ead73..6c2b29dde 100644 --- a/src/server/audit/schema.ts +++ b/src/server/audit/schema.ts @@ -36,6 +36,7 @@ export const AuditSchema = z.object({ "document.deleted", "option.created", + "option.updated", "option.deleted", "share.created", From bb137b1939a63cf165eb9fe9197fb90e92e39c8f Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Tue, 30 Jul 2024 01:34:09 +0545 Subject: [PATCH 15/30] feat: schema update --- src/server/api/schema/option.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/server/api/schema/option.ts b/src/server/api/schema/option.ts index 2ec1b04a9..5a3e091ae 100644 --- a/src/server/api/schema/option.ts +++ b/src/server/api/schema/option.ts @@ -52,34 +52,34 @@ export const OptionSchema = z example: 4, }), - issueDate: z.string().datetime().openapi({ + issueDate: z.string().date().openapi({ description: "Issue Date", example: "2024-01-01", }), - expirationDate: z.string().datetime().openapi({ + expirationDate: z.string().date().openapi({ description: "Expiration Date", example: "2028-01-01", }), - vestingStartDate: z.string().datetime().openapi({ + vestingStartDate: z.string().date().openapi({ description: "Vesting Start Date", example: "2024-01-01", }), - boardApprovalDate: z.string().datetime().openapi({ + boardApprovalDate: z.string().date().openapi({ description: "Board Approval Date", example: "2024-01-01", }), - rule144Date: z.string().datetime().openapi({ + rule144Date: z.string().date().openapi({ description: "Rule 144 Date", example: "2024-01-01", }), stakeholderId: z.string().openapi({ description: "Stakeholder ID", - example: "stakeholder123", + example: "clz5vr0bd0001tqroiuc7lw1b", }), companyId: z.string().cuid().openapi({ @@ -89,7 +89,7 @@ export const OptionSchema = z equityPlanId: z.string().openapi({ description: "Equity Plan ID", - example: "equityPlan123", + example: "clz5vtipf0003tqrovvrpepp8", }), documents: z @@ -118,7 +118,7 @@ export const OptionSchema = z }), }) .openapi({ - description: "Get a Single Option by the ID", + description: "Get a single option by ID", }); export const CreateOptionSchema = OptionSchema.omit({ @@ -134,6 +134,8 @@ export const CreateOptionSchema = OptionSchema.omit({ export const UpdateOptionSchema = OptionSchema.omit({ id: true, + documents: true, + companyId: true, createdAt: true, updatedAt: true, }) From 35c2f58436b0fb60a69debbb5e0f19d3e063044c Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Tue, 30 Jul 2024 01:37:05 +0545 Subject: [PATCH 16/30] fix: type and date conversion typo --- .../services/stock-option/update-option.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/server/services/stock-option/update-option.ts b/src/server/services/stock-option/update-option.ts index 63f988282..8596e407c 100644 --- a/src/server/services/stock-option/update-option.ts +++ b/src/server/services/stock-option/update-option.ts @@ -1,8 +1,4 @@ -import { - TOptionSchema, - type TUpdateOptionSchema, -} from "@/server/api/schema/option"; -import type { UpdateShareSchemaType } from "@/server/api/schema/shares"; +import type { TUpdateOptionSchema } from "@/server/api/schema/option"; import { Audit } from "@/server/audit"; import { db } from "@/server/db"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; @@ -39,7 +35,7 @@ export const updateOption = async (payload: TUpdateOption) => { ...payload.data, }; - const updatedOption = await db.$transaction(async (tx) => { + const updated = await db.$transaction(async (tx) => { const updated = await tx.option.update({ where: { id: optionId }, //@ts-ignore @@ -68,19 +64,11 @@ export const updateOption = async (payload: TUpdateOption) => { return { success: true, message: "🎉 Successfully updated the option.", - data: { - ...updatedOption, - issueDate: updatedOption.issueDate.toISOString(), - rule144Date: updatedOption.rule144Date.toISOString(), - vestingStartDate: updatedOption.vestingStartDate.toISOString(), - boardApprovalDate: updatedOption.boardApprovalDate.toISOString(), - expirationDate: updatedOption.expirationDate.toISOString(), - }, + data: updated, }; } catch (error) { console.error("updateOption", error); if (error instanceof PrismaClientKnownRequestError) { - // Unique constraints error code in prisma if (error.code === "P2002") { return { success: false, From 32481dd56d1f9642ee7549a036e36976886f2ad2 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Tue, 30 Jul 2024 01:38:01 +0545 Subject: [PATCH 17/30] fix: type and date conversion typo in add-option service --- .../services/stock-option/add-option.ts | 47 +++++++------------ 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/src/server/services/stock-option/add-option.ts b/src/server/services/stock-option/add-option.ts index 987999a34..b2ba25b61 100644 --- a/src/server/services/stock-option/add-option.ts +++ b/src/server/services/stock-option/add-option.ts @@ -1,30 +1,23 @@ import { generatePublicId } from "@/common/id"; -import { - type TCreateOptionSchema, - TOptionSchema, -} from "@/server/api/schema/option"; +import type { TCreateOptionSchema } from "@/server/api/schema/option"; import { Audit } from "@/server/audit"; import { db } from "@/server/db"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import type { TUpdateOption } from "./update-option"; -interface TAddOption { - companyId: string; - requestIP: string; - userAgent: string; +type AuditPromise = ReturnType; + +type TAddOption = Omit & { data: TCreateOptionSchema; memberId: string; - user: { - id: string; - name: string; - }; -} +}; export const addOption = async (payload: TAddOption) => { try { const { data, user, memberId } = payload; const documents = data.documents; - const issuedOption = await db.$transaction(async (tx) => { + const newOption = await db.$transaction(async (tx) => { const _data = { grantId: data.grantId, quantity: data.quantity, @@ -46,8 +39,7 @@ export const addOption = async (payload: TAddOption) => { const option = await tx.option.create({ data: _data }); - // biome-ignore lint/suspicious/noExplicitAny: - let auditPromises: any = []; + let auditPromises: AuditPromise[] = []; if (documents && documents.length > 0) { const bulkDocuments = documents.map((doc) => ({ @@ -72,18 +64,17 @@ export const addOption = async (payload: TAddOption) => { actor: { type: "user", id: user.id }, context: { userAgent: payload.userAgent, - requestIp: payload.requestIP, + requestIp: payload.requestIp, }, target: [{ type: "document", id: doc.id }], - summary: `${user.name} created a document : ${doc.name}`, + summary: `${user.name} created a document while issuing a stock option : ${doc.name}`, }, tx, ), ); } - await Promise.all([ - ...auditPromises, + auditPromises.push( Audit.create( { action: "option.created", @@ -91,14 +82,16 @@ export const addOption = async (payload: TAddOption) => { actor: { type: "user", id: user.id }, context: { userAgent: payload.userAgent, - requestIp: payload.requestIP, + requestIp: payload.requestIp, }, target: [{ type: "option", id: option.id }], summary: `${user.name} issued an option for stakeholder : ${option.stakeholderId}`, }, tx, ), - ]); + ); + + await Promise.all(auditPromises); return option; }); @@ -106,19 +99,13 @@ export const addOption = async (payload: TAddOption) => { return { success: true, message: "🎉 Successfully issued an option.", - data: { - ...issuedOption, - issueDate: issuedOption.issueDate.toISOString(), - rule144Date: issuedOption.rule144Date.toISOString(), - vestingStartDate: issuedOption.vestingStartDate.toISOString(), - boardApprovalDate: issuedOption.boardApprovalDate.toISOString(), - expirationDate: issuedOption.expirationDate.toISOString(), - }, + data: newOption, }; } catch (error) { console.error(error); if (error instanceof PrismaClientKnownRequestError) { // Unique constraints error code in prisma + // Only grantId column can throw this error code if (error.code === "P2002") { return { success: false, From e373359d7c601afc914b72cbccc22991145dbb2e Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Tue, 30 Jul 2024 01:38:43 +0545 Subject: [PATCH 18/30] fix: type in delete-option service --- .../services/stock-option/delete-option.ts | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/server/services/stock-option/delete-option.ts b/src/server/services/stock-option/delete-option.ts index 956a000ce..53142a27b 100644 --- a/src/server/services/stock-option/delete-option.ts +++ b/src/server/services/stock-option/delete-option.ts @@ -1,16 +1,8 @@ import { Audit } from "@/server/audit"; import { db } from "@/server/db"; +import type { TUpdateOption } from "./update-option"; -interface TDeleteOption { - optionId: string; - companyId: string; - userAgent: string; - requestIp: string; - user: { - id: string; - name: string; - }; -} +type TDeleteOption = Omit; export const deleteOption = async ({ optionId, @@ -35,13 +27,13 @@ export const deleteOption = async ({ } const option = await db.$transaction(async (tx) => { - const deletedOption = await tx.option.delete({ + const deleted = await tx.option.delete({ where: { id: optionId, }, }); - const { stakeholderId } = deletedOption; + const { stakeholderId } = deleted; await Audit.create( { @@ -52,13 +44,13 @@ export const deleteOption = async ({ userAgent, requestIp, }, - target: [{ type: "option", id: deletedOption.id }], + target: [{ type: "option", id: deleted.id }], summary: `${user.name} deleted the option for stakeholder ${stakeholderId}`, }, tx, ); - return deletedOption; + return deleted; }); return { success: true, @@ -71,7 +63,9 @@ export const deleteOption = async ({ success: false, code: "INTERNAL_SERVER_ERROR", message: - "Error deleting the option, please try again or contact support.", + error instanceof Error + ? error.message + : "Error deleting the option, please try again or contact support.", }; } }; From 44dbf595e1725250760c505f640f1f551d9eb13d Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Tue, 30 Jul 2024 01:40:27 +0545 Subject: [PATCH 19/30] chore: unknown type for hono response --- .../api/routes/company/stock-option/create.ts | 17 +++++++++-------- .../api/routes/company/stock-option/delete.ts | 4 ++-- .../api/routes/company/stock-option/getOne.ts | 7 +++---- .../api/routes/company/stock-option/update.ts | 19 +++++++++---------- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/server/api/routes/company/stock-option/create.ts b/src/server/api/routes/company/stock-option/create.ts index e39ef28cc..eaf7248ec 100644 --- a/src/server/api/routes/company/stock-option/create.ts +++ b/src/server/api/routes/company/stock-option/create.ts @@ -5,10 +5,11 @@ import { ErrorResponses, } from "@/server/api/error"; import type { PublicAPI } from "@/server/api/hono"; -import { OptionSchema, type TCreateOptionSchema } from "../../../schema/option"; -import type { TOptionSchema } from "./../../../schema/option"; - import { CreateOptionSchema } from "@/server/api/schema/option"; +import { + OptionSchema, + type TCreateOptionSchema, +} from "@/server/api/schema/option"; import { getHonoUserAgent, getIp } from "@/server/api/utils"; import { addOption } from "@/server/services/stock-option/add-option"; import { createRoute, z } from "@hono/zod-openapi"; @@ -37,7 +38,7 @@ const ResponseSchema = z.object({ const route = createRoute({ method: "post", path: "/v1/companies/{id}/options", - summary: "Issue option", + summary: "Issue a new stock option", description: "Issue stock option to a stakeholder in a company.", tags: ["Options"], request: { @@ -69,7 +70,7 @@ const create = (app: PublicAPI) => { const body = await c.req.json(); const { success, message, data, code } = await addOption({ - requestIP: getIp(c.req), + requestIp: getIp(c.req), userAgent: getHonoUserAgent(c.req), data: body as TCreateOptionSchema, companyId: company.id, @@ -80,7 +81,7 @@ const create = (app: PublicAPI) => { }, }); - if (!success) { + if (!success || !data) { throw new ApiError({ code: code as ErrorCodeType, message, @@ -89,8 +90,8 @@ const create = (app: PublicAPI) => { return c.json( { - message: "Stock option added successfully.", - data: data as unknown as TOptionSchema, + message, + data, }, 200, ); diff --git a/src/server/api/routes/company/stock-option/delete.ts b/src/server/api/routes/company/stock-option/delete.ts index a51ac2065..343a088c5 100644 --- a/src/server/api/routes/company/stock-option/delete.ts +++ b/src/server/api/routes/company/stock-option/delete.ts @@ -22,8 +22,8 @@ const ResponseSchema = z const route = createRoute({ method: "delete", path: "/v1/companies/{id}/options/{optionId}", - summary: "Delete issued options", - description: "Delete a Option by ID", + summary: "Delete an option by ID", + description: "Delete an Option by ID", tags: ["Options"], request: { params: RequestParamsSchema, diff --git a/src/server/api/routes/company/stock-option/getOne.ts b/src/server/api/routes/company/stock-option/getOne.ts index a516a4da2..c2a46e72e 100644 --- a/src/server/api/routes/company/stock-option/getOne.ts +++ b/src/server/api/routes/company/stock-option/getOne.ts @@ -67,20 +67,19 @@ const getOne = (app: PublicAPI) => { app.openapi(route, async (c: Context) => { const { company } = await withCompanyAuth(c); - // id destructured to companyId and optionId destructured to id const { optionId: id } = c.req.param(); - const option = (await db.option.findUnique({ + const option = await db.option.findUnique({ where: { id, companyId: company.id, }, - })) as unknown as TOptionSchema; + }); if (!option) { throw new ApiError({ code: "NOT_FOUND", - message: "Option not found", + message: "Required option not found", }); } diff --git a/src/server/api/routes/company/stock-option/update.ts b/src/server/api/routes/company/stock-option/update.ts index 2bbfaa04f..3b5b3a126 100644 --- a/src/server/api/routes/company/stock-option/update.ts +++ b/src/server/api/routes/company/stock-option/update.ts @@ -8,7 +8,6 @@ import type { PublicAPI } from "@/server/api/hono"; import { OptionSchema, - type TOptionSchema, type TUpdateOptionSchema, UpdateOptionSchema, } from "@/server/api/schema/option"; @@ -45,7 +44,7 @@ export const RequestParamsSchema = z }), }) .openapi({ - description: "Update a Option by ID", + description: "Update an option by ID", }); const ResponseSchema = z @@ -54,14 +53,14 @@ const ResponseSchema = z data: OptionSchema, }) .openapi({ - description: "Update a Option by ID", + description: "Update an option by ID", }); const route = createRoute({ method: "put", path: "/v1/companies/{id}/options/{optionId}", - summary: "Update issued options by ID", - description: "Update issued options by option ID", + summary: "Update an issued option by ID", + description: "Update issued option by option ID", tags: ["Options"], request: { params: RequestParamsSchema, @@ -86,7 +85,7 @@ const route = createRoute({ }, }); -const getOne = (app: PublicAPI) => { +const update = (app: PublicAPI) => { app.openapi(route, async (c: Context) => { const { company, user } = await withCompanyAuth(c); const { optionId } = c.req.param(); @@ -106,7 +105,7 @@ const getOne = (app: PublicAPI) => { const { success, message, code, data } = await updateOption(payload); - if (!success) { + if (!success || !data) { throw new ApiError({ code: code as ErrorCodeType, message, @@ -115,12 +114,12 @@ const getOne = (app: PublicAPI) => { return c.json( { - message: message, - data: data as unknown as TOptionSchema, + message, + data, }, 200, ); }); }; -export default getOne; +export default update; From bb8dd592704fbc6b0d381143d94920dfe690517d Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:49:58 +0545 Subject: [PATCH 20/30] chore: refactoring for adapting new auth middleware --- .../api/routes/company/stock-option/create.ts | 101 -------------- .../api/routes/company/stock-option/delete.ts | 73 ---------- .../routes/company/stock-option/getMany.ts | 84 ------------ .../api/routes/company/stock-option/getOne.ts | 95 ------------- .../api/routes/company/stock-option/index.ts | 14 -- .../api/routes/company/stock-option/update.ts | 125 ----------------- .../services/stock-option/add-option.ts | 126 ------------------ .../services/stock-option/delete-option.ts | 71 ---------- .../services/stock-option/get-option.ts | 42 ------ .../services/stock-option/update-option.ts | 89 ------------- 10 files changed, 820 deletions(-) delete mode 100644 src/server/api/routes/company/stock-option/create.ts delete mode 100644 src/server/api/routes/company/stock-option/delete.ts delete mode 100644 src/server/api/routes/company/stock-option/getMany.ts delete mode 100644 src/server/api/routes/company/stock-option/getOne.ts delete mode 100644 src/server/api/routes/company/stock-option/index.ts delete mode 100644 src/server/api/routes/company/stock-option/update.ts delete mode 100644 src/server/services/stock-option/add-option.ts delete mode 100644 src/server/services/stock-option/delete-option.ts delete mode 100644 src/server/services/stock-option/get-option.ts delete mode 100644 src/server/services/stock-option/update-option.ts diff --git a/src/server/api/routes/company/stock-option/create.ts b/src/server/api/routes/company/stock-option/create.ts deleted file mode 100644 index eaf7248ec..000000000 --- a/src/server/api/routes/company/stock-option/create.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { withCompanyAuth } from "@/server/api/auth"; -import { - ApiError, - type ErrorCodeType, - ErrorResponses, -} from "@/server/api/error"; -import type { PublicAPI } from "@/server/api/hono"; -import { CreateOptionSchema } from "@/server/api/schema/option"; -import { - OptionSchema, - type TCreateOptionSchema, -} from "@/server/api/schema/option"; -import { getHonoUserAgent, getIp } from "@/server/api/utils"; -import { addOption } from "@/server/services/stock-option/add-option"; -import { createRoute, z } from "@hono/zod-openapi"; -import type { Context } from "hono"; - -const ParamsSchema = z.object({ - id: z - .string() - .cuid() - .openapi({ - description: "Company ID", - param: { - name: "id", - in: "path", - }, - - example: "clycjihpy0002c5fzcyf4gjjc", - }), -}); - -const ResponseSchema = z.object({ - message: z.string(), - data: OptionSchema, -}); - -const route = createRoute({ - method: "post", - path: "/v1/companies/{id}/options", - summary: "Issue a new stock option", - description: "Issue stock option to a stakeholder in a company.", - tags: ["Options"], - request: { - params: ParamsSchema, - body: { - content: { - "application/json": { - schema: CreateOptionSchema, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: ResponseSchema, - }, - }, - description: "Issue options to stakeholders", - }, - ...ErrorResponses, - }, -}); - -const create = (app: PublicAPI) => { - app.openapi(route, async (c: Context) => { - const { company, member, user } = await withCompanyAuth(c); - const body = await c.req.json(); - - const { success, message, data, code } = await addOption({ - requestIp: getIp(c.req), - userAgent: getHonoUserAgent(c.req), - data: body as TCreateOptionSchema, - companyId: company.id, - memberId: member.id, - user: { - id: user.id, - name: user.name || "", - }, - }); - - if (!success || !data) { - throw new ApiError({ - code: code as ErrorCodeType, - message, - }); - } - - return c.json( - { - message, - data, - }, - 200, - ); - }); -}; - -export default create; diff --git a/src/server/api/routes/company/stock-option/delete.ts b/src/server/api/routes/company/stock-option/delete.ts deleted file mode 100644 index 343a088c5..000000000 --- a/src/server/api/routes/company/stock-option/delete.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { withCompanyAuth } from "@/server/api/auth"; -import { - ApiError, - type ErrorCodeType, - ErrorResponses, -} from "@/server/api/error"; -import type { PublicAPI } from "@/server/api/hono"; -import { getHonoUserAgent, getIp } from "@/server/api/utils"; -import { deleteOption } from "@/server/services/stock-option/delete-option"; -import { createRoute, z } from "@hono/zod-openapi"; -import type { Context } from "hono"; -import { RequestParamsSchema } from "./update"; - -const ResponseSchema = z - .object({ - message: z.string(), - }) - .openapi({ - description: "Delete a Option by ID", - }); - -const route = createRoute({ - method: "delete", - path: "/v1/companies/{id}/options/{optionId}", - summary: "Delete an option by ID", - description: "Delete an Option by ID", - tags: ["Options"], - request: { - params: RequestParamsSchema, - }, - responses: { - 200: { - content: { - "application/json": { - schema: ResponseSchema, - }, - }, - description: "Delete a Option by ID", - }, - ...ErrorResponses, - }, -}); - -const deleteOne = (app: PublicAPI) => { - app.openapi(route, async (c: Context) => { - const { company, user } = await withCompanyAuth(c); - const { optionId: id } = c.req.param(); - - const { success, code, message } = await deleteOption({ - companyId: company.id, - requestIp: getIp(c.req), - userAgent: getHonoUserAgent(c.req), - optionId: id as string, - user: { id: user.id, name: user.name || "" }, - }); - - if (!success) { - throw new ApiError({ - code: code as ErrorCodeType, - message, - }); - } - - return c.json( - { - message: message, - }, - 200, - ); - }); -}; - -export default deleteOne; diff --git a/src/server/api/routes/company/stock-option/getMany.ts b/src/server/api/routes/company/stock-option/getMany.ts deleted file mode 100644 index cb9d404ed..000000000 --- a/src/server/api/routes/company/stock-option/getMany.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { withCompanyAuth } from "@/server/api/auth"; -import { ErrorResponses } from "@/server/api/error"; -import type { PublicAPI } from "@/server/api/hono"; -import { OptionSchema } from "@/server/api/schema/option"; -import { - DEFAULT_PAGINATION_LIMIT, - PaginationQuerySchema, - PaginationResponseSchema, -} from "@/server/api/schema/pagination"; -import { getPaginatedOptions } from "@/server/services/stock-option/get-option"; -import { createRoute, z } from "@hono/zod-openapi"; -import type { Context } from "hono"; - -const ParamsSchema = z.object({ - id: z - .string() - .cuid() - .openapi({ - description: "Company ID", - param: { - name: "id", - in: "path", - }, - - example: "clxwbok580000i7nge8nm1ry0", - }), -}); - -const ResponseSchema = z - .object({ - data: z.array(OptionSchema), - meta: PaginationResponseSchema, - }) - .openapi({ - description: "Get Options by Company ID", - }); - -const route = createRoute({ - method: "get", - path: "/v1/companies/{id}/options", - summary: "Get list of issued options", - description: "Get list of issued options for a company", - tags: ["Options"], - request: { - params: ParamsSchema, - query: PaginationQuerySchema, - }, - responses: { - 200: { - content: { - "application/json": { - schema: ResponseSchema, - }, - }, - description: "Retrieve the options for the company", - }, - ...ErrorResponses, - }, -}); - -const getMany = (app: PublicAPI) => { - app.openapi(route, async (c: Context) => { - const { company } = await withCompanyAuth(c); - - const { limit, cursor, total } = c.req.query(); - - const { data, meta } = await getPaginatedOptions({ - companyId: company.id, - take: Number(limit || DEFAULT_PAGINATION_LIMIT), - cursor, - total: Number(total), - }); - - return c.json( - { - data, - meta, - }, - 200, - ); - }); -}; - -export default getMany; diff --git a/src/server/api/routes/company/stock-option/getOne.ts b/src/server/api/routes/company/stock-option/getOne.ts deleted file mode 100644 index c2a46e72e..000000000 --- a/src/server/api/routes/company/stock-option/getOne.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { withCompanyAuth } from "@/server/api/auth"; -import { ApiError, ErrorResponses } from "@/server/api/error"; -import type { PublicAPI } from "@/server/api/hono"; -import { OptionSchema, type TOptionSchema } from "@/server/api/schema/option"; -import { db } from "@/server/db"; -import { createRoute, z } from "@hono/zod-openapi"; -import type { Context } from "hono"; - -const ParamsSchema = z.object({ - id: z - .string() - .cuid() - .openapi({ - description: "Company ID", - param: { - name: "id", - in: "path", - }, - - example: "clxwbok580000i7nge8nm1ry0", - }), - optionId: z - .string() - .cuid() - .openapi({ - description: "Option ID", - param: { - name: "optionId", - in: "path", - }, - - example: "clyd3i9sw000008ij619eabva", - }), -}); - -const ResponseSchema = z - .object({ - data: OptionSchema, - }) - .openapi({ - description: "Get a single stock option by ID", - }); - -const route = createRoute({ - method: "get", - path: "/v1/companies/{id}/options/{optionId}", - summary: "Get an issued option by ID", - description: "Get a single issued option record by ID", - tags: ["Options"], - request: { - params: ParamsSchema, - }, - responses: { - 200: { - content: { - "application/json": { - schema: ResponseSchema, - }, - }, - description: "Retrieve the option for the company", - }, - ...ErrorResponses, - }, -}); - -const getOne = (app: PublicAPI) => { - app.openapi(route, async (c: Context) => { - const { company } = await withCompanyAuth(c); - - const { optionId: id } = c.req.param(); - - const option = await db.option.findUnique({ - where: { - id, - companyId: company.id, - }, - }); - - if (!option) { - throw new ApiError({ - code: "NOT_FOUND", - message: "Required option not found", - }); - } - - return c.json( - { - data: option, - }, - 200, - ); - }); -}; - -export default getOne; diff --git a/src/server/api/routes/company/stock-option/index.ts b/src/server/api/routes/company/stock-option/index.ts deleted file mode 100644 index 75b3ea56b..000000000 --- a/src/server/api/routes/company/stock-option/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { PublicAPI } from "@/server/api/hono"; -import create from "./create"; -import deleteOne from "./delete"; -import getMany from "./getMany"; -import getOne from "./getOne"; -import update from "./update"; - -export const registerOptionRoutes = (api: PublicAPI) => { - create(api); - getOne(api); - getMany(api); - update(api); - deleteOne(api); -}; diff --git a/src/server/api/routes/company/stock-option/update.ts b/src/server/api/routes/company/stock-option/update.ts deleted file mode 100644 index 3b5b3a126..000000000 --- a/src/server/api/routes/company/stock-option/update.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { withCompanyAuth } from "@/server/api/auth"; -import { - ApiError, - type ErrorCodeType, - ErrorResponses, -} from "@/server/api/error"; -import type { PublicAPI } from "@/server/api/hono"; - -import { - OptionSchema, - type TUpdateOptionSchema, - UpdateOptionSchema, -} from "@/server/api/schema/option"; -import { getHonoUserAgent, getIp } from "@/server/api/utils"; -import { updateOption } from "@/server/services/stock-option/update-option"; -import { createRoute, z } from "@hono/zod-openapi"; -import type { Context } from "hono"; - -export const RequestParamsSchema = z - .object({ - id: z - .string() - .cuid() - .openapi({ - description: "Company ID", - param: { - name: "id", - in: "path", - }, - - example: "clxwbok580000i7nge8nm1ry0", - }), - optionId: z - .string() - .cuid() - .openapi({ - description: "Option ID", - param: { - name: "optionId", - in: "path", - }, - - example: "clyd3i9sw000008ij619eabva", - }), - }) - .openapi({ - description: "Update an option by ID", - }); - -const ResponseSchema = z - .object({ - message: z.string(), - data: OptionSchema, - }) - .openapi({ - description: "Update an option by ID", - }); - -const route = createRoute({ - method: "put", - path: "/v1/companies/{id}/options/{optionId}", - summary: "Update an issued option by ID", - description: "Update issued option by option ID", - tags: ["Options"], - request: { - params: RequestParamsSchema, - body: { - content: { - "application/json": { - schema: UpdateOptionSchema, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: ResponseSchema, - }, - }, - description: "Update the option by ID", - }, - ...ErrorResponses, - }, -}); - -const update = (app: PublicAPI) => { - app.openapi(route, async (c: Context) => { - const { company, user } = await withCompanyAuth(c); - const { optionId } = c.req.param(); - const body = await c.req.json(); - - const payload = { - optionId: optionId as string, - companyId: company.id, - requestIp: getIp(c.req), - userAgent: getHonoUserAgent(c.req), - data: body as TUpdateOptionSchema, - user: { - id: user.id, - name: user.name as string, - }, - }; - - const { success, message, code, data } = await updateOption(payload); - - if (!success || !data) { - throw new ApiError({ - code: code as ErrorCodeType, - message, - }); - } - - return c.json( - { - message, - data, - }, - 200, - ); - }); -}; - -export default update; diff --git a/src/server/services/stock-option/add-option.ts b/src/server/services/stock-option/add-option.ts deleted file mode 100644 index b2ba25b61..000000000 --- a/src/server/services/stock-option/add-option.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { generatePublicId } from "@/common/id"; -import type { TCreateOptionSchema } from "@/server/api/schema/option"; -import { Audit } from "@/server/audit"; -import { db } from "@/server/db"; -import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; -import type { TUpdateOption } from "./update-option"; - -type AuditPromise = ReturnType; - -type TAddOption = Omit & { - data: TCreateOptionSchema; - memberId: string; -}; - -export const addOption = async (payload: TAddOption) => { - try { - const { data, user, memberId } = payload; - const documents = data.documents; - - const newOption = await db.$transaction(async (tx) => { - const _data = { - grantId: data.grantId, - quantity: data.quantity, - exercisePrice: data.exercisePrice, - type: data.type, - status: data.status, - cliffYears: data.cliffYears, - vestingYears: data.vestingYears, - issueDate: new Date(data.issueDate), - expirationDate: new Date(data.expirationDate), - vestingStartDate: new Date(data.vestingStartDate), - boardApprovalDate: new Date(data.boardApprovalDate), - rule144Date: new Date(data.rule144Date), - - stakeholderId: data.stakeholderId, - equityPlanId: data.equityPlanId, - companyId: payload.companyId, - }; - - const option = await tx.option.create({ data: _data }); - - let auditPromises: AuditPromise[] = []; - - if (documents && documents.length > 0) { - const bulkDocuments = documents.map((doc) => ({ - companyId: payload.companyId, - uploaderId: memberId, - publicId: generatePublicId(), - name: doc.name, - bucketId: doc.bucketId, - optionId: option.id, - })); - - const docs = await tx.document.createManyAndReturn({ - data: bulkDocuments, - skipDuplicates: true, - }); - - auditPromises = docs.map((doc) => - Audit.create( - { - action: "document.created", - companyId: payload.companyId, - actor: { type: "user", id: user.id }, - context: { - userAgent: payload.userAgent, - requestIp: payload.requestIp, - }, - target: [{ type: "document", id: doc.id }], - summary: `${user.name} created a document while issuing a stock option : ${doc.name}`, - }, - tx, - ), - ); - } - - auditPromises.push( - Audit.create( - { - action: "option.created", - companyId: payload.companyId, - actor: { type: "user", id: user.id }, - context: { - userAgent: payload.userAgent, - requestIp: payload.requestIp, - }, - target: [{ type: "option", id: option.id }], - summary: `${user.name} issued an option for stakeholder : ${option.stakeholderId}`, - }, - tx, - ), - ); - - await Promise.all(auditPromises); - - return option; - }); - - return { - success: true, - message: "🎉 Successfully issued an option.", - data: newOption, - }; - } catch (error) { - console.error(error); - if (error instanceof PrismaClientKnownRequestError) { - // Unique constraints error code in prisma - // Only grantId column can throw this error code - if (error.code === "P2002") { - return { - success: false, - code: "BAD_REQUEST", - message: "Please use unique grant Id", - }; - } - } - return { - success: false, - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error - ? error.message - : "Something went wrong. Please try again later or contact support.", - }; - } -}; diff --git a/src/server/services/stock-option/delete-option.ts b/src/server/services/stock-option/delete-option.ts deleted file mode 100644 index 53142a27b..000000000 --- a/src/server/services/stock-option/delete-option.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Audit } from "@/server/audit"; -import { db } from "@/server/db"; -import type { TUpdateOption } from "./update-option"; - -type TDeleteOption = Omit; - -export const deleteOption = async ({ - optionId, - companyId, - requestIp, - userAgent, - user, -}: TDeleteOption) => { - try { - const existingOption = await db.option.findUnique({ - where: { - id: optionId, - }, - }); - - if (!existingOption) { - return { - success: false, - code: "NOT_FOUND", - message: `Option with ID ${optionId} not found`, - }; - } - - const option = await db.$transaction(async (tx) => { - const deleted = await tx.option.delete({ - where: { - id: optionId, - }, - }); - - const { stakeholderId } = deleted; - - await Audit.create( - { - action: "option.deleted", - companyId, - actor: { type: "user", id: user.id }, - context: { - userAgent, - requestIp, - }, - target: [{ type: "option", id: deleted.id }], - summary: `${user.name} deleted the option for stakeholder ${stakeholderId}`, - }, - tx, - ); - - return deleted; - }); - return { - success: true, - message: "🎉 Successfully deleted the option", - option, - }; - } catch (error) { - console.error("Error Deleting the option: ", error); - return { - success: false, - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error - ? error.message - : "Error deleting the option, please try again or contact support.", - }; - } -}; diff --git a/src/server/services/stock-option/get-option.ts b/src/server/services/stock-option/get-option.ts deleted file mode 100644 index 21545c56d..000000000 --- a/src/server/services/stock-option/get-option.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ProxyPrismaModel } from "@/server/api/pagination/prisma-proxy"; -import { db } from "@/server/db"; - -type GetPaginatedOptions = { - companyId: string; - take: number; - cursor?: string; - total?: number; -}; - -export const getPaginatedOptions = async (payload: GetPaginatedOptions) => { - const queryCriteria = { - where: { - companyId: payload.companyId, - }, - orderBy: { - createdAt: "desc", - }, - }; - - const paginationData = { - take: payload.take, - cursor: payload.cursor, - total: payload.total, - }; - - const prismaModel = ProxyPrismaModel(db.option); - - const { data, count, total, cursor } = await prismaModel.findManyPaginated( - queryCriteria, - paginationData, - ); - - return { - data, - meta: { - count, - total, - cursor, - }, - }; -}; diff --git a/src/server/services/stock-option/update-option.ts b/src/server/services/stock-option/update-option.ts deleted file mode 100644 index 8596e407c..000000000 --- a/src/server/services/stock-option/update-option.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { TUpdateOptionSchema } from "@/server/api/schema/option"; -import { Audit } from "@/server/audit"; -import { db } from "@/server/db"; -import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; - -export type TUpdateOption = { - optionId: string; - companyId: string; - requestIp: string; - userAgent: string; - user: { - id: string; - name: string; - }; - data: TUpdateOptionSchema; -}; - -export const updateOption = async (payload: TUpdateOption) => { - try { - const { optionId, companyId, user } = payload; - - const existingOption = await db.option.findUnique({ - where: { id: optionId }, - }); - - if (!existingOption) { - return { - success: false, - message: `Option with ID ${optionId} not be found`, - }; - } - - const optionData = { - ...existingOption, - ...payload.data, - }; - - const updated = await db.$transaction(async (tx) => { - const updated = await tx.option.update({ - where: { id: optionId }, - //@ts-ignore - data: optionData, - }); - - const { id: _optionId } = updated; - - await Audit.create( - { - action: "option.updated", - companyId: companyId, - actor: { type: "user", id: user.id }, - context: { - userAgent: payload.userAgent, - requestIp: payload.requestIp, - }, - target: [{ type: "option", id: _optionId }], - summary: `${user.name} updated option with option ID : ${_optionId}`, - }, - tx, - ); - - return updated; - }); - return { - success: true, - message: "🎉 Successfully updated the option.", - data: updated, - }; - } catch (error) { - console.error("updateOption", error); - if (error instanceof PrismaClientKnownRequestError) { - if (error.code === "P2002") { - return { - success: false, - code: "BAD_REQUEST", - message: "Please use unique grant Id", - }; - } - } - return { - success: false, - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error - ? error.message - : "Something went wrong. Please try again or contact support.", - }; - } -}; From 73d95478232ef007bda5098fe8a61076bd75031e Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:51:21 +0545 Subject: [PATCH 21/30] feat: create option route --- src/server/api/routes/stock-option/create.ts | 146 +++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/server/api/routes/stock-option/create.ts diff --git a/src/server/api/routes/stock-option/create.ts b/src/server/api/routes/stock-option/create.ts new file mode 100644 index 000000000..f5c4706c5 --- /dev/null +++ b/src/server/api/routes/stock-option/create.ts @@ -0,0 +1,146 @@ +import { generatePublicId } from "@/common/id"; +import { + CreateOptionSchema, + type TOptionSchema, +} from "@/server/api/schema/option"; +import { + authMiddleware, + withAuthApiV1, +} from "@/server/api/utils/endpoint-creator"; +import { Audit } from "@/server/audit"; +import { z } from "@hono/zod-openapi"; + +type AuditPromise = ReturnType; + +const ResponseSchema = z.object({ + message: z.string(), + data: CreateOptionSchema, +}); + +const ParamsSchema = z.object({ + companyId: z.string().openapi({ + param: { + name: "companyId", + in: "path", + }, + description: "Company ID", + type: "string", + example: "clxwbok580000i7nge8nm1ry0", + }), +}); + +export const create = withAuthApiV1 + .createRoute({ + method: "post", + path: "/v1/{companyId}/options", + summary: "Create options", + description: "Issue options to a stakeholder in a company.", + tags: ["Options"], + middleware: [authMiddleware()], + request: { + params: ParamsSchema, + body: { + content: { + "application/json": { + schema: CreateOptionSchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Confirmation of options issued with relevant details.", + }, + }, + }) + .handler(async (c) => { + const { db, audit, client } = c.get("services"); + const { membership } = c.get("session"); + const { requestIp, userAgent } = client; + + const { documents, ...rest } = c.req.valid("json"); + + const option = await db.$transaction(async (tx) => { + const option = await tx.option.create({ + data: { ...rest, companyId: membership.companyId }, + }); + + let auditPromises: AuditPromise[] = []; + + if (documents && documents.length > 0) { + const bulkDocuments = documents.map((doc) => ({ + companyId: membership.companyId, + uploaderId: membership.memberId, + publicId: generatePublicId(), + name: doc.name, + bucketId: doc.bucketId, + optionId: option.id, + })); + + const docs = await tx.document.createManyAndReturn({ + data: bulkDocuments, + skipDuplicates: true, + select: { + id: true, + name: true, + }, + }); + + auditPromises = docs.map((doc) => + Audit.create( + { + action: "document.created", + companyId: membership.companyId, + actor: { type: "user", id: membership.userId }, + context: { + userAgent, + requestIp, + }, + target: [{ type: "document", id: doc.id }], + summary: `${membership.user.name} created a document while issuing a stock option : ${doc.name}`, + }, + tx, + ), + ); + } + + auditPromises.push( + audit.create( + { + action: "option.created", + companyId: membership.companyId, + actor: { type: "user", id: membership.userId }, + context: { + userAgent, + requestIp, + }, + target: [{ type: "option", id: option.id }], + summary: `${membership.user.name} added option for stakeholder ${option.stakeholderId}`, + }, + tx, + ), + ); + + await Promise.all(auditPromises); + + return option; + }); + + const data: TOptionSchema = { + ...option, + createdAt: option.createdAt.toISOString(), + updatedAt: option.updatedAt.toISOString(), + issueDate: option.issueDate.toISOString(), + expirationDate: option.expirationDate.toISOString(), + rule144Date: option.rule144Date.toISOString(), + vestingStartDate: option.vestingStartDate.toISOString(), + boardApprovalDate: option.boardApprovalDate.toISOString(), + }; + + return c.json({ message: "Option successfully created.", data }, 200); + }); From 3177210123d05e8525795511129972ee80b6693b Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:51:50 +0545 Subject: [PATCH 22/30] feat: delete option route --- src/server/api/routes/stock-option/delete.ts | 111 +++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/server/api/routes/stock-option/delete.ts diff --git a/src/server/api/routes/stock-option/delete.ts b/src/server/api/routes/stock-option/delete.ts new file mode 100644 index 000000000..1ce9b88fd --- /dev/null +++ b/src/server/api/routes/stock-option/delete.ts @@ -0,0 +1,111 @@ +import { z } from "@hono/zod-openapi"; +import { ApiError } from "../../error"; + +import { authMiddleware, withAuthApiV1 } from "../../utils/endpoint-creator"; + +const ParamsSchema = z.object({ + id: z.string().openapi({ + param: { + name: "id", + in: "path", + }, + description: "Option ID", + type: "string", + example: "clyabgufg004u5tbtnz0r4cax", + }), + companyId: z.string().openapi({ + param: { + name: "companyId", + in: "path", + }, + description: "Company ID", + type: "string", + example: "clxwbok580000i7nge8nm1ry0", + }), +}); + +const ResponseSchema = z.object({ + message: z.string().openapi({ + description: + "A text providing details about the API request result, including success, error, or warning messages.", + }), +}); + +export const deleteOne = withAuthApiV1 + .createRoute({ + method: "delete", + path: "/v1/{companyId}/options/{id}", + summary: "Delete a option", + description: "Remove an issued option by its ID.", + tags: ["Options"], + middleware: [authMiddleware()], + request: { params: ParamsSchema }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Confirmation that the issued option has been removed.", + }, + }, + }) + .handler(async (c) => { + const { db, audit, client } = c.get("services"); + const { membership } = c.get("session"); + const { requestIp, userAgent } = client as { + requestIp: string; + userAgent: string; + }; + + const { id } = c.req.valid("param"); + + await db.$transaction(async (tx) => { + const option = await tx.option.findUnique({ + where: { + id, + companyId: membership.companyId, + }, + select: { + id: true, + stakeholderId: true, + }, + }); + + if (!option) { + throw new ApiError({ + code: "NOT_FOUND", + message: `Option with ID ${id} not found`, + }); + } + + await tx.option.delete({ + where: { + id: option.id, + }, + }); + + await audit.create( + { + action: "option.deleted", + companyId: membership.companyId, + actor: { type: "user", id: membership.userId }, + context: { + userAgent, + requestIp, + }, + target: [{ type: "option", id }], + summary: `${membership.user.name} deleted the option for stakeholder ${option.stakeholderId}`, + }, + tx, + ); + }); + + return c.json( + { + message: "Option deleted successfully", + }, + 200, + ); + }); From d3ba629534c58262280fc8a968d3d8fa9e7b4a80 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:52:07 +0545 Subject: [PATCH 23/30] feat: update option route --- src/server/api/routes/stock-option/update.ts | 136 +++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/server/api/routes/stock-option/update.ts diff --git a/src/server/api/routes/stock-option/update.ts b/src/server/api/routes/stock-option/update.ts new file mode 100644 index 000000000..52ab937fc --- /dev/null +++ b/src/server/api/routes/stock-option/update.ts @@ -0,0 +1,136 @@ +import { z } from "@hono/zod-openapi"; +import { ApiError } from "../../error"; + +import { + OptionSchema, + type TOptionSchema, + UpdateOptionSchema, +} from "@/server/api/schema/option"; + +import { + authMiddleware, + withAuthApiV1, +} from "@/server/api/utils/endpoint-creator"; + +const ParamsSchema = z.object({ + id: z.string().openapi({ + param: { + name: "id", + in: "path", + }, + description: "Option ID", + type: "string", + example: "clyabgufg004u5tbtnz0r4cax", + }), + companyId: z.string().openapi({ + param: { + name: "companyId", + in: "path", + }, + description: "Company ID", + type: "string", + example: "clxwbok580000i7nge8nm1ry0", + }), +}); + +const ResponseSchema = z.object({ + message: z.string(), + data: OptionSchema, +}); + +export const update = withAuthApiV1 + .createRoute({ + summary: "Update Issued Options", + description: "Update details of an issued option by its ID.", + tags: ["Shares"], + method: "patch", + path: "/v1/{companyId}/options/{id}", + middleware: [authMiddleware()], + request: { + params: ParamsSchema, + body: { + content: { + "application/json": { + schema: UpdateOptionSchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Confirmation of updated issued option details.", + }, + }, + }) + .handler(async (c) => { + const { db, audit, client } = c.get("services"); + const { membership } = c.get("session"); + const { requestIp, userAgent } = client; + const { id } = c.req.valid("param"); + + const body = c.req.valid("json"); + + const updatedOption = await db.$transaction(async (tx) => { + const option = await db.option.findUnique({ + where: { + id, + companyId: membership.companyId, + }, + }); + + if (!option) { + throw new ApiError({ + code: "NOT_FOUND", + message: `No option with the provided Id ${id}`, + }); + } + + const updatedOption = await tx.option.update({ + where: { + id: option.id, + }, + data: body, + }); + + await audit.create( + { + action: "option.updated", + companyId: membership.companyId, + actor: { type: "user", id: membership.userId }, + context: { + userAgent: userAgent, + requestIp: requestIp, + }, + target: [{ type: "option", id: option.id }], + summary: `${membership.user.name} updated option the option ID ${updatedOption.id}`, + }, + tx, + ); + + return updatedOption; + }); + + const data: TOptionSchema = { + ...updatedOption, + createdAt: updatedOption.createdAt.toISOString(), + updatedAt: updatedOption.updatedAt.toISOString(), + issueDate: updatedOption.issueDate.toISOString(), + expirationDate: updatedOption.expirationDate.toISOString(), + rule144Date: updatedOption.rule144Date.toISOString(), + vestingStartDate: updatedOption.vestingStartDate.toISOString(), + boardApprovalDate: updatedOption.boardApprovalDate.toISOString(), + }; + + return c.json( + { + message: "Option updated successfully", + data, + }, + 200, + ); + }); From 930077c6fb3978ed26a0eb1d6ba4435d7e30c641 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:52:38 +0545 Subject: [PATCH 24/30] feat: getOne option route --- src/server/api/routes/stock-option/getOne.ts | 93 ++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/server/api/routes/stock-option/getOne.ts diff --git a/src/server/api/routes/stock-option/getOne.ts b/src/server/api/routes/stock-option/getOne.ts new file mode 100644 index 000000000..6b2b10428 --- /dev/null +++ b/src/server/api/routes/stock-option/getOne.ts @@ -0,0 +1,93 @@ +import { OptionSchema, type TOptionSchema } from "@/server/api/schema/option"; +import { z } from "@hono/zod-openapi"; +import { ApiError } from "../../error"; + +import { + authMiddleware, + withAuthApiV1, +} from "@/server/api/utils/endpoint-creator"; + +const ParamsSchema = z.object({ + id: z.string().openapi({ + param: { + name: "id", + in: "path", + }, + description: "Option ID", + type: "string", + example: "clyabgufg004u5tbtnz0r4cax", + }), + companyId: z.string().openapi({ + param: { + name: "companyId", + in: "path", + }, + description: "Company ID", + type: "string", + example: "clxwbok580000i7nge8nm1ry0", + }), +}); + +const ResponseSchema = z.object({ + data: OptionSchema, +}); + +export const getOne = withAuthApiV1 + .createRoute({ + summary: "Get an option", + description: "Fetch a single issued option record by its ID.", + tags: ["Options"], + method: "get", + path: "/v1/{companyId}/options/{id}", + middleware: [authMiddleware()], + request: { + params: ParamsSchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "Details of the requested issued option.", + }, + }, + }) + .handler(async (c) => { + const { db } = c.get("services"); + const { membership } = c.get("session"); + const { id } = c.req.valid("param"); + + const option = await db.option.findUnique({ + where: { + id, + companyId: membership.companyId, + }, + }); + + if (!option) { + throw new ApiError({ + code: "NOT_FOUND", + message: `No option with the provided Id ${id}`, + }); + } + + const data: TOptionSchema = { + ...option, + createdAt: option.createdAt.toISOString(), + updatedAt: option.updatedAt.toISOString(), + issueDate: option.issueDate.toISOString(), + expirationDate: option.expirationDate.toISOString(), + rule144Date: option.rule144Date.toISOString(), + vestingStartDate: option.vestingStartDate.toISOString(), + boardApprovalDate: option.boardApprovalDate.toISOString(), + }; + + return c.json( + { + data, + }, + 200, + ); + }); From 4213ffa621f64fd1de5a3d9187d9b47c68176a2c Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:52:57 +0545 Subject: [PATCH 25/30] feat: getMany option route --- src/server/api/routes/stock-option/getMany.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/server/api/routes/stock-option/getMany.ts diff --git a/src/server/api/routes/stock-option/getMany.ts b/src/server/api/routes/stock-option/getMany.ts new file mode 100644 index 000000000..03a8cf6fc --- /dev/null +++ b/src/server/api/routes/stock-option/getMany.ts @@ -0,0 +1,77 @@ +import { OptionSchema } from "@/server/api/schema/option"; +import { z } from "@hono/zod-openapi"; +import { + PaginationQuerySchema, + PaginationResponseSchema, +} from "../../schema/pagination"; + +import { authMiddleware, withAuthApiV1 } from "../../utils/endpoint-creator"; + +const ResponseSchema = z.object({ + data: z.array(OptionSchema), + meta: PaginationResponseSchema, +}); + +const ParamsSchema = z.object({ + companyId: z.string().openapi({ + param: { + name: "companyId", + in: "path", + }, + description: "Company ID", + type: "string", + example: "clxwbok580000i7nge8nm1ry0", + }), +}); + +export const getMany = withAuthApiV1 + .createRoute({ + summary: "List options", + description: "Retrieve a list of issued options for the company.", + tags: ["Options"], + method: "get", + path: "/v1/{companyId}/options", + middleware: [authMiddleware()], + request: { + params: ParamsSchema, + query: PaginationQuerySchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ResponseSchema, + }, + }, + description: "A list of issued options with their details.", + }, + }, + }) + .handler(async (c) => { + const { membership } = c.get("session"); + const { db } = c.get("services"); + const query = c.req.valid("query"); + + const [data, meta] = await db.option + .paginate({ where: { companyId: membership.companyId } }) + .withCursor({ + limit: query.limit, + after: query.cursor, + }); + + const response: z.infer = { + meta, + data: data.map((i) => ({ + ...i, + createdAt: i.createdAt.toISOString(), + updatedAt: i.updatedAt.toISOString(), + issueDate: i.issueDate.toISOString(), + expirationDate: i.expirationDate.toISOString(), + rule144Date: i.rule144Date.toISOString(), + vestingStartDate: i.vestingStartDate.toISOString(), + boardApprovalDate: i.boardApprovalDate.toISOString(), + })), + }; + + return c.json(response, 200); + }); From df67be58be119eab32749a62fb9102b3bf195cfa Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:53:34 +0545 Subject: [PATCH 26/30] feat: register option routes --- src/server/api/routes/stock-option/index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/server/api/routes/stock-option/index.ts diff --git a/src/server/api/routes/stock-option/index.ts b/src/server/api/routes/stock-option/index.ts new file mode 100644 index 000000000..0f7f16260 --- /dev/null +++ b/src/server/api/routes/stock-option/index.ts @@ -0,0 +1,14 @@ +import type { PublicAPI } from "@/server/api/hono"; +import { create } from "./create"; +import { deleteOne } from "./delete"; +import { getMany } from "./getMany"; +import { getOne } from "./getOne"; +import { update } from "./update"; + +export const registerOptionRoutes = (api: PublicAPI) => { + api.openapi(create.route, create.handler); + api.openapi(getOne.route, getOne.handler); + api.openapi(getMany.route, getMany.handler); + api.openapi(update.route, update.handler); + api.openapi(deleteOne.route, deleteOne.handler); +}; From ac9b1ca409ef4a81b8a6f0711658da711be484c7 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:54:44 +0545 Subject: [PATCH 27/30] chore: change date to dateTime in schema(zod) --- src/server/api/schema/option.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/server/api/schema/option.ts b/src/server/api/schema/option.ts index 5a3e091ae..9d874fde0 100644 --- a/src/server/api/schema/option.ts +++ b/src/server/api/schema/option.ts @@ -52,29 +52,29 @@ export const OptionSchema = z example: 4, }), - issueDate: z.string().date().openapi({ + issueDate: z.string().datetime().openapi({ description: "Issue Date", example: "2024-01-01", }), - expirationDate: z.string().date().openapi({ + expirationDate: z.string().datetime().openapi({ description: "Expiration Date", - example: "2028-01-01", + example: "2024-01-01T00:00:00.000Z", }), - vestingStartDate: z.string().date().openapi({ + vestingStartDate: z.string().datetime().openapi({ description: "Vesting Start Date", - example: "2024-01-01", + example: "2024-01-01T00:00:00.000Z", }), - boardApprovalDate: z.string().date().openapi({ + boardApprovalDate: z.string().datetime().openapi({ description: "Board Approval Date", - example: "2024-01-01", + example: "2024-01-01T00:00:00.000Z", }), - rule144Date: z.string().date().openapi({ + rule144Date: z.string().datetime().openapi({ description: "Rule 144 Date", - example: "2024-01-01", + example: "2024-01-01T00:00:00.000Z", }), stakeholderId: z.string().openapi({ From cd34e3aa52efef69d1ffa6bfbd567e9b94b1b839 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Sun, 11 Aug 2024 00:55:23 +0545 Subject: [PATCH 28/30] chore: cleanups --- src/server/api/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 65171e0fe..a8448b335 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -3,8 +3,7 @@ import { middlewareServices } from "./middlewares/services"; import { registerCompanyRoutes } from "./routes/company"; import { registerShareRoutes } from "./routes/share"; import { registerStakeholderRoutes } from "./routes/stakeholder"; - - +import { registerOptionRoutes } from "./routes/stock-option"; const api = PublicAPI(); @@ -14,6 +13,6 @@ api.use("*", middlewareServices()); registerCompanyRoutes(api); registerShareRoutes(api); registerStakeholderRoutes(api); -// registerOptionRoutes(api); +registerOptionRoutes(api); export default api; From c00cb05ac8d5a3c6ae63f2d86fbba64f8527c7d5 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 12 Aug 2024 02:16:34 +0545 Subject: [PATCH 29/30] feat: add cursor parser to fix serialization/parsing error --- src/server/api/routes/stock-option/getMany.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/server/api/routes/stock-option/getMany.ts b/src/server/api/routes/stock-option/getMany.ts index 03a8cf6fc..b43fa89fa 100644 --- a/src/server/api/routes/stock-option/getMany.ts +++ b/src/server/api/routes/stock-option/getMany.ts @@ -52,11 +52,19 @@ export const getMany = withAuthApiV1 const { db } = c.get("services"); const query = c.req.valid("query"); + // To get rid of parseCursor in each getMany route + //@TODO(Better to have parsing at server/db in root ) const [data, meta] = await db.option .paginate({ where: { companyId: membership.companyId } }) .withCursor({ limit: query.limit, after: query.cursor, + getCursor({ id }) { + return id; + }, + parseCursor(cursor) { + return { id: cursor }; + }, }); const response: z.infer = { @@ -72,6 +80,5 @@ export const getMany = withAuthApiV1 boardApprovalDate: i.boardApprovalDate.toISOString(), })), }; - return c.json(response, 200); }); From 3a28a37b662fa38a04bfa26b13bd0d6a913a3683 Mon Sep 17 00:00:00 2001 From: Raju kadel Date: Mon, 12 Aug 2024 02:17:34 +0545 Subject: [PATCH 30/30] chore: cleanups --- src/server/api/routes/stock-option/update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/routes/stock-option/update.ts b/src/server/api/routes/stock-option/update.ts index 52ab937fc..2b354f8a2 100644 --- a/src/server/api/routes/stock-option/update.ts +++ b/src/server/api/routes/stock-option/update.ts @@ -42,7 +42,7 @@ export const update = withAuthApiV1 .createRoute({ summary: "Update Issued Options", description: "Update details of an issued option by its ID.", - tags: ["Shares"], + tags: ["Options"], method: "patch", path: "/v1/{companyId}/options/{id}", middleware: [authMiddleware()],