aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/schemas.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/schemas.ts')
-rw-r--r--src/lib/schemas.ts132
1 files changed, 104 insertions, 28 deletions
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<typeof groupFormSchema>
-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<SplitMode, [SplitMode, ...SplitMode[]]>(
+ 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<typeof expenseFormSchema>