Skip to content

Commit 6e280b9

Browse files
committed
feat: save payment method
1 parent f832d58 commit 6e280b9

File tree

10 files changed

+251
-50
lines changed

10 files changed

+251
-50
lines changed

src/app/_components/Ledger/Accounts/LedgerAccountOverviewCard.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import Card from '@/components/UI/Card'
44
import DepositModal from '@/components/Ledger/Modals/DepositModal'
55
import PayoutModal from '@/components/Ledger/Modals/PayoutModal'
66
import Button from '@/components/UI/Button'
7+
import { getUser } from '@/auth/getUser'
8+
import { createStripeCustomerSessionAction } from '@/services/stripeCustomers/actions'
79

810
type Props = {
911
ledgerAccountId: number,
@@ -13,17 +15,35 @@ type Props = {
1315
showDeactivateButton?: boolean,
1416
}
1517

16-
export default function LedgerAccountOverview({
18+
const getCustomerSessionClientSecret = async () => {
19+
const { user } = await getUser()
20+
if (!user) {
21+
return undefined
22+
}
23+
24+
const customerSessionResult = await createStripeCustomerSessionAction({ userId: user.id })
25+
if (!customerSessionResult.success) {
26+
return undefined
27+
}
28+
29+
return customerSessionResult.data.customerSessionClientSecret
30+
}
31+
32+
export default async function LedgerAccountOverview({
1733
showFees,
1834
ledgerAccountId,
1935
showPayoutButton,
2036
showDepositButton,
2137
showDeactivateButton,
2238
}: Props) {
39+
const customerSessionClientSecret = showDepositButton
40+
? await getCustomerSessionClientSecret()
41+
: undefined
42+
2343
return <Card heading="Kontooversikt">
2444
<LedgerAccountBalance ledgerAccountId={ledgerAccountId} showFees={showFees} />
2545
<div className={styles.ledgerAccountOverviewButtons}>
26-
{ showDepositButton && <DepositModal ledgerAccountId={ledgerAccountId} /> }
46+
{ showDepositButton && <DepositModal ledgerAccountId={ledgerAccountId} customerSessionClientSecret={customerSessionClientSecret} /> }
2747
{ showPayoutButton && <PayoutModal ledgerAccountId={ledgerAccountId} /> }
2848
{ showDeactivateButton && <Button color="red" className={styles.rightAligned}>Deaktiver</Button> }
2949
</div>

src/app/_components/Ledger/Accounts/LedgerAccountPaymentMethodsCard.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,23 @@ import { unwrapActionReturn } from '@/app/redirectToErrorPage'
44
import { readUserAction } from '@/services/users/actions'
55
import BooleanIndicator from '@/components/UI/BooleanIndicator'
66
import Link from 'next/link'
7+
import { createStripeCustomerSessionAction } from '@/services/stripeCustomers/actions'
78

89
type Props = {
910
userId: number,
1011
}
1112

13+
const getCustomerSessionClientSecret = async (userId: number) => {
14+
const customerSessionResult = await createStripeCustomerSessionAction({ userId})
15+
if (customerSessionResult.success) {
16+
return customerSessionResult.data.customerSessionClientSecret
17+
}
18+
return undefined
19+
}
20+
1221
export default async function LedgerAccountPaymentMethods({ userId }: Props) {
1322
const user = unwrapActionReturn(await readUserAction({ id: userId }))
23+
const customerSessionClientSecret = await getCustomerSessionClientSecret(userId)
1424

1525
const hasBankCard = false // TODO: Actually check with Stripe
1626
const hasStudentCard = user.studentCard !== null
@@ -21,7 +31,7 @@ export default async function LedgerAccountPaymentMethods({ userId }: Props) {
2131
Du kan lagre kortinformasjonen din for senere betalinger.
2232
Kortinformasjonen lagres kun hos betalingsleverandøren vår, Stripe, og ikke på våre tjenere.
2333
</p>
24-
<BankCardModal userId={userId} />
34+
<BankCardModal customerSessionClientSecret={customerSessionClientSecret} />
2535
<h3>NTNU-kort <BooleanIndicator value={hasStudentCard} /></h3>
2636
<p>Kortnummer: <strong>{hasStudentCard ? user.studentCard : 'ikke registrert'}</strong></p>
2737
<p>For å benytte Kiogeskabet på Lophtet må et NTNU-kort være registrert.</p>

src/app/_components/Ledger/Modals/BankCardModal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ import StripePayment from '@/components/Stripe/StripePayment'
77
import StripeProvider from '@/components/Stripe/StripeProvider'
88

99
type PropTypes = {
10-
userId: number,
10+
customerSessionClientSecret?: string,
1111
}
1212

13-
export default function BankCardModal({ userId }: PropTypes) {
13+
export default function BankCardModal({ customerSessionClientSecret }: PropTypes) {
1414
return (
1515
<PopUp
1616
PopUpKey="BankAccountModal"
1717
customShowButton={(open) => <Button onClick={open}>Legg til bankkort</Button>}
1818
>
1919
<h3>Legg til bankkort</h3>
2020
<div className={styles.bankCardFormContainer}>
21-
<StripeProvider mode="setup">
21+
<StripeProvider mode="setup" customerSessionClientSecret={customerSessionClientSecret}>
2222
<StripePayment />
2323
</StripeProvider>
2424
</div>

src/app/_components/Ledger/Modals/DepositModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ const paymentProviderNames: Record<PaymentProvider, string> = {
2828

2929
type Props = {
3030
ledgerAccountId: number,
31+
customerSessionClientSecret?: string,
3132
}
3233

33-
export default function DepositModal({ ledgerAccountId }: Props) {
34+
export default function DepositModal({ ledgerAccountId, customerSessionClientSecret }: Props) {
3435
const [funds, setFunds] = useState(MINIMUM_PAYMENT_AMOUNT)
3536
const [manualFees, setManualFees] = useState(0)
3637
const [selectedProvider, setSelectedProvider] = useState<PaymentProvider>(defaultPaymentProvider)
@@ -111,7 +112,7 @@ export default function DepositModal({ ledgerAccountId }: Props) {
111112
</fieldset>
112113

113114
{selectedProvider === 'STRIPE' && (
114-
<StripeProvider mode="payment" amount={funds} >
115+
<StripeProvider mode="payment" amount={funds} customerSessionClientSecret={customerSessionClientSecret} >
115116
<StripePayment ref={stripePaymentRef} />
116117
</StripeProvider>
117118
)}

src/app/_components/Stripe/StripeProvider.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { MINIMUM_PAYMENT_AMOUNT } from '@/services/ledger/payments/config'
44
import { Elements } from '@stripe/react-stripe-js'
55
import { loadStripe } from '@stripe/stripe-js'
6-
import type { CustomerOptions } from '@stripe/stripe-js'
76
import type { ReactNode } from 'react'
87

98
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
@@ -16,16 +15,16 @@ type Props = {
1615
children?: ReactNode,
1716
mode: 'payment' | 'setup',
1817
amount?: number,
19-
customerOptions?: CustomerOptions,
18+
customerSessionClientSecret?: string,
2019
}
2120

22-
export default function StripeProvider({ children, mode, amount, customerOptions }: Props) {
21+
export default function StripeProvider({ children, mode, amount, customerSessionClientSecret }: Props) {
2322
return (
2423
<Elements stripe={stripe} options={{
2524
mode,
2625
currency: 'nok',
2726
amount: amount ? Math.max(MINIMUM_PAYMENT_AMOUNT, amount) : undefined,
28-
customerOptions,
27+
customerSessionClientSecret,
2928
}}>
3029
{children}
3130
</Elements>

src/prisma/schema/ledger.prisma

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
// Join table between groups and their ledger accounts
2+
model GroupLedgerAccount {
3+
id Int @id @default(autoincrement())
4+
// TODO: Finnish this
5+
6+
// group Group @relation(fields: [groupId], references: [id], onDelete: Cascade, onUpdate: Cascade)
7+
// groupId Int
8+
// ledgerAccount LedgerAccount @relation(fields: [ledgerAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade)
9+
// ledgerAccountId Int
10+
11+
createdAt DateTime @default(now())
12+
updatedAt DateTime @updatedAt
13+
14+
// Index on IDs for faster look up
15+
// @@index([groupId])
16+
// @@index([ledgerAccountId])
17+
}
18+
119
// In theory the type of a ledger accounts could be inferred from its relations,
220
// but to simplify logic an enum is used. In addition this also
321
// makes the ledger account type known even after the relation is lost.

src/prisma/schema/user.prisma

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,45 +17,66 @@ model User {
1717
allergies String?
1818
mobile String?
1919
emailVerified DateTime?
20-
createdAt DateTime @default(now())
21-
updatedAt DateTime @updatedAt // is also updated manually
22-
image Image? @relation(fields: [imageId], references: [id])
20+
image Image? @relation(fields: [imageId], references: [id]) // TODO: Rename to "profilePicture"?
2321
imageId Int?
2422
studentCard String? @unique
23+
24+
createdAt DateTime @default(now())
25+
updatedAt DateTime @updatedAt // is also updated manually
2526
27+
// Authentication info used for logging in.
2628
credentials Credentials?
2729
feideAccount FeideAccount?
2830
31+
// Memberships to groups (committees, interest groups, classes, etc...).
2932
memberships Membership[]
3033
34+
// Lockers used by the user.
3135
LockerReservation LockerReservation[]
36+
37+
// Omega quotes posted by the user.
3238
omegaQuote OmegaQuote[]
3339
40+
// Which ledger account (i.e. internal bank account) and
41+
// stripe customer this user is associated with.
3442
ledgerAccount LedgerAccount?
43+
stripeCustomer StripeCustomer?
3544
45+
// What notifications the user whiches to received and
46+
// which mailing lists the user is on.
3647
notificationSubscriptions NotificationSubscription[]
3748
mailingLists MailingListUser[]
3849
50+
// Which admissions (a.k.a. "opptak") the user has taken
51+
// and which admissions thay have registered for others.
3952
admissionTrials AdmissionTrial[] @relation(name: "user")
4053
registeredAdmissionTrial AdmissionTrial[] @relation(name: "registeredBy")
4154
55+
// The user's applications to committees.
4256
Application Application[]
4357
58+
// Which events the user has registered for
59+
// and which events they have created.
4460
EventRegistration EventRegistration[]
4561
Event Event[]
4662
63+
// Which dots (a.k.a. "prikker") the user has received and given.
4764
dots DotWrapper[] @relation(name: "dot_user")
4865
dotsAccused DotWrapper[] @relation(name: "dot_accuser")
4966
67+
// The queue used to determine who is registering cards at Kiogeskabet.
5068
registerStudentCardQueue RegisterStudentCardQueue[]
5169
70+
// Which cabin bookings the user has made.
5271
cabinBooking Booking[] @relation()
5372
5473
// We need to explicitly mark the combination of 'id', 'username' and 'email' as
5574
// unique to make the relation to 'Credentials' work.
5675
@@unique([id, username, email])
5776
}
5877

78+
// This model primaraly exists to keep the password hash separate from the user table.
79+
// This is to reduce the risk of leaking the password hashes..
5980
model Credentials {
6081
user User @relation(fields: [userId, username, email], references: [id, username, email], onDelete: Cascade, onUpdate: Cascade)
6182
userId Int @unique
@@ -69,22 +90,36 @@ model Credentials {
6990
@@unique([userId, username, email])
7091
}
7192

93+
// Associates each user with their Feide account.
7294
model FeideAccount {
7395
id String @id
7496
accessToken String @db.Text
7597
email String @unique
7698
expiresAt DateTime
7799
issuedAt DateTime
78-
userId Int @unique
79100
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
101+
userId Int @unique
102+
}
103+
104+
// Associates each user with their Stripe customer id.
105+
model StripeCustomer {
106+
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
107+
userId Int @id
108+
customerId String @unique
109+
110+
createdAt DateTime @default(now())
111+
updatedAt DateTime @updatedAt
80112
}
81113

114+
// When a user wants to register their student card, they are put in this queue.
115+
// Then they must scan their card with the card reader at Kiogeskabet.
82116
model RegisterStudentCardQueue {
83117
userId Int @id
84118
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
85119
expiry DateTime
86120
}
87121

122+
// TODO: Someone should add a comment for ContactDetails because I have noe idea what it is for. Is it for anonymous users?
88123
model ContactDetails {
89124
id Int @id @default(autoincrement())
90125
name String

src/services/ledger/stripeCustomer/methods.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use server'
2+
3+
import { action } from "../action"
4+
import { StripeCustomerMethods } from "./methods"
5+
6+
export const createStripeCustomerSessionAction = action(StripeCustomerMethods.createSession)

0 commit comments

Comments
 (0)