2 Commits

Author SHA1 Message Date
Sebastien Castiel
a11efc79c1 Fix Prettier issues
All checks were successful
CI / checks (push) Successful in 1m34s
2025-04-20 11:10:30 -04:00
Sebastien Castiel
e63f3aa68f Fix TypeScript issues
Some checks failed
CI / checks (push) Failing after 1m35s
2025-04-19 15:46:37 -04:00
4 changed files with 137 additions and 118 deletions

View File

@@ -191,7 +191,7 @@ function ReceiptDialogContent() {
<Unknown /> <Unknown />
) )
) : ( ) : (
'' || '…' ''
)} )}
</div> </div>
</div> </div>

View File

@@ -44,6 +44,7 @@ import { calculateShare } from '@/lib/totals'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { AppRouterOutput } from '@/trpc/routers/_app' import { AppRouterOutput } from '@/trpc/routers/_app'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { RecurrenceRule } from '@prisma/client'
import { Save } from 'lucide-react' import { Save } from 'lucide-react'
import { useTranslations } from 'next-intl' import { useTranslations } from 'next-intl'
import Link from 'next/link' import Link from 'next/link'
@@ -54,7 +55,6 @@ import { match } from 'ts-pattern'
import { DeletePopup } from '../../../../components/delete-popup' import { DeletePopup } from '../../../../components/delete-popup'
import { extractCategoryFromTitle } from '../../../../components/expense-form-actions' import { extractCategoryFromTitle } from '../../../../components/expense-form-actions'
import { Textarea } from '../../../../components/ui/textarea' import { Textarea } from '../../../../components/ui/textarea'
import { RecurrenceRule } from '@prisma/client'
const enforceCurrencyPattern = (value: string) => const enforceCurrencyPattern = (value: string) =>
value value
@@ -190,7 +190,7 @@ export function ExpenseForm({
isReimbursement: expense.isReimbursement, isReimbursement: expense.isReimbursement,
documents: expense.documents, documents: expense.documents,
notes: expense.notes ?? '', notes: expense.notes ?? '',
recurrenceRule: expense.recurrenceRule, recurrenceRule: expense.recurrenceRule ?? undefined,
} }
: searchParams.get('reimbursement') : searchParams.get('reimbursement')
? { ? {
@@ -516,7 +516,7 @@ export function ExpenseForm({
defaultValue={getSelectedRecurrenceRule(field)} defaultValue={getSelectedRecurrenceRule(field)}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="NONE"/> <SelectValue placeholder="NONE" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="NONE"> <SelectItem value="NONE">

View File

@@ -1,6 +1,11 @@
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas' import { ExpenseFormValues, GroupFormValues } from '@/lib/schemas'
import { ActivityType, Expense, RecurrenceRule, RecurringExpenseLink } from '@prisma/client' import {
ActivityType,
Expense,
RecurrenceRule,
RecurringExpenseLink,
} from '@prisma/client'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
export function randomId() { export function randomId() {
@@ -50,11 +55,12 @@ export async function createExpense(
data: expenseFormValues.title, data: expenseFormValues.title,
}) })
const isCreateRecurrence = expenseFormValues.recurrenceRule !== RecurrenceRule.NONE const isCreateRecurrence =
expenseFormValues.recurrenceRule !== RecurrenceRule.NONE
const recurringExpenseLinkPayload = createPayloadForNewRecurringExpenseLink( const recurringExpenseLinkPayload = createPayloadForNewRecurringExpenseLink(
expenseFormValues.recurrenceRule as RecurrenceRule, expenseFormValues.recurrenceRule as RecurrenceRule,
expenseFormValues.expenseDate, expenseFormValues.expenseDate,
groupId groupId,
) )
return prisma.expense.create({ return prisma.expense.create({
@@ -71,11 +77,9 @@ export async function createExpense(
recurringExpenseLink: { recurringExpenseLink: {
...(isCreateRecurrence ...(isCreateRecurrence
? { ? {
create: recurringExpenseLinkPayload create: recurringExpenseLinkPayload,
} }
: {} : {}),
),
}, },
paidFor: { paidFor: {
createMany: { createMany: {
@@ -169,30 +173,31 @@ export async function updateExpense(
data: expenseFormValues.title, data: expenseFormValues.title,
}) })
const isDeleteRecurrenceExpenseLink = const isDeleteRecurrenceExpenseLink =
existingExpense.recurrenceRule !== RecurrenceRule.NONE && existingExpense.recurrenceRule !== RecurrenceRule.NONE &&
expenseFormValues.recurrenceRule === RecurrenceRule.NONE && expenseFormValues.recurrenceRule === RecurrenceRule.NONE &&
// Delete the existing RecurrenceExpenseLink only if it has not been acted upon yet // Delete the existing RecurrenceExpenseLink only if it has not been acted upon yet
existingExpense.recurringExpenseLink?.nextExpenseCreatedAt === null existingExpense.recurringExpenseLink?.nextExpenseCreatedAt === null
const isUpdateRecurrenceExpenseLink = existingExpense.recurrenceRule !== expenseFormValues.recurrenceRule && const isUpdateRecurrenceExpenseLink =
existingExpense.recurrenceRule !== expenseFormValues.recurrenceRule &&
// Update the exisiting RecurrenceExpenseLink only if it has not been acted upon yet // Update the exisiting RecurrenceExpenseLink only if it has not been acted upon yet
existingExpense.recurringExpenseLink?.nextExpenseCreatedAt === null existingExpense.recurringExpenseLink?.nextExpenseCreatedAt === null
const isCreateRecurrenceExpenseLink = const isCreateRecurrenceExpenseLink =
existingExpense.recurrenceRule === RecurrenceRule.NONE && existingExpense.recurrenceRule === RecurrenceRule.NONE &&
expenseFormValues.recurrenceRule !== RecurrenceRule.NONE && expenseFormValues.recurrenceRule !== RecurrenceRule.NONE &&
// Create a new RecurrenceExpenseLink only if one does not already exist for the expense // Create a new RecurrenceExpenseLink only if one does not already exist for the expense
existingExpense.recurringExpenseLink === null existingExpense.recurringExpenseLink === null
const newRecurringExpenseLink = createPayloadForNewRecurringExpenseLink( const newRecurringExpenseLink = createPayloadForNewRecurringExpenseLink(
expenseFormValues.recurrenceRule as RecurrenceRule, expenseFormValues.recurrenceRule as RecurrenceRule,
expenseFormValues.expenseDate, expenseFormValues.expenseDate,
groupId groupId,
) )
const updatedRecurrenceExpenseLinkNextExpenseDate = calculateNextDate( const updatedRecurrenceExpenseLinkNextExpenseDate = calculateNextDate(
expenseFormValues.recurrenceRule as RecurrenceRule, expenseFormValues.recurrenceRule as RecurrenceRule,
existingExpense.expenseDate existingExpense.expenseDate,
) )
return prisma.expense.update({ return prisma.expense.update({
@@ -238,18 +243,16 @@ export async function updateExpense(
recurringExpenseLink: { recurringExpenseLink: {
...(isCreateRecurrenceExpenseLink ...(isCreateRecurrenceExpenseLink
? { ? {
create: newRecurringExpenseLink create: newRecurringExpenseLink,
} }
: {} : {}),
),
...(isUpdateRecurrenceExpenseLink ...(isUpdateRecurrenceExpenseLink
? { ? {
update: { update: {
nextExpenseDate: updatedRecurrenceExpenseLinkNextExpenseDate nextExpenseDate: updatedRecurrenceExpenseLinkNextExpenseDate,
},
} }
} : {}),
: {}
),
delete: isDeleteRecurrenceExpenseLink, delete: isDeleteRecurrenceExpenseLink,
}, },
isReimbursement: expenseFormValues.isReimbursement, isReimbursement: expenseFormValues.isReimbursement,
@@ -371,7 +374,13 @@ export async function getGroupExpenseCount(groupId: string) {
export async function getExpense(groupId: string, expenseId: string) { export async function getExpense(groupId: string, expenseId: string) {
return prisma.expense.findUnique({ return prisma.expense.findUnique({
where: { id: expenseId }, where: { id: expenseId },
include: { paidBy: true, paidFor: true, category: true, documents: true, recurringExpenseLink: true }, include: {
paidBy: true,
paidFor: true,
category: true,
documents: true,
recurringExpenseLink: true,
},
}) })
} }
@@ -420,35 +429,38 @@ export async function logActivity(
}) })
} }
async function createRecurringExpenses(){ async function createRecurringExpenses() {
const localDate = new Date(); // Current local date const localDate = new Date() // Current local date
const utcDateFromLocal = new Date(Date.UTC( const utcDateFromLocal = new Date(
Date.UTC(
localDate.getUTCFullYear(), localDate.getUTCFullYear(),
localDate.getUTCMonth(), localDate.getUTCMonth(),
localDate.getUTCDate(), localDate.getUTCDate(),
// More precision beyond date is required to ensure that recurring Expenses are created within <most precises unit> of when expected // More precision beyond date is required to ensure that recurring Expenses are created within <most precises unit> of when expected
localDate.getUTCHours(), localDate.getUTCHours(),
localDate.getUTCMinutes(), localDate.getUTCMinutes(),
)); ),
)
const recurringExpenseLinksWithExpensesToCreate = await prisma.recurringExpenseLink.findMany({ const recurringExpenseLinksWithExpensesToCreate =
where: { await prisma.recurringExpenseLink.findMany({
nextExpenseCreatedAt: null, where: {
nextExpenseDate: { nextExpenseCreatedAt: null,
lte: utcDateFromLocal nextExpenseDate: {
} lte: utcDateFromLocal,
},
include: {
currentFrameExpense: {
include: {
paidBy: true,
paidFor: true,
category: true,
documents: true
}, },
} },
} include: {
}) currentFrameExpense: {
include: {
paidBy: true,
paidFor: true,
category: true,
documents: true,
},
},
},
})
for (const recurringExpenseLink of recurringExpenseLinksWithExpensesToCreate) { for (const recurringExpenseLink of recurringExpenseLinksWithExpensesToCreate) {
let newExpenseDate = recurringExpenseLink.nextExpenseDate let newExpenseDate = recurringExpenseLink.nextExpenseDate
@@ -459,74 +471,84 @@ async function createRecurringExpenses(){
while (newExpenseDate < utcDateFromLocal) { while (newExpenseDate < utcDateFromLocal) {
const newExpenseId = randomId() const newExpenseId = randomId()
const newRecurringExpenseLinkId = randomId() const newRecurringExpenseLinkId = randomId()
const newRecurringExpenseNextExpenseDate = calculateNextDate( const newRecurringExpenseNextExpenseDate = calculateNextDate(
currentExpenseRecord.recurrenceRule as RecurrenceRule, currentExpenseRecord.recurrenceRule as RecurrenceRule,
newExpenseDate newExpenseDate,
) )
const { const {
category, paidBy, paidFor, documents, category,
paidBy,
paidFor,
documents,
...destructeredCurrentExpenseRecord ...destructeredCurrentExpenseRecord
} = currentExpenseRecord } = currentExpenseRecord
// Use a transacton to ensure that the only one expense is created for the RecurringExpenseLink // Use a transacton to ensure that the only one expense is created for the RecurringExpenseLink
// just in case two clients are processing the same RecurringExpenseLink at the same time // just in case two clients are processing the same RecurringExpenseLink at the same time
const newExpense = await prisma.$transaction(async (transaction) => { const newExpense = await prisma
const newExpense = await transaction.expense.create({ .$transaction(async (transaction) => {
data: { const newExpense = await transaction.expense.create({
...destructeredCurrentExpenseRecord, data: {
categoryId: currentExpenseRecord.categoryId, ...destructeredCurrentExpenseRecord,
paidById: currentExpenseRecord.paidById, categoryId: currentExpenseRecord.categoryId,
paidFor: { paidById: currentExpenseRecord.paidById,
createMany: { paidFor: {
data: currentExpenseRecord.paidFor.map((paidFor) => ({ createMany: {
participantId: paidFor.participantId, data: currentExpenseRecord.paidFor.map((paidFor) => ({
shares: paidFor.shares, participantId: paidFor.participantId,
})), shares: paidFor.shares,
})),
},
},
documents: {
connect: currentExpenseRecord.documents.map(
(documentRecord) => ({
id: documentRecord.id,
}),
),
},
id: newExpenseId,
expenseDate: newExpenseDate,
recurringExpenseLink: {
create: {
groupId: currentExpenseRecord.groupId,
id: newRecurringExpenseLinkId,
nextExpenseDate: newRecurringExpenseNextExpenseDate,
},
}, },
}, },
documents: { // Ensure that the same information is available on the returned record that was created
connect: currentExpenseRecord.documents.map((documentRecord) => ({ include: {
id: documentRecord.id paidFor: true,
})), documents: true,
category: true,
paidBy: true,
}, },
id: newExpenseId, })
expenseDate: newExpenseDate,
recurringExpenseLink: {
create: {
groupId: currentExpenseRecord.groupId,
id: newRecurringExpenseLinkId,
nextExpenseDate: newRecurringExpenseNextExpenseDate
}
}
},
// Ensure that the same information is available on the returned record that was created
include: {
paidFor: true,
documents: true,
category: true,
paidBy: true
}
})
// Mark the RecurringExpenseLink as being "completed" since the new Expense was created // Mark the RecurringExpenseLink as being "completed" since the new Expense was created
// if an expense hasn't been created for this RecurringExpenseLink yet // if an expense hasn't been created for this RecurringExpenseLink yet
await transaction.recurringExpenseLink.update({ await transaction.recurringExpenseLink.update({
where: { where: {
id: currentReccuringExpenseLinkId, id: currentReccuringExpenseLinkId,
nextExpenseCreatedAt: null, nextExpenseCreatedAt: null,
}, },
data: { data: {
nextExpenseCreatedAt: newExpense.createdAt nextExpenseCreatedAt: newExpense.createdAt,
}, },
}) })
return newExpense return newExpense
}).catch(() => { })
console.error("Failed to created recurringExpense for expenseId: %s", currentExpenseRecord.id) .catch(() => {
return null console.error(
}) 'Failed to created recurringExpense for expenseId: %s',
currentExpenseRecord.id,
)
return null
})
// If the new expense failed to be created, break out of the while-loop // If the new expense failed to be created, break out of the while-loop
if (newExpense === null) break if (newExpense === null) break
@@ -545,15 +567,15 @@ function createPayloadForNewRecurringExpenseLink(
groupId: String, groupId: String,
): RecurringExpenseLink { ): RecurringExpenseLink {
const nextExpenseDate = calculateNextDate( const nextExpenseDate = calculateNextDate(
recurrenceRule, recurrenceRule,
priorDateToNextRecurrence priorDateToNextRecurrence,
) )
const recurringExpenseLinkId = randomId() const recurringExpenseLinkId = randomId()
const recurringExpenseLinkPayload = { const recurringExpenseLinkPayload = {
id: recurringExpenseLinkId, id: recurringExpenseLinkId,
groupId: groupId, groupId: groupId,
nextExpenseDate: nextExpenseDate nextExpenseDate: nextExpenseDate,
} }
return recurringExpenseLinkPayload as RecurringExpenseLink return recurringExpenseLinkPayload as RecurringExpenseLink
@@ -567,10 +589,10 @@ function createPayloadForNewRecurringExpenseLink(
// will be created for Feb 28th, March 28, etc. until it is cancelled or fixed // will be created for Feb 28th, March 28, etc. until it is cancelled or fixed
function calculateNextDate( function calculateNextDate(
recurrenceRule: RecurrenceRule, recurrenceRule: RecurrenceRule,
priorDateToNextRecurrence: Date priorDateToNextRecurrence: Date,
): Date { ): Date {
const nextDate = new Date(priorDateToNextRecurrence) const nextDate = new Date(priorDateToNextRecurrence)
switch(recurrenceRule) { switch (recurrenceRule) {
case RecurrenceRule.DAILY: case RecurrenceRule.DAILY:
nextDate.setUTCDate(nextDate.getUTCDate() + 1) nextDate.setUTCDate(nextDate.getUTCDate() + 1)
break break
@@ -578,7 +600,7 @@ function calculateNextDate(
nextDate.setUTCDate(nextDate.getUTCDate() + 7) nextDate.setUTCDate(nextDate.getUTCDate() + 7)
break break
case RecurrenceRule.MONTHLY: case RecurrenceRule.MONTHLY:
const nextYear = nextDate.getUTCFullYear() const nextYear = nextDate.getUTCFullYear()
const nextMonth = nextDate.getUTCMonth() + 1 const nextMonth = nextDate.getUTCMonth() + 1
let nextDay = nextDate.getUTCDate() let nextDay = nextDate.getUTCDate()
@@ -596,15 +618,12 @@ function calculateNextDate(
function isDateInNextMonth( function isDateInNextMonth(
utcYear: number, utcYear: number,
utcMonth: number, utcMonth: number,
utcDate: number utcDate: number,
): Boolean { ): Boolean {
const testDate = new Date(Date.UTC( const testDate = new Date(Date.UTC(utcYear, utcMonth, utcDate))
utcYear, utcMonth, utcDate
))
// We're not concerned if the year or month changes. We only want to make sure that the date is our target date // We're not concerned if the year or month changes. We only want to make sure that the date is our target date
if (testDate.getUTCDate() !== utcDate if (testDate.getUTCDate() !== utcDate) {
) {
return false return false
} }

View File

@@ -1,4 +1,4 @@
import { SplitMode, RecurrenceRule } from '@prisma/client' import { RecurrenceRule, SplitMode } from '@prisma/client'
import * as z from 'zod' import * as z from 'zod'
export const groupFormSchema = z export const groupFormSchema = z
@@ -105,9 +105,9 @@ export const expenseFormSchema = z
) )
.default([]), .default([]),
notes: z.string().optional(), notes: z.string().optional(),
recurrenceRule:z recurrenceRule: z
.enum<RecurrenceRule, [RecurrenceRule, ...RecurrenceRule[]]>( .enum<RecurrenceRule, [RecurrenceRule, ...RecurrenceRule[]]>(
Object.values(RecurrenceRule) as any Object.values(RecurrenceRule) as any,
) )
.default('NONE'), .default('NONE'),
}) })