Skip to content

Instantly share code, notes, and snippets.

@PuruVJ
Created November 3, 2023 06:31
Show Gist options
  • Save PuruVJ/81f77a31e91a65bcaa117d7a2b102eca to your computer and use it in GitHub Desktop.
Save PuruVJ/81f77a31e91a65bcaa117d7a2b102eca to your computer and use it in GitHub Desktop.
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