|
|
|
|
@@ -1,6 +1,11 @@
|
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
|
|
|
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'
|
|
|
|
|
|
|
|
|
|
export function randomId() {
|
|
|
|
|
@@ -50,11 +55,12 @@ export async function createExpense(
|
|
|
|
|
data: expenseFormValues.title,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const isCreateRecurrence = expenseFormValues.recurrenceRule !== RecurrenceRule.NONE
|
|
|
|
|
const isCreateRecurrence =
|
|
|
|
|
expenseFormValues.recurrenceRule !== RecurrenceRule.NONE
|
|
|
|
|
const recurringExpenseLinkPayload = createPayloadForNewRecurringExpenseLink(
|
|
|
|
|
expenseFormValues.recurrenceRule as RecurrenceRule,
|
|
|
|
|
expenseFormValues.expenseDate,
|
|
|
|
|
groupId
|
|
|
|
|
groupId,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return prisma.expense.create({
|
|
|
|
|
@@ -71,11 +77,9 @@ export async function createExpense(
|
|
|
|
|
recurringExpenseLink: {
|
|
|
|
|
...(isCreateRecurrence
|
|
|
|
|
? {
|
|
|
|
|
create: recurringExpenseLinkPayload
|
|
|
|
|
create: recurringExpenseLinkPayload,
|
|
|
|
|
}
|
|
|
|
|
: {}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
: {}),
|
|
|
|
|
},
|
|
|
|
|
paidFor: {
|
|
|
|
|
createMany: {
|
|
|
|
|
@@ -175,7 +179,8 @@ export async function updateExpense(
|
|
|
|
|
// Delete the existing RecurrenceExpenseLink only if it has not been acted upon yet
|
|
|
|
|
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
|
|
|
|
|
existingExpense.recurringExpenseLink?.nextExpenseCreatedAt === null
|
|
|
|
|
const isCreateRecurrenceExpenseLink =
|
|
|
|
|
@@ -187,12 +192,12 @@ export async function updateExpense(
|
|
|
|
|
const newRecurringExpenseLink = createPayloadForNewRecurringExpenseLink(
|
|
|
|
|
expenseFormValues.recurrenceRule as RecurrenceRule,
|
|
|
|
|
expenseFormValues.expenseDate,
|
|
|
|
|
groupId
|
|
|
|
|
groupId,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const updatedRecurrenceExpenseLinkNextExpenseDate = calculateNextDate(
|
|
|
|
|
expenseFormValues.recurrenceRule as RecurrenceRule,
|
|
|
|
|
existingExpense.expenseDate
|
|
|
|
|
existingExpense.expenseDate,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return prisma.expense.update({
|
|
|
|
|
@@ -238,18 +243,16 @@ export async function updateExpense(
|
|
|
|
|
recurringExpenseLink: {
|
|
|
|
|
...(isCreateRecurrenceExpenseLink
|
|
|
|
|
? {
|
|
|
|
|
create: newRecurringExpenseLink
|
|
|
|
|
create: newRecurringExpenseLink,
|
|
|
|
|
}
|
|
|
|
|
: {}
|
|
|
|
|
),
|
|
|
|
|
: {}),
|
|
|
|
|
...(isUpdateRecurrenceExpenseLink
|
|
|
|
|
? {
|
|
|
|
|
update: {
|
|
|
|
|
nextExpenseDate: updatedRecurrenceExpenseLinkNextExpenseDate
|
|
|
|
|
nextExpenseDate: updatedRecurrenceExpenseLinkNextExpenseDate,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
: {}
|
|
|
|
|
),
|
|
|
|
|
: {}),
|
|
|
|
|
delete: isDeleteRecurrenceExpenseLink,
|
|
|
|
|
},
|
|
|
|
|
isReimbursement: expenseFormValues.isReimbursement,
|
|
|
|
|
@@ -371,7 +374,13 @@ export async function getGroupExpenseCount(groupId: string) {
|
|
|
|
|
export async function getExpense(groupId: string, expenseId: string) {
|
|
|
|
|
return prisma.expense.findUnique({
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -421,22 +430,25 @@ export async function logActivity(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createRecurringExpenses() {
|
|
|
|
|
const localDate = new Date(); // Current local date
|
|
|
|
|
const utcDateFromLocal = new Date(Date.UTC(
|
|
|
|
|
const localDate = new Date() // Current local date
|
|
|
|
|
const utcDateFromLocal = new Date(
|
|
|
|
|
Date.UTC(
|
|
|
|
|
localDate.getUTCFullYear(),
|
|
|
|
|
localDate.getUTCMonth(),
|
|
|
|
|
localDate.getUTCDate(),
|
|
|
|
|
// More precision beyond date is required to ensure that recurring Expenses are created within <most precises unit> of when expected
|
|
|
|
|
localDate.getUTCHours(),
|
|
|
|
|
localDate.getUTCMinutes(),
|
|
|
|
|
));
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const recurringExpenseLinksWithExpensesToCreate = await prisma.recurringExpenseLink.findMany({
|
|
|
|
|
const recurringExpenseLinksWithExpensesToCreate =
|
|
|
|
|
await prisma.recurringExpenseLink.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
nextExpenseCreatedAt: null,
|
|
|
|
|
nextExpenseDate: {
|
|
|
|
|
lte: utcDateFromLocal
|
|
|
|
|
}
|
|
|
|
|
lte: utcDateFromLocal,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
currentFrameExpense: {
|
|
|
|
|
@@ -444,10 +456,10 @@ async function createRecurringExpenses(){
|
|
|
|
|
paidBy: true,
|
|
|
|
|
paidFor: true,
|
|
|
|
|
category: true,
|
|
|
|
|
documents: true
|
|
|
|
|
documents: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for (const recurringExpenseLink of recurringExpenseLinksWithExpensesToCreate) {
|
|
|
|
|
@@ -462,17 +474,21 @@ async function createRecurringExpenses(){
|
|
|
|
|
|
|
|
|
|
const newRecurringExpenseNextExpenseDate = calculateNextDate(
|
|
|
|
|
currentExpenseRecord.recurrenceRule as RecurrenceRule,
|
|
|
|
|
newExpenseDate
|
|
|
|
|
newExpenseDate,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
category, paidBy, paidFor, documents,
|
|
|
|
|
category,
|
|
|
|
|
paidBy,
|
|
|
|
|
paidFor,
|
|
|
|
|
documents,
|
|
|
|
|
...destructeredCurrentExpenseRecord
|
|
|
|
|
} = currentExpenseRecord
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
const newExpense = await prisma.$transaction(async (transaction) => {
|
|
|
|
|
const newExpense = await prisma
|
|
|
|
|
.$transaction(async (transaction) => {
|
|
|
|
|
const newExpense = await transaction.expense.create({
|
|
|
|
|
data: {
|
|
|
|
|
...destructeredCurrentExpenseRecord,
|
|
|
|
|
@@ -487,9 +503,11 @@ async function createRecurringExpenses(){
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
documents: {
|
|
|
|
|
connect: currentExpenseRecord.documents.map((documentRecord) => ({
|
|
|
|
|
id: documentRecord.id
|
|
|
|
|
})),
|
|
|
|
|
connect: currentExpenseRecord.documents.map(
|
|
|
|
|
(documentRecord) => ({
|
|
|
|
|
id: documentRecord.id,
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
id: newExpenseId,
|
|
|
|
|
expenseDate: newExpenseDate,
|
|
|
|
|
@@ -497,17 +515,17 @@ async function createRecurringExpenses(){
|
|
|
|
|
create: {
|
|
|
|
|
groupId: currentExpenseRecord.groupId,
|
|
|
|
|
id: newRecurringExpenseLinkId,
|
|
|
|
|
nextExpenseDate: newRecurringExpenseNextExpenseDate
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
paidBy: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Mark the RecurringExpenseLink as being "completed" since the new Expense was created
|
|
|
|
|
@@ -518,13 +536,17 @@ async function createRecurringExpenses(){
|
|
|
|
|
nextExpenseCreatedAt: null,
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
nextExpenseCreatedAt: newExpense.createdAt
|
|
|
|
|
nextExpenseCreatedAt: newExpense.createdAt,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return newExpense
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
console.error("Failed to created recurringExpense for expenseId: %s", currentExpenseRecord.id)
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
console.error(
|
|
|
|
|
'Failed to created recurringExpense for expenseId: %s',
|
|
|
|
|
currentExpenseRecord.id,
|
|
|
|
|
)
|
|
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@@ -546,14 +568,14 @@ function createPayloadForNewRecurringExpenseLink(
|
|
|
|
|
): RecurringExpenseLink {
|
|
|
|
|
const nextExpenseDate = calculateNextDate(
|
|
|
|
|
recurrenceRule,
|
|
|
|
|
priorDateToNextRecurrence
|
|
|
|
|
priorDateToNextRecurrence,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const recurringExpenseLinkId = randomId()
|
|
|
|
|
const recurringExpenseLinkPayload = {
|
|
|
|
|
id: recurringExpenseLinkId,
|
|
|
|
|
groupId: groupId,
|
|
|
|
|
nextExpenseDate: nextExpenseDate
|
|
|
|
|
nextExpenseDate: nextExpenseDate,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return recurringExpenseLinkPayload as RecurringExpenseLink
|
|
|
|
|
@@ -567,7 +589,7 @@ function createPayloadForNewRecurringExpenseLink(
|
|
|
|
|
// will be created for Feb 28th, March 28, etc. until it is cancelled or fixed
|
|
|
|
|
function calculateNextDate(
|
|
|
|
|
recurrenceRule: RecurrenceRule,
|
|
|
|
|
priorDateToNextRecurrence: Date
|
|
|
|
|
priorDateToNextRecurrence: Date,
|
|
|
|
|
): Date {
|
|
|
|
|
const nextDate = new Date(priorDateToNextRecurrence)
|
|
|
|
|
switch (recurrenceRule) {
|
|
|
|
|
@@ -596,15 +618,12 @@ function calculateNextDate(
|
|
|
|
|
function isDateInNextMonth(
|
|
|
|
|
utcYear: number,
|
|
|
|
|
utcMonth: number,
|
|
|
|
|
utcDate: number
|
|
|
|
|
utcDate: number,
|
|
|
|
|
): Boolean {
|
|
|
|
|
const testDate = new Date(Date.UTC(
|
|
|
|
|
utcYear, utcMonth, utcDate
|
|
|
|
|
))
|
|
|
|
|
const testDate = new Date(Date.UTC(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
|
|
|
|
|
if (testDate.getUTCDate() !== utcDate
|
|
|
|
|
) {
|
|
|
|
|
if (testDate.getUTCDate() !== utcDate) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|