Merge pull request #458 from Alfredoeb9/separate-vesting-schedule

fix: separated vestingSchedule
This commit is contained in:
Puru Dahal
2024-07-24 23:02:18 -05:00
committed by GitHub
13 changed files with 267 additions and 102 deletions
@@ -0,0 +1,15 @@
/*
Warnings:
- You are about to drop the column `vestingSchedule` on the `Option` table. All the data in the column will be lost.
- You are about to drop the column `vestingSchedule` on the `Share` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Option" DROP COLUMN "vestingSchedule";
-- AlterTable
ALTER TABLE "Share" DROP COLUMN "vestingSchedule";
-- DropEnum
DROP TYPE "VestingScheduleEnum";
+6 -18
View File
@@ -669,16 +669,6 @@ enum SecuritiesStatusEnum {
PENDING
}
enum VestingScheduleEnum {
VESTING_0_0_0 // Immediate vesting
VESTING_0_0_1 // 1 year cliff with no vesting
VESTING_4_1_0 // 4 years vesting every month with no cliff
VESTING_4_1_1 // 4 years vesting every month with 1 year cliff
VESTING_4_3_1 // 4 years vesting every 3 months with 1 year cliff
VESTING_4_6_1 // 4 years vesting every 6 months with 1 year cliff
VESTING_4_12_1 // 4 years vesting every year with 1 year cliff
}
enum ShareLegendsEnum {
US_SECURITIES_ACT // US Securities Act of 1933
SALE_AND_ROFR // Sale and Right of first refusal
@@ -697,9 +687,8 @@ model Share {
debtCancelled Float? // Amount of debt cancelled
otherContributions Float? // Other contributions
cliffYears Int @default(0) // 0 means immediate vesting, 1 means vesting starts after 1 year
vestingYears Int @default(0) // 0 means immediate vesting, 1 means vesting over 1 year
vestingSchedule VestingScheduleEnum
cliffYears Int @default(0) // 0 means immediate vesting, 1 means vesting starts after 1 year
vestingYears Int @default(0) // 0 means immediate vesting, 1 means vesting over 1 year
companyLegends ShareLegendsEnum[] @default([])
@@ -748,11 +737,10 @@ model Option {
quantity Int
exercisePrice Float
type OptionTypeEnum
status OptionStatusEnum @default(DRAFT)
cliffYears Int @default(0) // 0 means immediate vesting, 1 means vesting starts after 1 year
vestingYears Int @default(0) // 0 means immediate vesting, 1 means vesting over 1 year
vestingSchedule VestingScheduleEnum
type OptionTypeEnum
status OptionStatusEnum @default(DRAFT)
cliffYears Int @default(0) // 0 means immediate vesting, 1 means vesting starts after 1 year
vestingYears Int @default(0) // 0 means immediate vesting, 1 means vesting over 1 year
issueDate DateTime
expirationDate DateTime
@@ -16,8 +16,12 @@ import {
StepperPrev,
useStepper,
} from "@/components/ui/stepper";
import { VestingSchedule } from "@/lib/vesting";
import { VestingScheduleEnum } from "@/prisma/enums";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useStockOptionFormValues } from "@/providers/stock-option-form-provider";
import type { RouterOutputs } from "@/trpc/shared";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -28,7 +32,8 @@ import { EmptySelect } from "../../shared/EmptySelect";
const formSchema = z.object({
equityPlanId: z.string(),
vestingSchedule: z.nativeEnum(VestingScheduleEnum),
cliffYears: z.coerce.number().min(0),
vestingYears: z.coerce.number().min(0),
exercisePrice: z.coerce.number(),
stakeholderId: z.string(),
});
@@ -54,10 +59,10 @@ export const VestingDetails = (props: VestingDetailsProps) => {
const disabled = !stakeholders?.length && !equityPlans?.length;
const vestingSchedileOpts = Object.keys(VestingSchedule).map((vKey) => ({
value: vKey,
label: VestingSchedule[vKey] || "",
}));
// const vestingSchedileOpts = Object.keys(VestingSchedule).map((vKey) => ({
// value: vKey,
// label: VestingSchedule[vKey] || "",
// }));
const equityPlansOpts = equityPlans?.map(({ id, name }) => ({
value: id,
@@ -76,22 +81,90 @@ export const VestingDetails = (props: VestingDetailsProps) => {
className="flex flex-col gap-y-4"
>
<div className="grid gap-4">
<FormField
control={form.control}
name="vestingSchedule"
render={({ field }) => (
<FormItem>
<FormLabel>Vesting schedule</FormLabel>
<div>
<LinearCombobox
options={vestingSchedileOpts}
onValueChange={(option) => field.onChange(option.value)}
/>
</div>
<FormMessage className="text-xs font-light" />
</FormItem>
)}
/>
<div className="flex items-center gap-4">
<div className="flex-1">
<FormField
control={form.control}
name="vestingYears"
render={({ field }) => {
const { onChange, ...rest } = field;
return (
<FormItem>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<FormLabel>Vesting</FormLabel>
</TooltipTrigger>
<TooltipContent>
<p>Vesting starts over</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<FormControl>
<NumericFormat
thousandSeparator
allowedDecimalSeparators={["%"]}
suffix={field.value > 1 ? " years" : " year"}
decimalScale={0}
{...rest}
customInput={Input}
onValueChange={(values) => {
const { floatValue } = values;
onChange(floatValue);
}}
/>
</FormControl>
</div>
<FormMessage className="text-xs font-light" />
</FormItem>
);
}}
/>
</div>
<div className="flex-1">
<FormField
control={form.control}
name="cliffYears"
render={({ field }) => {
const { onChange, ...rest } = field;
return (
<FormItem>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<FormLabel>Cliff</FormLabel>
</TooltipTrigger>
<TooltipContent>
<p>Vesting starts after</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<FormControl>
<NumericFormat
thousandSeparator
allowedDecimalSeparators={["%"]}
decimalScale={0}
suffix={field.value > 1 ? " years" : " year"}
{...rest}
customInput={Input}
onValueChange={(values) => {
const { floatValue } = values;
onChange(floatValue);
}}
/>
</FormControl>
</div>
<FormMessage className="text-xs font-light" />
</FormItem>
);
}}
/>
</div>
</div>
{equityPlans?.length ? (
<FormField
@@ -26,17 +26,18 @@ import {
StepperPrev,
useStepper,
} from "@/components/ui/stepper";
import { VestingSchedule } from "@/lib/vesting";
import {
SecuritiesStatusEnum,
ShareLegendsEnum,
VestingScheduleEnum,
} from "@/prisma/enums";
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { SecuritiesStatusEnum, ShareLegendsEnum } from "@/prisma/enums";
import { useAddShareFormValues } from "@/providers/add-share-form-provider";
import type { RouterOutputs } from "@/trpc/shared";
import { zodResolver } from "@hookform/resolvers/zod";
import { RiAddFill } from "@remixicon/react";
import { useForm } from "react-hook-form";
import { type UseFormReturn, useForm } from "react-hook-form";
import { NumericFormat } from "react-number-format";
import { z } from "zod";
@@ -58,7 +59,8 @@ const formSchema = z.object({
certificateId: z.string(),
status: z.nativeEnum(SecuritiesStatusEnum),
quantity: z.coerce.number().min(0),
vestingSchedule: z.nativeEnum(VestingScheduleEnum),
cliffYears: z.coerce.number().min(0),
vestingYears: z.coerce.number().min(0),
companyLegends: z.nativeEnum(ShareLegendsEnum).array(),
pricePerShare: z.coerce.number().min(0),
});
@@ -72,14 +74,13 @@ interface GeneralDetailsProps {
}
export const GeneralDetails = ({ shareClasses = [] }: GeneralDetailsProps) => {
const form = useForm<TFormSchema>({
const form: UseFormReturn<TFormSchema> = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
});
const { next } = useStepper();
const { setValue } = useAddShareFormValues();
const status = Object.values(SecuritiesStatusEnum);
const vestingSchedule = Object.values(VestingScheduleEnum);
const companyLegends = Object.values(ShareLegendsEnum);
const handleSubmit = (data: TFormSchema) => {
@@ -92,10 +93,10 @@ export const GeneralDetails = ({ shareClasses = [] }: GeneralDetailsProps) => {
label: share.name,
}));
const vestingScheduleOpts = vestingSchedule.map((vs) => ({
value: vs,
label: VestingSchedule[vs] || "",
}));
// const vestingScheduleOpts = vestingSchedule.map((vs) => ({
// value: vs,
// label: VestingSchedule[vs] || "",
// }));
const statusOpts = status.map((s) => ({
value: s,
@@ -253,22 +254,92 @@ export const GeneralDetails = ({ shareClasses = [] }: GeneralDetailsProps) => {
</div>
</div>
<FormField
control={form.control}
name="vestingSchedule"
render={({ field }) => (
<FormItem>
<FormLabel>Vesting schedule</FormLabel>
<div>
<LinearCombobox
options={vestingScheduleOpts}
onValueChange={(option) => field.onChange(option.value)}
/>
</div>
<FormMessage className="text-xs font-light" />
</FormItem>
)}
/>
<div className="flex items-center gap-4">
<div className="flex-1">
<FormField
control={form.control}
name="vestingYears"
render={({ field }) => {
const { onChange, ...rest } = field;
return (
<FormItem>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<FormLabel>Vesting</FormLabel>
</TooltipTrigger>
<TooltipContent>
<p>Vesting starts over</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<FormControl>
<NumericFormat
thousandSeparator
allowedDecimalSeparators={["%"]}
decimalScale={0}
suffix={field.value > 1 ? " years" : " year"}
{...rest}
customInput={Input}
onValueChange={(values) => {
const { floatValue } = values;
onChange(floatValue);
}}
/>
</FormControl>
</div>
<FormMessage className="text-xs font-light" />
</FormItem>
);
}}
/>
</div>
<div className="flex-1">
<FormField
control={form.control}
name="cliffYears"
render={({ field }) => {
const { onChange, ...rest } = field;
return (
<FormItem>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<FormLabel>Cliff</FormLabel>
</TooltipTrigger>
<TooltipContent>
<p>Vesting starts after</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<FormControl>
<NumericFormat
thousandSeparator
allowedDecimalSeparators={["%"]}
decimalScale={0}
suffix={field.value > 1 ? " years" : " year"}
{...rest}
customInput={Input}
onValueChange={(values) => {
const { floatValue } = values;
onChange(floatValue);
}}
/>
</FormControl>
</div>
<FormMessage className="text-xs font-light" />
</FormItem>
);
}}
/>
</div>
</div>
<FormField
control={form.control}
+1 -1
View File
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { DropdownButton } from "@/components/ui/dropdown-button";
import { api } from "@/trpc/react";
import type { Block } from "@blocknote/core";
import type { Block, PartialBlock } from "@blocknote/core";
import type { Update } from "@prisma/client";
import { RiArrowDownSLine } from "@remixicon/react";
import Link from "next/link";
+25 -9
View File
@@ -1,7 +1,10 @@
import { withCompanyAuth } from "@/server/api/auth";
import { ApiError, ErrorResponses } from "@/server/api/error";
import type { PublicAPI } from "@/server/api/hono";
import { CreateShareSchema } from "@/server/api/schema/shares";
import {
CreateShareSchema,
type ShareSchemaType,
} from "@/server/api/schema/shares";
import { getHonoUserAgent, getIp } from "@/server/api/utils";
import { addShare } from "@/server/services/shares/add-share";
import { createRoute, z } from "@hono/zod-openapi";
@@ -61,7 +64,7 @@ const create = (app: PublicAPI) => {
const { company, member, user } = await withCompanyAuth(c);
const body = await c.req.json();
const { success, message, data } = await addShare({
const response = await addShare({
...body,
companyId: company.id,
memberId: member.id,
@@ -73,6 +76,10 @@ const create = (app: PublicAPI) => {
},
});
const data = response?.data;
const success = response?.success;
const message: string = response?.message.toString();
if (!success || !data) {
throw new ApiError({
code: "INTERNAL_SERVER_ERROR",
@@ -81,8 +88,8 @@ const create = (app: PublicAPI) => {
}
// Ensure data matches ResponseSchema
const responseData = {
status: data.status as string, // Cast to string if necessary
const responseData: ShareSchemaType = {
status: data.status, // Cast to string if necessary
certificateId: data.certificateId,
quantity: data.quantity,
pricePerShare: data.pricePerShare ?? 0,
@@ -90,12 +97,21 @@ const create = (app: PublicAPI) => {
ipContribution: data.ipContribution ?? 0,
debtCancelled: data.debtCancelled ?? 0,
otherContributions: data.otherContributions ?? 0,
vestingSchedule: data.vestingSchedule ?? "",
cliffYears: data.cliffYears ?? 0,
vestingYears: data.vestingYears ?? 0,
companyLegends: data.companyLegends ?? "", // Add missing fields
issueDate: data.issueDate ?? new Date().toISOString(), // Add missing fields
rule144Date: data.rule144Date ?? new Date().toISOString(), // Add missing fields
vestingStartDate: data.vestingStartDate ?? new Date().toISOString(), // Add missing fields
boardApprovalDate: data.boardApprovalDate ?? new Date().toISOString(), // Add boardApprovalDate
issueDate: data.issueDate
? data.issueDate.toISOString()
: new Date().toISOString(), // Add missing fields
rule144Date: data.rule144Date
? data.rule144Date.toISOString()
: new Date().toISOString(), // Convert rule144Date to string
vestingStartDate: data.vestingStartDate
? data.vestingStartDate.toISOString()
: new Date().toISOString(), // Add missing fields
boardApprovalDate: data.boardApprovalDate
? data.boardApprovalDate.toISOString()
: new Date().toISOString(), // Add boardApprovalDate
stakeholderId: data.stakeholderId ?? "", // Add stakeholderId
shareClassId: data.shareClassId,
};
+10 -14
View File
@@ -1,15 +1,6 @@
import { z } from "@hono/zod-openapi";
import {
SecuritiesStatusEnum,
ShareLegendsEnum,
VestingScheduleEnum,
} from "@prisma/client";
import { Share } from "next/font/google";
import { SecuritiesStatusEnum, ShareLegendsEnum } from "@prisma/client";
const VestingScheduleArr = Object.values(VestingScheduleEnum) as [
string,
...string[],
];
const ShareLegendsArr = Object.values(ShareLegendsEnum) as [
string,
...string[],
@@ -66,9 +57,14 @@ export const ShareSchema = z
example: 0,
}),
vestingSchedule: z.enum(VestingScheduleArr).openapi({
description: "Vesting Schedule",
example: "VESTING_0_0_0",
cliffYears: z.number().nullish().openapi({
description: "Cliff Years",
example: 1,
}),
vestingYears: z.number().nullish().openapi({
description: "Vesting Years",
example: 4,
}),
companyLegends: z
@@ -106,7 +102,7 @@ export const ShareSchema = z
example: "cly13ipa40000i7ng42mv4x7b",
}),
companyId: z.string().cuid().openapi({
companyId: z.string().cuid().optional().openapi({
description: "Company ID",
example: "clyvb28ak0000f1ngcn2i0p2m",
}),
+2 -1
View File
@@ -31,7 +31,8 @@ export const addShare = async (input: AddShareType) => {
ipContribution: input.ipContribution,
debtCancelled: input.debtCancelled,
otherContributions: input.otherContributions,
vestingSchedule: input.vestingSchedule,
cliffYears: input.cliffYears,
vestingYears: input.vestingYears,
companyLegends: input.companyLegends,
issueDate: new Date(input.issueDate),
rule144Date: new Date(input.rule144Date),
@@ -28,7 +28,8 @@ export const addOptionProcedure = withAuth
exercisePrice: input.exercisePrice,
type: input.type,
status: input.status,
vestingSchedule: input.vestingSchedule,
cliffYears: input.cliffYears,
vestingYears: input.vestingYears,
issueDate: new Date(input.issueDate),
expirationDate: new Date(input.expirationDate),
vestingStartDate: new Date(input.vestingStartDate),
@@ -33,7 +33,8 @@ export const addShareProcedure = withAuth
ipContribution: input.ipContribution,
debtCancelled: input.debtCancelled,
otherContributions: input.otherContributions,
vestingSchedule: input.vestingSchedule,
cliffYears: input.cliffYears,
vestingYears: input.vestingYears,
companyLegends: input.companyLegends,
issueDate: new Date(input.issueDate),
rule144Date: new Date(input.rule144Date),
@@ -17,7 +17,8 @@ export const getOptionsProcedure = withAuth.query(
exercisePrice: true,
type: true,
status: true,
vestingSchedule: true,
cliffYears: true,
vestingYears: true,
issueDate: true,
expirationDate: true,
vestingStartDate: true,
@@ -19,7 +19,8 @@ export const getSharesProcedure = withAuth.query(
ipContribution: true,
debtCancelled: true,
otherContributions: true,
vestingSchedule: true,
cliffYears: true,
vestingYears: true,
companyLegends: true,
status: true,
+4 -3
View File
@@ -2,7 +2,6 @@ import {
OptionStatusEnum,
OptionTypeEnum,
ShareLegendsEnum,
VestingScheduleEnum,
} from "@/prisma/enums";
import { SecuritiesStatusEnum } from "@prisma/client";
import { z } from "zod";
@@ -16,7 +15,8 @@ export const ZodAddOptionMutationSchema = z.object({
exercisePrice: z.coerce.number().min(0),
type: z.nativeEnum(OptionTypeEnum),
status: z.nativeEnum(OptionStatusEnum),
vestingSchedule: z.nativeEnum(VestingScheduleEnum),
cliffYears: z.coerce.number().min(0),
vestingYears: z.coerce.number().min(0),
issueDate: z.string().date(),
expirationDate: z.string().date(),
vestingStartDate: z.string().date(),
@@ -57,7 +57,8 @@ export const ZodAddShareMutationSchema = z.object({
debtCancelled: z.coerce.number().min(0),
otherContributions: z.coerce.number().min(0),
status: z.nativeEnum(SecuritiesStatusEnum),
vestingSchedule: z.nativeEnum(VestingScheduleEnum),
cliffYears: z.coerce.number().min(0),
vestingYears: z.coerce.number().min(0),
companyLegends: z.nativeEnum(ShareLegendsEnum).array(),
issueDate: z.string().date(),
rule144Date: z.string().date(),