Skip to content

Comments

Feat/ledger#444

Open
Paulijuz wants to merge 70 commits intomainfrom
feat/ledger
Open

Feat/ledger#444
Paulijuz wants to merge 70 commits intomainfrom
feat/ledger

Conversation

@Paulijuz
Copy link
Member

@Paulijuz Paulijuz commented Sep 23, 2025

After more than of year of waiting the ledger (money) system is finally here!* 💵 💸 💰

This includes:
✅ A flexible double entry ledger system for tracking transaction (deposits, payouts, purchases, etc.)
✅ Simple logic to calculate balances (goodbye ginormous sql query)
✅ Uniform ledger account interface between groups and users (no more source and drain accounts)
✅ Payments with Stripe (integrated with webhooks like before)
✅ Card detail saving with Stripe (makes use of new-ish CustomerSessions API for simpler integration)**

⚠️ *This PR is marked as draft until #443 is merged as it builts on top of it. I also have to tie up a few loose ends regarding permissions, although I think it might be best to do that in another PR.

⚠️ **I have not tested this fully yet so a few changes might still come.

@Paulijuz Paulijuz marked this pull request as ready for review January 19, 2026 19:35
@Paulijuz
Copy link
Member Author

As this PR is large enough as it is i will add integration to the event system in the branch feat/event-payments.

@Paulijuz
Copy link
Member Author

Paulijuz commented Feb 9, 2026

More merge conflicts... 😩

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive ledger (money/accounting) system for the application, introducing double-entry bookkeeping, Stripe payment integration, and ledger accounts for users and groups. The implementation builds on top of PR #443 which simplified service method interfaces.

Changes:

  • Added complete ledger schema with accounts, transactions, entries, and payment models supporting both Stripe and manual payments
  • Implemented double-entry accounting logic with automatic fee calculations and balance tracking
  • Integrated Stripe for payment processing, including webhook handling and saved payment method management
  • Created UI components for account management, deposits, payouts, and transaction history
  • Added test infrastructure with utilities for testing ledger operations

Reviewed changes

Copilot reviewed 101 out of 103 changed files in this pull request and generated 22 comments.

Show a summary per file
File Description
src/prisma/schema/ledger.prisma Core ledger database schema defining accounts, transactions, entries, and payments
src/prisma/schema/user.prisma Added ledger account and Stripe customer relations to users
src/prisma/schema/group.prisma Added ledger account relation to groups
src/services/ledger/accounts/operations.ts Ledger account CRUD operations and balance calculations
src/services/ledger/transactions/operations.ts Transaction creation, state management, and advancement logic
src/services/ledger/transactions/calculateFees.ts Fee calculation formulas for debit and credit entries
src/services/ledger/transactions/determineTransactionState.ts State machine for transaction lifecycle management
src/services/ledger/payments/operations.ts Payment creation and initiation for Stripe and manual payments
src/services/ledger/payments/stripeWebhookCallback.ts Webhook handler for Stripe payment events
src/services/ledger/movements/operations.ts High-level deposit and payout orchestration
src/services/stripeCustomers/operations.ts Stripe customer management and saved payment methods
src/lib/currency/convert.ts Currency conversion utilities replacing old money module
src/app/api/stripe-events/route.ts API route for receiving Stripe webhooks
src/app/_components/Ledger/* UI components for ledger account and transaction management
src/app/_components/Stripe/* Stripe integration components for payments
tests/services/ledger/* Test files for ledger functionality
tests/utils.ts Test utility for handling promise settlement
package.json Added Stripe SDK dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* The lifecycle of the transaction is automatically handled by the system.
*/
create: defineOperation({
authorizer: () => RequireNothing.staticFields({}).dynamicFields({}), // TODO,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ledger transaction creation also uses RequireNothing authorization. Anyone can create ledger transactions, which is a critical security issue for a financial system.

Copilot uses AI. Check for mistakes.
},
})
} else {
console.error(`Stripe payment is not part of a ledger transaction: ${stripePayment.payment.id}`)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webhook handler uses console.error instead of the logger that's imported and used elsewhere in the file. This inconsistency should be fixed to use the logger for consistent logging behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +20
const sign = convertedAmount > 0 ? '+' : '-'

return `${withSign && convertedAmount !== 0 ? sign : ''}${amountString} ${currencySymbol}`
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sign logic in displayAmount is incorrect. When convertedAmount is negative, the sign is set to '-', but the amountString already includes the negative sign from toFixed(2). This will result in incorrect output for negative amounts (e.g., "--5.00" instead of "-5.00" when withSign is true). The sign should be based on whether to show a positive sign, and the absolute value should be used for the amountString when withSign is true.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +103
// If searching by userId/groupId we want to create the account if it doesn't exist.
// TODO: Is this something we want?

const account = await prisma.ledgerAccount.findUnique({
where: {
userId: params.userId,
groupId: params.groupId,
},
})

if (account) return account

return ledgerAccountOperations.create({ session, data: params })
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The read operation automatically creates a ledger account if it doesn't exist when searching by userId/groupId (line 103). This behavior is questionable and noted by the TODO comment. Auto-creation during a read operation violates the principle of least surprise and could lead to unintended side effects. Consider separating this into explicit "readOrCreate" and "read" operations, or at least require an explicit flag to enable auto-creation.

Copilot uses AI. Check for mistakes.
customerId: customer.id,
},
update: {
// Dont update anything. Let the first created account win.
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in the comment: "Dont" should be "Don't".

Copilot uses AI. Check for mistakes.
* @returns The created account.
*/
create: defineOperation({
authorizer: () => RequireNothing.staticFields({}).dynamicFields({}), // TODO: Add proper auther
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in multiple TODO comments: "auther" should be "authorizer". Also, all ledger account operations are using RequireNothing authorization, which means anyone can create, read, update ledger accounts and calculate balances. This is a critical security issue that should be addressed before merging. The PR description mentions tying up loose ends regarding permissions in another PR, but given the financial nature of this feature, authorization should be implemented before merging.

Copilot uses AI. Check for mistakes.
Comment on lines +217 to +229
deleteSavedPaymentMethod: defineOperation({
authorizer: () => RequireNothing.staticFields({}).dynamicFields({}), // TODO: This should probably be authed?
paramsSchema: z.object({
paymentMethodId: z.string(),
}),
operation: async ({ params: { paymentMethodId } }) => {
await stripe.paymentMethods.detach(paymentMethodId)

return {
success: true
}
}
}),
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deleteSavedPaymentMethod operation has no authorization, allowing anyone to delete any payment method by ID. This is a critical security vulnerability. An attacker could potentially delete other users' saved payment methods. This operation must verify that the paymentMethodId belongs to the authenticated user before deleting it.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +9
test('balance', async () => {
})
})
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ledger accounts test file contains an empty test. This should either be implemented or removed before merging, as empty tests provide no value and can mask missing test coverage.

Copilot uses AI. Check for mistakes.
try {
return await stripeWebhookCallback(event)
} catch {
return new Response('A server-side error occured.', { status: 500 })
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in the error message: "occured" should be "occurred".

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +22
try {
return await stripeWebhookCallback(event)
} catch {
return new Response('A server-side error occured.', { status: 500 })
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block at line 20 swallows all error information, making debugging difficult. The error should be logged before returning the generic error response. Consider using the logger that's available in the codebase to log the caught error.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant