Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: openapi for stock option endpoint #465

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
55e96e4
feat: getting started with stock-option index file
Raju-kadel-27 Jul 25, 2024
f0bbeb4
feat: add stock option service
Raju-kadel-27 Jul 28, 2024
10b1daa
feat: add update option service
Raju-kadel-27 Jul 28, 2024
59c63d6
feat: add delete option by Id service
Raju-kadel-27 Jul 28, 2024
94412a8
feat: add get options service
Raju-kadel-27 Jul 28, 2024
463ffd4
feat: add create option route
Raju-kadel-27 Jul 28, 2024
b5ecd59
feat: add delete option route
Raju-kadel-27 Jul 28, 2024
b959564
feat: add getMany option route
Raju-kadel-27 Jul 28, 2024
d81a193
feat: add getOne option route
Raju-kadel-27 Jul 28, 2024
83004a5
feat: add update option route
Raju-kadel-27 Jul 28, 2024
ad4791e
feat: zod schemas for option endpoint
Raju-kadel-27 Jul 28, 2024
626071c
feat: export option routes
Raju-kadel-27 Jul 28, 2024
4e4e928
fix: conflict in index
Raju-kadel-27 Jul 28, 2024
5f8cea4
feat: audit events for docs added
Raju-kadel-27 Jul 28, 2024
bb137b1
feat: schema update
Raju-kadel-27 Jul 29, 2024
35c2f58
fix: type and date conversion typo
Raju-kadel-27 Jul 29, 2024
32481dd
fix: type and date conversion typo in add-option service
Raju-kadel-27 Jul 29, 2024
e373359
fix: type in delete-option service
Raju-kadel-27 Jul 29, 2024
44dbf59
chore: unknown type for hono response
Raju-kadel-27 Jul 29, 2024
bb8dd59
chore: refactoring for adapting new auth middleware
Raju-kadel-27 Aug 10, 2024
73d9547
feat: create option route
Raju-kadel-27 Aug 10, 2024
3177210
feat: delete option route
Raju-kadel-27 Aug 10, 2024
d3ba629
feat: update option route
Raju-kadel-27 Aug 10, 2024
930077c
feat: getOne option route
Raju-kadel-27 Aug 10, 2024
4213ffa
feat: getMany option route
Raju-kadel-27 Aug 10, 2024
df67be5
feat: register option routes
Raju-kadel-27 Aug 10, 2024
ac9b1ca
chore: change date to dateTime in schema(zod)
Raju-kadel-27 Aug 10, 2024
cd34e3a
chore: cleanups
Raju-kadel-27 Aug 10, 2024
c00cb05
feat: add cursor parser to fix serialization/parsing error
Raju-kadel-27 Aug 11, 2024
3a28a37
chore: cleanups
Raju-kadel-27 Aug 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/server/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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();

Expand All @@ -12,5 +13,6 @@ api.use("*", middlewareServices());
registerCompanyRoutes(api);
registerShareRoutes(api);
registerStakeholderRoutes(api);
registerOptionRoutes(api);

export default api;
146 changes: 146 additions & 0 deletions src/server/api/routes/stock-option/create.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Audit.create>;

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);
});
111 changes: 111 additions & 0 deletions src/server/api/routes/stock-option/delete.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
84 changes: 84 additions & 0 deletions src/server/api/routes/stock-option/getMany.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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");

// 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<typeof ResponseSchema> = {
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);
});
Loading