From 0a8e56f80033ce889b38b01f2287cd6fbbe36dd6 Mon Sep 17 00:00:00 2001 From: Sebastien Castiel Date: Mon, 8 Jan 2024 18:11:11 +0100 Subject: Add splitmode and shares to expenses (#11) * Add splitmode and shares to expenses * Update balances based on shares * Change field size * Form validation * Redesign expense form * Split unevenly by amount --- src/lib/schemas.ts | 132 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 28 deletions(-) (limited to 'src/lib/schemas.ts') diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 0bebae6..9b7d428 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -1,3 +1,4 @@ +import { SplitMode } from '@prisma/client' import * as z from 'zod' export const groupFormSchema = z @@ -38,36 +39,111 @@ export const groupFormSchema = z export type GroupFormValues = z.infer -export const expenseFormSchema = z.object({ - title: z - .string({ required_error: 'Please enter a title.' }) - .min(2, 'Enter at least two characters.'), - amount: z - .union( - [ - z.number(), - z.string().transform((value, ctx) => { - const valueAsNumber = Number(value) - if (Number.isNaN(valueAsNumber)) +export const expenseFormSchema = z + .object({ + title: z + .string({ required_error: 'Please enter a title.' }) + .min(2, 'Enter at least two characters.'), + amount: z + .union( + [ + z.number(), + z.string().transform((value, ctx) => { + const valueAsNumber = Number(value) + if (Number.isNaN(valueAsNumber)) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid number.', + }) + return Math.round(valueAsNumber * 100) + }), + ], + { required_error: 'You must enter an amount.' }, + ) + .refine((amount) => amount >= 1, 'The amount must be higher than 0.01.') + .refine( + (amount) => amount <= 10_000_000_00, + 'The amount must be lower than 10,000,000.', + ), + paidBy: z.string({ required_error: 'You must select a participant.' }), + paidFor: z + .array( + z.object({ + participant: z.string(), + shares: z.union([ + z.number(), + z.string().transform((value, ctx) => { + const valueAsNumber = Number(value) + if (Number.isNaN(valueAsNumber)) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid number.', + }) + return Math.round(valueAsNumber * 100) + }), + ]), + }), + ) + .min(1, 'The expense must be paid for at least one participant.') + .superRefine((paidFor, ctx) => { + let sum = 0 + for (const { shares } of paidFor) { + sum += shares + if (shares < 1) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'Invalid number.', + message: 'All shares must be higher than 0.', }) - return Math.round(valueAsNumber * 100) - }), - ], - { required_error: 'You must enter an amount.' }, - ) - .refine((amount) => amount >= 1, 'The amount must be higher than 0.01.') - .refine( - (amount) => amount <= 10_000_000_00, - 'The amount must be lower than 10,000,000.', - ), - paidBy: z.string({ required_error: 'You must select a participant.' }), - paidFor: z - .array(z.string()) - .min(1, 'The expense must be paid for at least one participant.'), - isReimbursement: z.boolean(), -}) + } + } + }), + splitMode: z + .enum( + Object.values(SplitMode) as any, + ) + .default('EVENLY'), + isReimbursement: z.boolean(), + }) + .superRefine((expense, ctx) => { + let sum = 0 + for (const { shares } of expense.paidFor) { + sum += + typeof shares === 'number' ? shares : Math.round(Number(shares) * 100) + } + switch (expense.splitMode) { + case 'EVENLY': + break // noop + case 'BY_SHARES': + break // noop + case 'BY_AMOUNT': { + if (sum !== expense.amount) { + const detail = + sum < expense.amount + ? `${((expense.amount - sum) / 100).toFixed(2)} missing` + : `${((sum - expense.amount) / 100).toFixed(2)} surplus` + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Sum of amounts must equal the expense amount (${detail}).`, + path: ['paidFor'], + }) + } + break + } + case 'BY_PERCENTAGE': { + if (sum !== 10000) { + const detail = + sum < 10000 + ? `${((10000 - sum) / 100).toFixed(0)}% missing` + : `${((sum - 10000) / 100).toFixed(0)}% surplus` + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Sum of percentages must equal 100 (${detail})`, + path: ['paidFor'], + }) + } + break + } + } + }) export type ExpenseFormValues = z.infer -- cgit v1.3