diff --git a/package-lock.json b/package-lock.json
index 52b0e543..c1c46263 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54,7 +54,7 @@
"devDependencies": {
"@total-typescript/ts-reset": "^0.5.1",
"@types/content-disposition": "^0.5.8",
- "@types/node": "^20",
+ "@types/node": "^20.11.20",
"@types/pg": "^8.10.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
@@ -3111,10 +3111,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.8.9",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz",
- "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==",
- "license": "MIT",
+ "version": "20.11.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
+ "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
"dependencies": {
"undici-types": "~5.26.4"
}
diff --git a/package.json b/package.json
index 5706b1ef..d417e72d 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"next13-progressbar": "^1.1.1",
"openai": "^4.25.0",
"pg": "^8.11.3",
+ "prisma": "^5.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0",
@@ -53,13 +54,12 @@
"ts-pattern": "^5.0.6",
"uuid": "^9.0.1",
"vaul": "^0.8.0",
- "zod": "^3.22.4",
- "prisma": "^5.7.0"
+ "zod": "^3.22.4"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.5.1",
"@types/content-disposition": "^0.5.8",
- "@types/node": "^20",
+ "@types/node": "^20.11.20",
"@types/pg": "^8.10.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
diff --git a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
index 2514a77c..a566b3eb 100644
--- a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
+++ b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
@@ -5,6 +5,7 @@ import {
getCategories,
getExpense,
updateExpense,
+ archiveExpenseStateUpdate,
} from '@/lib/api'
import { getRuntimeFeatureFlags } from '@/lib/featureFlags'
import { expenseFormSchema } from '@/lib/schemas'
@@ -36,7 +37,13 @@ export default async function EditExpensePage({
async function deleteExpenseAction() {
'use server'
- await deleteExpense(expenseId)
+ await deleteExpense(groupId, expenseId)
+ redirect(`/groups/${groupId}`)
+ }
+
+ async function archiveExpenseStateUpdateAction(state: boolean) {
+ 'use server'
+ await archiveExpenseStateUpdate(expenseId, state)
redirect(`/groups/${groupId}`)
}
@@ -48,6 +55,7 @@ export default async function EditExpensePage({
categories={categories}
onSubmit={updateExpenseAction}
onDelete={deleteExpenseAction}
+ onArchive={archiveExpenseStateUpdateAction}
runtimeFeatureFlags={await getRuntimeFeatureFlags()}
/>
diff --git a/src/app/groups/[groupId]/expenses/expense-list.tsx b/src/app/groups/[groupId]/expenses/expense-list.tsx
index cc908af0..2e2767b5 100644
--- a/src/app/groups/[groupId]/expenses/expense-list.tsx
+++ b/src/app/groups/[groupId]/expenses/expense-list.tsx
@@ -129,6 +129,11 @@ export function ExpenseList({
+
+
+ {(expense.isArchive ? "Archived \u00B7 " :"")}
+
+
{expense.title}
diff --git a/src/app/groups/[groupId]/expenses/page.tsx b/src/app/groups/[groupId]/expenses/page.tsx
index 9dc3ab5e..f30359e0 100644
--- a/src/app/groups/[groupId]/expenses/page.tsx
+++ b/src/app/groups/[groupId]/expenses/page.tsx
@@ -100,7 +100,7 @@ export default async function GroupExpensesPage({
async function Expenses({ groupId }: { groupId: string }) {
const group = await cached.getGroup(groupId)
if (!group) notFound()
- const expenses = await getGroupExpenses(group.id)
+ const expenses = (await getGroupExpenses(group.id))// .filter(e => !e.isArchive)
return (
>>
onSubmit: (values: ExpenseFormValues) => Promise
onDelete?: () => Promise
+ onArchive?: (state: boolean) => Promise
runtimeFeatureFlags: RuntimeFeatureFlags
}
@@ -62,9 +63,11 @@ export function ExpenseForm({
categories,
onSubmit,
onDelete,
+ onArchive,
runtimeFeatureFlags,
}: Props) {
const isCreate = expense === undefined
+ const isArchived = expense?.isArchive
const searchParams = useSearchParams()
const getSelectedPayer = (field?: { value: string }) => {
if (isCreate && typeof window !== 'undefined') {
@@ -615,6 +618,28 @@ export function ExpenseForm({
{isCreate ? <>Create> : <>Save>}
+ {!isCreate && !isArchived && onArchive && (
+ onArchive(true)}
+ >
+
+ Archive
+
+ )}
+ {!isCreate && isArchived && onArchive && (
+ onArchive(false)}
+ >
+
+ Unarchive
+
+ )}
{!isCreate && onDelete && (
{
+ setTimeout(resolve, duration)
+ })
+}
export function randomId() {
return nanoid()
@@ -27,6 +35,134 @@ export async function createGroup(groupFormValues: GroupFormValues) {
})
}
+export async function acquireLockRecurringTransaction(groupId:string, expenseId:string, lockId: string): Promise {
+ const prisma = await getPrisma()
+ let retryCnt = 5
+ let getRecTxn:RecurringTransactions[] = []
+ while(--retryCnt) {
+ const existingLock = await prisma.recurringTransactions.findMany({
+ where: {
+ groupId,
+ expenseId
+ }
+ })
+ if (!existingLock.length) {
+ const query = `
+ INSERT INTO "RecurringTransactions"
+ ("groupId", "expenseId", "createNextAt", "lockId")
+ VALUES
+ ('${groupId}', '${expenseId}', 0, '${lockId}')
+ ON CONFLICT("groupId", "expenseId")
+ DO NOTHING;
+ `
+ await prisma.$queryRawUnsafe(query)
+ }
+ await prisma.recurringTransactions.updateMany({
+ where: {
+ groupId,
+ expenseId,
+ lockId: null
+ },
+ data: {
+ lockId
+ }
+ })
+ getRecTxn = await prisma.recurringTransactions.findMany({
+ where: {
+ expenseId: expenseId,
+ groupId,
+ lockId
+ }
+ })
+ const receivedLockId = getRecTxn?.[0]?.lockId
+ if (receivedLockId === lockId) break;
+ await sleep(500);
+ }
+ return getRecTxn
+}
+
+export async function releaseLockRecurringTransaction(groupId:string, expenseId:string, lockId: string): Promise{
+ const prisma = await getPrisma()
+ await prisma.recurringTransactions.updateMany({
+ where: {
+ groupId,
+ expenseId: expenseId,
+ lockId,
+ },
+ data: {
+ lockId: null
+ }
+ })
+}
+
+export async function createOrUpdateRecurringTransaction(
+ expenseFormValues: ExpenseFormValues,
+ groupId: string,
+ expenseId: string,
+ expenseRef: string|null = null
+): Promise {
+ let expenseDate = expenseFormValues.expenseDate
+ const prisma = await getPrisma()
+ if (!+expenseFormValues.recurringDays) {
+ await prisma.recurringTransactions.deleteMany({
+ where: {
+ groupId,
+ expenseId: expenseRef || expenseId
+ }
+ })
+ return Promise.resolve(undefined)
+ }
+ const lockId:string = randomId();
+ const receivedLockId = (await acquireLockRecurringTransaction(groupId, expenseRef || expenseId, lockId))?.[0]?.lockId
+ let recTxn;
+ if (!receivedLockId) return recTxn
+ const epochTime = Math.floor((new Date(expenseDate)).getTime() / 1000)
+ const nextAt: number = epochTime + (+expenseFormValues.recurringDays * 86400)
+ if (!isNaN(nextAt)) {
+ if (expenseRef) {
+ expenseDate = new Date(nextAt*1000)
+ await prisma.recurringTransactions.updateMany({
+ where: {
+ groupId,
+ expenseId: expenseRef,
+ lockId
+ },
+ data: {
+ createNextAt: nextAt,
+ expenseId
+ }
+ })
+ recTxn = (await prisma.recurringTransactions.findMany({
+ where: {
+ groupId,
+ expenseId
+ }
+ }))?.[0]
+ } else {
+ recTxn = await prisma.recurringTransactions.upsert({
+ where: {
+ groupId_expenseId: {
+ groupId,
+ expenseId: expenseId
+ },
+ lockId
+ },
+ update: {
+ createNextAt: nextAt,
+ },
+ create: {
+ groupId,
+ expenseId,
+ createNextAt: nextAt,
+ lockId
+ }
+ })
+ }
+ }
+ await releaseLockRecurringTransaction(groupId, expenseId, lockId)
+ return recTxn;
+}
+
export async function createExpense(
expenseFormValues: ExpenseFormValues,
groupId: string,
@@ -45,34 +181,11 @@ export async function createExpense(
const expenseId = randomId()
const prisma = await getPrisma()
let expenseDate = expenseFormValues.expenseDate
- if (+expenseFormValues.recurringDays) {
- const nextAt: number = Math.floor((new Date(expenseDate)).getTime() / 1000) + (+expenseFormValues.recurringDays * 86400)
- if (!isNaN(nextAt)) {
- if (expenseRef) {
- expenseDate = new Date(nextAt*1000)
- await prisma.recurringTransactions.updateMany({
- where: {
- groupId,
- expenseId: expenseRef
- },
- data: {
- createNextAt: nextAt,
- expenseId
- }
- })
- } else {
- await prisma.recurringTransactions.create({
- data: {
- groupId,
- expenseId,
- createNextAt: nextAt,
- lockId: null
- }
- })
- }
- }
- }
+ const recurringTxn = await createOrUpdateRecurringTransaction(expenseFormValues, groupId, expenseId, expenseRef)
+ if (recurringTxn) {
+ expenseDate = new Date(recurringTxn.createNextAt*1000)
+ }
return prisma.expense.create({
data: {
id: expenseId,
@@ -108,14 +221,35 @@ export async function createExpense(
})
}
-export async function deleteExpense(expenseId: string) {
+export async function deleteExpense(groupId: string, expenseId: string) {
const prisma = await getPrisma()
+ const lockId = randomId()
+ const recurringTxns = await prisma.recurringTransactions.findMany({
+ where: {groupId, expenseId}
+ })
+ const receivedRecurringTxn = () => acquireLockRecurringTransaction(groupId, expenseId, lockId)
+ if (recurringTxns.length && (await receivedRecurringTxn())?.[0]?.lockId) {
+ await prisma.recurringTransactions.deleteMany({
+ where: {groupId, expenseId}
+ })
+ }
+ await releaseLockRecurringTransaction(groupId, expenseId, lockId)
await prisma.expense.delete({
- where: { id: expenseId },
+ where: { id: expenseId, groupId },
include: { paidFor: true, paidBy: true },
})
}
+export async function archiveExpenseStateUpdate(expenseId: string, state: boolean) {
+ const prisma = await getPrisma()
+ await prisma.expense.updateMany({
+ where: { id: expenseId },
+ data: {
+ isArchive: state
+ }
+ })
+}
+
export async function getGroupExpensesParticipants(groupId: string) {
const expenses = await getGroupExpenses(groupId)
return Array.from(
@@ -159,7 +293,7 @@ export async function updateExpense(
if (!group.participants.some((p) => p.id === participant))
throw new Error(`Invalid participant ID: ${participant}`)
}
-
+ await createOrUpdateRecurringTransaction(expenseFormValues, groupId, expenseId, expenseId)
const prisma = await getPrisma()
return prisma.expense.update({
where: { id: expenseId },
@@ -217,6 +351,8 @@ export async function updateExpense(
id: doc.id,
})),
},
+ recurringDays: +expenseFormValues.recurringDays,
+ isArchive: expenseFormValues.isArchive
},
})
}
@@ -302,56 +438,27 @@ export async function getGroupExpenses(groupId: string) {
},
})
for (let i=0; i ({
- participant: paidFor.participant.id,
- shares: paidFor.shares,
- })),
- isReimbursement: relatedExpenses[i].isReimbursement,
- documents: relatedExpenses[i].documents,
- recurringDays: String(relatedExpenses[i].recurringDays),
- isArchive: relatedExpenses[i].isArchive,
- }, groupId, relatedExpenses[i].id);
- await prisma.recurringTransactions.updateMany({
- where: {
- groupId,
- expenseId: newExpense.id,
- },
- data: {
- lockId: null
- }
- })
- }
+ await createExpense({
+ expenseDate: relatedExpenses[i].expenseDate,
+ title: relatedExpenses[i].title,
+ category: relatedExpenses[i].category?.id || 0,
+ amount: relatedExpenses[i].amount,
+ paidBy: relatedExpenses[i].paidBy.id,
+ splitMode: relatedExpenses[i].splitMode,
+ paidFor: relatedExpenses[i].paidFor
+ .map((paidFor) => ({
+ participant: paidFor.participant.id,
+ shares: paidFor.shares,
+ })),
+ isReimbursement: relatedExpenses[i].isReimbursement,
+ documents: relatedExpenses[i].documents,
+ recurringDays: String(relatedExpenses[i].recurringDays),
+ isArchive: relatedExpenses[i].isArchive,
+ }, groupId, relatedExpenses[i].id);
}
allPendingRecurringTxns = await prisma.recurringTransactions.findMany({
where: {
diff --git a/src/lib/balances.ts b/src/lib/balances.ts
index aa8d26f4..b4f8d9d2 100644
--- a/src/lib/balances.ts
+++ b/src/lib/balances.ts
@@ -19,6 +19,7 @@ export function getBalances(
const balances: Balances = {}
for (const expense of expenses) {
+ if (expense.isArchive) continue;
const paidBy = expense.paidById
const paidFors = expense.paidFor