Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
84dd97c
feat: services for accounts, desposits and payouts
Paulijuz Mar 10, 2025
9716f0a
feat: payments
Paulijuz Mar 11, 2025
fdc90c0
feat: transaction paging
Paulijuz Mar 12, 2025
a545516
refactor: rename lib/money to lib/currency
Paulijuz Mar 13, 2025
25e94f8
feat: money stuff
Paulijuz Mar 13, 2025
bd01d6a
chore: update package lock
Paulijuz Apr 27, 2025
121e5d0
chore: update ledger methods to use schemas
Paulijuz Apr 27, 2025
32e5246
refactor: stripe event handling
Paulijuz Apr 30, 2025
4b4bb40
feat: ledger schema
Paulijuz Aug 10, 2025
88e947f
feat: payment service
Paulijuz Aug 10, 2025
0e86994
feat: balance calculation
Paulijuz Aug 11, 2025
7d7f43b
refactor: simplify relation between transaction and entries/payment/p…
Paulijuz Aug 15, 2025
5b82357
feat: transaction validation logic
Paulijuz Aug 15, 2025
3d6e24e
feat: more ledger transaction validation
Paulijuz Aug 20, 2025
68f5d32
feat: ledger operations
Paulijuz Aug 20, 2025
8d7f0b8
chore: clean up commented out code
Paulijuz Aug 20, 2025
9c86fca
feat: add manual transfer to schema (forgot to commit this)
Paulijuz Aug 20, 2025
c2a64b7
test: beginning of ledger tests
Paulijuz Aug 20, 2025
d4e9d01
feat: add reason for why transaction failed
Paulijuz Aug 21, 2025
177b41b
style: always use state in stead of status
Paulijuz Aug 21, 2025
ed2326b
fix: set default balance to zero
Paulijuz Aug 21, 2025
736eca2
test: calculate fees test
Paulijuz Aug 21, 2025
173ff8f
fix: calculate fees logic
Paulijuz Aug 21, 2025
308efd6
fix: disable mail sending during testing
Paulijuz Aug 21, 2025
841cf9c
fix: disable email rendering during testing
Paulijuz Aug 21, 2025
3d3dc3e
feat: promis util for tests
Paulijuz Aug 21, 2025
16c0f53
fiix: actually disable mail sending during testing
Paulijuz Aug 21, 2025
ec99d01
context: tests
Paulijuz Aug 21, 2025
dd8d8a8
test: ledger transaction tests
Paulijuz Aug 21, 2025
d0c57e4
fix: package-lock.json after rebase
Paulijuz Aug 21, 2025
3e1de12
feat: add ui from my other computer
Paulijuz Aug 22, 2025
86a673c
refactor: improve services to make UI easier
Paulijuz Aug 25, 2025
02a60ea
feat: ui and the like
Paulijuz Aug 29, 2025
12ef564
style: linting
Paulijuz Sep 21, 2025
29738ca
refactor: reorganize UI components (no work)
Paulijuz Sep 21, 2025
2a8c869
feat: more boring ui cleanup
Paulijuz Sep 21, 2025
4527f2a
feat: working transaction lists ++
Paulijuz Sep 22, 2025
c156d17
feat: ledger list ui polish
Paulijuz Sep 22, 2025
f832d58
style: linting, linting and more linting
Paulijuz Sep 23, 2025
6e280b9
feat: save payment method
Paulijuz Sep 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.default
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ LOG_MAX_FILES=365
NEXTAUTH_URL="http://localhost:80"
NEXTAUTH_SECRET=cake_is_love_cake_is_life

# Stripe
STRIPE_SECRET_KEY=sk_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Password Hashing and Encryption
PASSWORD_SALT_ROUNDS="12"
PASSWORD_ENCRYPTION_KEY="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" # Must be 256 bits (43 characters) long
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ services:
API_KEY_ENCRYPTION_KEY: ${API_KEY_ENCRYPTION_KEY}
FEIDE_CLIENT_ID: ${FEIDE_CLIENT_ID}
FEIDE_CLIENT_SECRET: ${FEIDE_CLIENT_SECRET}
MAIL_SERVER: ${MAIL_SERVER}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
MAIL_DOMAIN: ${MAIL_DOMAIN}
DOMAIN: ${DOMAIN}
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
Expand Down
5 changes: 5 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const config: Config = {
// This is needed becaue jest doesn't handle the this code is inside node_modules
'^@/prisma-dobbel-omega/(.*)$': '<rootDir>/node_modules/.prisma-dobbel-omega/$1',
},
globals: {
'ts-jest': {
useESM: true,
},
}
}

export default async function jestConfig() {
Expand Down
1,267 changes: 466 additions & 801 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@prisma/client": "^6.1.0",
"@react-email/components": "^0.0.31",
"@react-email/render": "^1.0.3",
"@stripe/react-stripe-js": "^3.3.0",
"@stripe/stripe-js": "^5.9.2",
"bcrypt": "^5.1.1",
"html5-qrcode": "^2.3.8",
"jsonwebtoken": "^9.0.2",
Expand All @@ -55,6 +57,7 @@
"sass": "^1.83.0",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"stripe": "^17.7.0",
"unified": "^11.0.5",
"uuid": "^10.0.0",
"winston": "^3.17.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@use "@/styles/ohma";

.LedgerAccountBalance {
display: grid;
grid-template-columns: auto 1fr auto;
grid-auto-flow: row;
grid-auto-columns: max-content;
column-gap: 2*ohma.$gap;
white-space: nowrap;
overflow: hidden;

// @include ohma.screenMobile {
// grid-template-columns: auto 1fr;
// }
// align-items: center;
// justify-content: space-between;
}

.amountRow {
display: contents;
font-size: ohma.$fonts-xxl;
}

.feesRow {
display: contents;
font-size: ohma.$fonts-l;
}

.total {
text-align: right;
// width: 100%; // ensures it stretches to the container
}
26 changes: 26 additions & 0 deletions src/app/_components/Ledger/Accounts/LedgerAccountBalance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import styles from './LedgerAccountBalance.module.scss'
import { unwrapActionReturn } from '@/app/redirectToErrorPage'
import { displayAmount } from '@/lib/currency/convert'
import { calculateLedgerAccountBalanceAction } from '@/services/ledger/ledgerAccount/actions'

type Props = {
ledgerAccountId: number,
showFees?: boolean,
}

export default async function LedgerAccountBalance({ ledgerAccountId: accountId, showFees }: Props) {
const balance = unwrapActionReturn(await calculateLedgerAccountBalanceAction({ id: accountId }))

return <div className={styles.LedgerAccountBalance}>
<div className={styles.amountRow}>
<div>Saldo</div>
<div className={styles.total}>{displayAmount(balance.amount)}</div>
<div className={styles.currencySymbol}>Muenter</div>
</div>
{showFees && <div className={styles.feesRow}>
<div>Avgifter</div>
<div className={styles.total}>{displayAmount(balance.fees)}</div>
<div className={styles.currencySymbol}>Muenter</div>
</div>}
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use "@/styles/ohma";

.ledgerAccountListTable {
@include ohma.table();
}
27 changes: 27 additions & 0 deletions src/app/_components/Ledger/Accounts/LedgerAccountList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client'

import styles from './LedgerAccountList.module.scss'
import EndlessScroll from '@/components/PagingWrappers/EndlessScroll'
import LedgerAccountPagingProvider, { LedgerAccountPagingContext } from '@/contexts/paging/LedgerAccountPaging'
import Link from 'next/link'

export default function LedgerAccountList() {
return <LedgerAccountPagingProvider startPage={{ page: 0, pageSize: 10 }} details={{ accountType: 'GROUP' }} serverRenderedData={[]}>
<table className={styles.ledgerAccountListTable}>
<thead>
<tr>
<th>Navn</th>
<th>Saldo</th>
</tr>
</thead>
<tbody>
<EndlessScroll pagingContext={LedgerAccountPagingContext} renderer={account =>
<tr key={account.id}>
<td><Link href={`accounts/${account.id}`}>{account.name}</Link></td>
<td>19.19 Klinguende Muente</td>
</tr>
}/>
</tbody>
</table>
</LedgerAccountPagingProvider>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@use "@/styles/ohma";

.ledgerAccountOverviewButtons {
margin-top: 3*ohma.$gap;
display: flex;
flex-direction: row;

.rightAligned {
margin-left: auto;
}
}
51 changes: 51 additions & 0 deletions src/app/_components/Ledger/Accounts/LedgerAccountOverviewCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import styles from './LedgerAccountOverview.module.scss'
import LedgerAccountBalance from './LedgerAccountBalance'
import Card from '@/components/UI/Card'
import DepositModal from '@/components/Ledger/Modals/DepositModal'
import PayoutModal from '@/components/Ledger/Modals/PayoutModal'
import Button from '@/components/UI/Button'
import { getUser } from '@/auth/getUser'
import { createStripeCustomerSessionAction } from '@/services/stripeCustomers/actions'

type Props = {
ledgerAccountId: number,
showFees?: boolean,
showDepositButton?: boolean,
showPayoutButton?: boolean,
showDeactivateButton?: boolean,
}

const getCustomerSessionClientSecret = async () => {
const { user } = await getUser()
if (!user) {
return undefined
}

const customerSessionResult = await createStripeCustomerSessionAction({ userId: user.id })
if (!customerSessionResult.success) {
return undefined
}

return customerSessionResult.data.customerSessionClientSecret
}

export default async function LedgerAccountOverview({
showFees,
ledgerAccountId,
showPayoutButton,
showDepositButton,
showDeactivateButton,
}: Props) {
const customerSessionClientSecret = showDepositButton
? await getCustomerSessionClientSecret()
: undefined

return <Card heading="Kontooversikt">
<LedgerAccountBalance ledgerAccountId={ledgerAccountId} showFees={showFees} />
<div className={styles.ledgerAccountOverviewButtons}>
{ showDepositButton && <DepositModal ledgerAccountId={ledgerAccountId} customerSessionClientSecret={customerSessionClientSecret} /> }
{ showPayoutButton && <PayoutModal ledgerAccountId={ledgerAccountId} /> }
{ showDeactivateButton && <Button color="red" className={styles.rightAligned}>Deaktiver</Button> }
</div>
</Card>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import BankCardModal from '@/components/Ledger/Modals/BankCardModal'
import Card from '@/components/UI/Card'
import { unwrapActionReturn } from '@/app/redirectToErrorPage'
import { readUserAction } from '@/services/users/actions'
import BooleanIndicator from '@/components/UI/BooleanIndicator'
import Link from 'next/link'
import { createStripeCustomerSessionAction } from '@/services/stripeCustomers/actions'

type Props = {
userId: number,
}

const getCustomerSessionClientSecret = async (userId: number) => {
const customerSessionResult = await createStripeCustomerSessionAction({ userId})
if (customerSessionResult.success) {
return customerSessionResult.data.customerSessionClientSecret
}
return undefined
}

export default async function LedgerAccountPaymentMethods({ userId }: Props) {
const user = unwrapActionReturn(await readUserAction({ id: userId }))
const customerSessionClientSecret = await getCustomerSessionClientSecret(userId)

const hasBankCard = false // TODO: Actually check with Stripe
const hasStudentCard = user.studentCard !== null

return <Card heading="Betalingsalternativer">
<h3>Bankkort <BooleanIndicator value={hasBankCard} /></h3>
<p>
Du kan lagre kortinformasjonen din for senere betalinger.
Kortinformasjonen lagres kun hos betalingsleverandΓΈren vΓ₯r, Stripe, og ikke pΓ₯ vΓ₯re tjenere.
</p>
<BankCardModal customerSessionClientSecret={customerSessionClientSecret} />
<h3>NTNU-kort <BooleanIndicator value={hasStudentCard} /></h3>
<p>Kortnummer: <strong>{hasStudentCard ? user.studentCard : 'ikke registrert'}</strong></p>
<p>For Γ₯ benytte Kiogeskabet pΓ₯ Lophtet mΓ₯ et NTNU-kort vΓ¦re registrert.</p>
<Link href={`/users/${user.username}/settings`}>GΓ₯ til siden for kortregistrering.</Link>
</Card>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Card from '@/components/UI/Card'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
import Link from 'next/link'

type Props = {
ledgerAccountId: number,
transactionsHref?: string,
}

export default function LedgerAccountTransactionSummary({ transactionsHref }: Props) {
return <Card heading="Transaksjoner">
<table>
<tbody>
<tr>
<td>En transaksjon</td>
</tr>
<tr>
<td>En annen transaksjon</td>
</tr>
</tbody>
</table>
{ transactionsHref && <Link href={transactionsHref}>Se alle transaksjoner <FontAwesomeIcon icon={faArrowRight} /></Link> }
</Card>
}
10 changes: 10 additions & 0 deletions src/app/_components/Ledger/Modals/BankCardModal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@use "@/styles/ohma";

.bankCardFormContainer {
width: 500px; // TODO: Is there a better way to do this?
margin: ohma.$gap * 3;
}

.paymentDetails {
min-height: 50px;
}
27 changes: 27 additions & 0 deletions src/app/_components/Ledger/Modals/BankCardModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client'

import styles from './BankCardModal.module.scss'
import PopUp from '@/app/_components/PopUp/PopUp'
import Button from '@/app/_components/UI/Button'
import StripePayment from '@/components/Stripe/StripePayment'
import StripeProvider from '@/components/Stripe/StripeProvider'

type PropTypes = {
customerSessionClientSecret?: string,
}

export default function BankCardModal({ customerSessionClientSecret }: PropTypes) {
return (
<PopUp
PopUpKey="BankAccountModal"
customShowButton={(open) => <Button onClick={open}>Legg til bankkort</Button>}
>
<h3>Legg til bankkort</h3>
<div className={styles.bankCardFormContainer}>
<StripeProvider mode="setup" customerSessionClientSecret={customerSessionClientSecret}>
<StripePayment />
</StripeProvider>
</div>
</PopUp>
)
}
7 changes: 7 additions & 0 deletions src/app/_components/Ledger/Modals/CheckoutModal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.checkoutFormContainer {
width: 500px; // TODO: Is there a better way to do this?
}

.paymentDetails {
min-height: 50px;
}
Loading
Loading