Created
November 3, 2023 06:31
-
-
Save PuruVJ/81f77a31e91a65bcaa117d7a2b102eca to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { FormError } from '$lib/errors.js'; | |
import { db } from '$lib/server/db.js'; | |
import { GROUP_QUERY } from '$lib/server/queries/group.query.js'; | |
import { | |
expenses_table, | |
group_members_table, | |
ledger_table, | |
users_table, | |
} from '$lib/server/schema.js'; | |
import { listify_names, sum_arr } from '$lib/utils.js'; | |
import { error } from '@sveltejs/kit'; | |
import { and, desc, eq, inArray } from 'drizzle-orm'; | |
import { generateRandomString } from 'lucia/utils'; | |
import { ZodError, z } from 'zod'; | |
export async function load({ params: { group_id }, locals }) { | |
const { user } = (await locals.auth.validate())!; | |
const member = await GROUP_QUERY.user_in_group(user.userId, group_id); | |
if (!member) { | |
throw error(403, 'User not in group'); | |
} | |
// Now get all the expenses of the group | |
const expenses = await db | |
.select({ | |
id: expenses_table.id, | |
amount: expenses_table.amount, | |
description: expenses_table.description, | |
created_at: expenses_table.created_at, | |
payer_user_id: expenses_table.payer_user_id, | |
payer_full_name: users_table.full_name, | |
}) | |
.from(expenses_table) | |
.innerJoin(users_table, eq(users_table.id, expenses_table.payer_user_id)) | |
.where(eq(expenses_table.group_id, group_id)) | |
.orderBy(desc(expenses_table.created_at)); | |
return { | |
expenses, | |
}; | |
} | |
const schema = z | |
.object({ | |
description: z.string().min(1, 'Description is required').max(200, 'Description too long'), | |
amount: z | |
.string() | |
.min(1, 'Amount is required') | |
.transform((v) => +v) | |
.refine((amount) => !isNaN(amount), { message: 'Amount must be a number' }) | |
.refine((amount) => amount > 0, { | |
message: 'Amount must be greater than 0', | |
}), | |
members: z.array(z.string()).min(1, 'Please select at least one member'), | |
values: z.array(z.string().min(1, 'Required').default('0')).transform((v) => v.map(Number)), | |
mode: z.enum(['equally', 'unequally', 'percentage', 'share']).default('equally'), | |
group_id: z.string(), | |
}) | |
.refine( | |
({ mode, members, values }) => !(mode !== 'equally' && members.length !== values.length), | |
{ | |
message: 'Please enter values for all members', | |
path: ['values'], | |
}, | |
) | |
.refine(({ mode, values, amount }) => !(mode === 'unequally' && sum_arr(values) !== amount), { | |
message: 'Ledger sum must be equal to amount', | |
path: ['values'], | |
}) | |
.transform((v) => { | |
const { values, members, mode, amount } = v; | |
const values_sum = sum_arr(values); | |
const ledger: Record<string, number> = {}; | |
for (let i = 0; i < members.length; i++) { | |
const id = members[i]; | |
let value = 0; | |
if (mode === 'equally') { | |
value = amount / members.length; | |
} else if (mode === 'unequally') { | |
value = values[i]; | |
} else if (mode === 'percentage') { | |
value = (amount * values[i]) / 100; | |
} else if (mode === 'share') { | |
value = (values[i] / values_sum) * amount; | |
} | |
ledger[id] = value; | |
} | |
return { ...v, ledger }; | |
}) | |
.superRefine(async ({ group_id, members }, ctx) => { | |
// Check whether `members` array fit within the group | |
const members_info = await db | |
.select({ | |
user_id: users_table.id, | |
full_name: users_table.full_name, | |
}) | |
.from(group_members_table) | |
.innerJoin(users_table, eq(users_table.id, group_members_table.user_id)) | |
.where( | |
and( | |
eq(group_members_table.group_id, group_id), | |
inArray(group_members_table.user_id, members), | |
), | |
); | |
if (members_info.length !== members.length) { | |
// Some of the members sent don't exist. Figure out which, and send back | |
const members_in_group = members_info.map((m) => m.user_id); | |
const members_not_in_group = members.filter((m) => !members_in_group.includes(m.toString())); | |
ctx.addIssue({ | |
code: 'custom', | |
message: `${listify_names( | |
members_not_in_group | |
.map((v) => members_info.find((mi) => mi.user_id === v)?.full_name) | |
.filter(Boolean) as string[], | |
)} are not in the group. Try again`, | |
path: ['members'], | |
}); | |
} | |
}); | |
export const actions = { | |
default: async ({ request, params: { group_id }, locals }) => { | |
const formdata = await request.formData(); | |
const amount = +(formdata.get('amount') ?? 0); | |
const description = formdata.get('description'); | |
const members = formdata.getAll('members'); | |
const values = formdata.getAll('values').map(Number); | |
const mode = formdata.get('mode'); | |
const data = { | |
amount, | |
description, | |
members, | |
values, | |
mode, | |
group_id, | |
}; | |
const form_errors = new FormError(); | |
const { user } = (await locals.auth.validate())!; | |
// Check first if user in group | |
const member = await GROUP_QUERY.user_in_group(user.userId, group_id); | |
if (!member) return { success: false, message: 'User not in group', errors: [] }; | |
try { | |
const { amount, description, members, values } = await schema.parseAsync(data); | |
// Add expense | |
const expense_id = generateRandomString(31); | |
console.time('insert'); | |
await db.batch([ | |
db.insert(expenses_table).values({ | |
id: expense_id, | |
group_id, | |
amount, | |
description, | |
created_at: new Date(), | |
currency: 'INR', | |
payer_user_id: user.userId, | |
}), | |
db.insert(ledger_table).values( | |
members.map((user_id, idx) => ({ | |
payer_user_id: user.userId, | |
expense_id, | |
share: values[idx], | |
payee_user_id: user_id, | |
})), | |
), | |
]); | |
console.timeEnd('insert'); | |
} catch (e) { | |
if (e instanceof ZodError) { | |
form_errors.add_from_zod(e); | |
} | |
console.log('add expense', e); | |
return form_errors.throw(); | |
} | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment