From 3ac5b770a004a3fcff0cbae6265876d8018ad9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Gubitosi?= Date: Fri, 19 Jun 2026 13:20:56 -0300 Subject: [PATCH] feat(securities): express vesting & cliff in months instead of years Vesting and cliff durations for shares and stock options were entered and stored in years; switch the unit to months (e.g. a 4-year / 1-year-cliff grant is now 48 months / 12 months). - Rename the Share/Option `vestingYears`/`cliffYears` fields to `vestingMonths`/`cliffMonths`, with a Prisma migration that renames the columns and converts existing values from years to months (x12). - Update the Zod mutation schemas, the public OpenAPI schema, the add/get procedures for shares and options, and the share/option creation forms (the numeric input suffix now reads "month(s)"). - No vesting math exists in the app, so this is purely a unit/label/storage change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migration.sql | 20 +++++++++++++++++++ prisma/schema.prisma | 8 ++++---- .../options/steps/vesting-details.tsx | 12 +++++------ .../shares/steps/general-details.tsx | 12 +++++------ src/server/api/schema/shares.ts | 12 +++++------ .../procedures/add-option.ts | 4 ++-- .../securities-router/procedures/add-share.ts | 6 ++---- .../procedures/get-options.ts | 4 ++-- .../procedures/get-shares.ts | 4 ++-- src/trpc/routers/securities-router/schema.ts | 8 ++++---- 10 files changed, 54 insertions(+), 36 deletions(-) create mode 100644 prisma/migrations/20260619131445_vesting_cliff_in_months/migration.sql diff --git a/prisma/migrations/20260619131445_vesting_cliff_in_months/migration.sql b/prisma/migrations/20260619131445_vesting_cliff_in_months/migration.sql new file mode 100644 index 000000000..62ae4769a --- /dev/null +++ b/prisma/migrations/20260619131445_vesting_cliff_in_months/migration.sql @@ -0,0 +1,20 @@ +/* + Vesting and cliff durations are now expressed in MONTHS instead of years. + + - Rename "vestingYears" -> "vestingMonths" and "cliffYears" -> "cliffMonths" + on the "Share" and "Option" tables. + - Convert existing values from years to months by multiplying by 12 + (e.g. a 4-year / 1-year-cliff grant becomes 48 / 12 months). +*/ + +-- AlterTable +ALTER TABLE "Share" RENAME COLUMN "vestingYears" TO "vestingMonths"; +ALTER TABLE "Share" RENAME COLUMN "cliffYears" TO "cliffMonths"; + +-- AlterTable +ALTER TABLE "Option" RENAME COLUMN "vestingYears" TO "vestingMonths"; +ALTER TABLE "Option" RENAME COLUMN "cliffYears" TO "cliffMonths"; + +-- Convert existing durations from years to months +UPDATE "Share" SET "vestingMonths" = "vestingMonths" * 12, "cliffMonths" = "cliffMonths" * 12; +UPDATE "Option" SET "vestingMonths" = "vestingMonths" * 12, "cliffMonths" = "cliffMonths" * 12; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7a2c88460..d4762b8a6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -690,8 +690,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 + cliffMonths Int @default(0) // vesting cliff in months; 0 means immediate vesting + vestingMonths Int @default(0) // total vesting period in months; 0 means immediate vesting companyLegends ShareLegendsEnum[] @default([]) @@ -742,8 +742,8 @@ model Option { 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 + cliffMonths Int @default(0) // vesting cliff in months; 0 means immediate vesting + vestingMonths Int @default(0) // total vesting period in months; 0 means immediate vesting issueDate DateTime expirationDate DateTime diff --git a/src/components/securities/options/steps/vesting-details.tsx b/src/components/securities/options/steps/vesting-details.tsx index 969d826ee..48085d85c 100644 --- a/src/components/securities/options/steps/vesting-details.tsx +++ b/src/components/securities/options/steps/vesting-details.tsx @@ -32,8 +32,8 @@ import { EmptySelect } from "../../shared/EmptySelect"; const formSchema = z.object({ equityPlanId: z.string(), - cliffYears: z.coerce.number().min(0), - vestingYears: z.coerce.number().min(0), + cliffMonths: z.coerce.number().min(0), + vestingMonths: z.coerce.number().min(0), exercisePrice: z.coerce.number(), stakeholderId: z.string(), }); @@ -85,7 +85,7 @@ export const VestingDetails = (props: VestingDetailsProps) => {
{ const { onChange, ...rest } = field; return ( @@ -105,7 +105,7 @@ export const VestingDetails = (props: VestingDetailsProps) => { 1 ? " years" : " year"} + suffix={field.value > 1 ? " months" : " month"} decimalScale={0} {...rest} customInput={Input} @@ -126,7 +126,7 @@ export const VestingDetails = (props: VestingDetailsProps) => {
{ const { onChange, ...rest } = field; return ( @@ -147,7 +147,7 @@ export const VestingDetails = (props: VestingDetailsProps) => { thousandSeparator allowedDecimalSeparators={["%"]} decimalScale={0} - suffix={field.value > 1 ? " years" : " year"} + suffix={field.value > 1 ? " months" : " month"} {...rest} customInput={Input} onValueChange={(values) => { diff --git a/src/components/securities/shares/steps/general-details.tsx b/src/components/securities/shares/steps/general-details.tsx index b03be7edb..5ff61cb70 100644 --- a/src/components/securities/shares/steps/general-details.tsx +++ b/src/components/securities/shares/steps/general-details.tsx @@ -59,8 +59,8 @@ const formSchema = z.object({ certificateId: z.string(), status: z.nativeEnum(SecuritiesStatusEnum), quantity: z.coerce.number().min(0), - cliffYears: z.coerce.number().min(0), - vestingYears: z.coerce.number().min(0), + cliffMonths: z.coerce.number().min(0), + vestingMonths: z.coerce.number().min(0), companyLegends: z.nativeEnum(ShareLegendsEnum).array(), pricePerShare: z.coerce.number().min(0), }); @@ -258,7 +258,7 @@ export const GeneralDetails = ({ shareClasses = [] }: GeneralDetailsProps) => {
{ const { onChange, ...rest } = field; return ( @@ -280,7 +280,7 @@ export const GeneralDetails = ({ shareClasses = [] }: GeneralDetailsProps) => { thousandSeparator allowedDecimalSeparators={["%"]} decimalScale={0} - suffix={field.value > 1 ? " years" : " year"} + suffix={field.value > 1 ? " months" : " month"} {...rest} customInput={Input} onValueChange={(values) => { @@ -301,7 +301,7 @@ export const GeneralDetails = ({ shareClasses = [] }: GeneralDetailsProps) => {
{ const { onChange, ...rest } = field; return ( @@ -322,7 +322,7 @@ export const GeneralDetails = ({ shareClasses = [] }: GeneralDetailsProps) => { thousandSeparator allowedDecimalSeparators={["%"]} decimalScale={0} - suffix={field.value > 1 ? " years" : " year"} + suffix={field.value > 1 ? " months" : " month"} {...rest} customInput={Input} onValueChange={(values) => { diff --git a/src/server/api/schema/shares.ts b/src/server/api/schema/shares.ts index a8401eef6..8b3784d28 100644 --- a/src/server/api/schema/shares.ts +++ b/src/server/api/schema/shares.ts @@ -57,13 +57,13 @@ export const ShareSchema = z example: 0, }), - cliffYears: z.number().openapi({ - description: "Cliff Years", - example: 1, + cliffMonths: z.number().openapi({ + description: "Vesting cliff in months", + example: 12, }), - vestingYears: z.number().openapi({ - description: "Vesting Years", - example: 4, + vestingMonths: z.number().openapi({ + description: "Total vesting period in months", + example: 48, }), companyLegends: z diff --git a/src/trpc/routers/securities-router/procedures/add-option.ts b/src/trpc/routers/securities-router/procedures/add-option.ts index 584868a23..1fdd1ca89 100644 --- a/src/trpc/routers/securities-router/procedures/add-option.ts +++ b/src/trpc/routers/securities-router/procedures/add-option.ts @@ -28,8 +28,8 @@ export const addOptionProcedure = withAuth exercisePrice: input.exercisePrice, type: input.type, status: input.status, - cliffYears: input.cliffYears, - vestingYears: input.vestingYears, + cliffMonths: input.cliffMonths, + vestingMonths: input.vestingMonths, issueDate: new Date(input.issueDate), expirationDate: new Date(input.expirationDate), vestingStartDate: new Date(input.vestingStartDate), diff --git a/src/trpc/routers/securities-router/procedures/add-share.ts b/src/trpc/routers/securities-router/procedures/add-share.ts index 694d0292f..692be5122 100644 --- a/src/trpc/routers/securities-router/procedures/add-share.ts +++ b/src/trpc/routers/securities-router/procedures/add-share.ts @@ -7,8 +7,6 @@ import { ZodAddShareMutationSchema } from "../schema"; export const addShareProcedure = withAuth .input(ZodAddShareMutationSchema) .mutation(async ({ ctx, input }) => { - console.log({ input }, "#############"); - const { userAgent, requestIp } = ctx; try { @@ -33,8 +31,8 @@ export const addShareProcedure = withAuth ipContribution: input.ipContribution, debtCancelled: input.debtCancelled, otherContributions: input.otherContributions, - cliffYears: input.cliffYears, - vestingYears: input.vestingYears, + cliffMonths: input.cliffMonths, + vestingMonths: input.vestingMonths, companyLegends: input.companyLegends, issueDate: new Date(input.issueDate), rule144Date: new Date(input.rule144Date), diff --git a/src/trpc/routers/securities-router/procedures/get-options.ts b/src/trpc/routers/securities-router/procedures/get-options.ts index eb8ef9e2a..ff7f6f377 100644 --- a/src/trpc/routers/securities-router/procedures/get-options.ts +++ b/src/trpc/routers/securities-router/procedures/get-options.ts @@ -17,8 +17,8 @@ export const getOptionsProcedure = withAuth.query( exercisePrice: true, type: true, status: true, - cliffYears: true, - vestingYears: true, + cliffMonths: true, + vestingMonths: true, issueDate: true, expirationDate: true, vestingStartDate: true, diff --git a/src/trpc/routers/securities-router/procedures/get-shares.ts b/src/trpc/routers/securities-router/procedures/get-shares.ts index 678647051..a649ae4f8 100644 --- a/src/trpc/routers/securities-router/procedures/get-shares.ts +++ b/src/trpc/routers/securities-router/procedures/get-shares.ts @@ -19,8 +19,8 @@ export const getSharesProcedure = withAuth.query( ipContribution: true, debtCancelled: true, otherContributions: true, - cliffYears: true, - vestingYears: true, + cliffMonths: true, + vestingMonths: true, companyLegends: true, status: true, diff --git a/src/trpc/routers/securities-router/schema.ts b/src/trpc/routers/securities-router/schema.ts index 0bc188601..2a2663257 100644 --- a/src/trpc/routers/securities-router/schema.ts +++ b/src/trpc/routers/securities-router/schema.ts @@ -15,8 +15,8 @@ export const ZodAddOptionMutationSchema = z.object({ exercisePrice: z.coerce.number().min(0), type: z.nativeEnum(OptionTypeEnum), status: z.nativeEnum(OptionStatusEnum), - cliffYears: z.coerce.number().min(0), - vestingYears: z.coerce.number().min(0), + cliffMonths: z.coerce.number().min(0), + vestingMonths: z.coerce.number().min(0), issueDate: z.string().date(), expirationDate: z.string().date(), vestingStartDate: z.string().date(), @@ -57,8 +57,8 @@ export const ZodAddShareMutationSchema = z.object({ debtCancelled: z.coerce.number().min(0), otherContributions: z.coerce.number().min(0), status: z.nativeEnum(SecuritiesStatusEnum), - cliffYears: z.coerce.number().min(0), - vestingYears: z.coerce.number().min(0), + cliffMonths: z.coerce.number().min(0), + vestingMonths: z.coerce.number().min(0), companyLegends: z.nativeEnum(ShareLegendsEnum).array(), issueDate: z.string().date(), rule144Date: z.string().date(),