From d25b894f38a2dae6096fdec928fc17705ebc7225 Mon Sep 17 00:00:00 2001
From: Phan Huy Tran
Date: Thu, 13 Mar 2025 15:02:32 +0100
Subject: [PATCH] feat: Save balance after successful payment
---
backend/requests/webhook.http | 1 +
.../casino/deposit/DepositController.java | 42 +++++++++++-
.../casino/deposit/TransactionEntity.java | 29 +++++++++
.../casino/deposit/TransactionRepository.java | 14 ++++
.../casino/deposit/TransactionService.java | 62 ++++++++++++++++++
.../casino/deposit/TransactionStatus.java | 6 ++
.../casino/deposit/WebhookController.java | 64 +++++++++++++++++++
.../security/KeycloakSecurityConfig.java | 17 +++--
.../java/de/szut/casino/user/UserEntity.java | 11 ++++
.../src/main/resources/application.properties | 3 +-
frontend/src/environments/environment.ts | 2 +-
11 files changed, 242 insertions(+), 9 deletions(-)
create mode 100644 backend/requests/webhook.http
create mode 100644 backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java
create mode 100644 backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java
create mode 100644 backend/src/main/java/de/szut/casino/deposit/TransactionService.java
create mode 100644 backend/src/main/java/de/szut/casino/deposit/TransactionStatus.java
create mode 100644 backend/src/main/java/de/szut/casino/deposit/WebhookController.java
diff --git a/backend/requests/webhook.http b/backend/requests/webhook.http
new file mode 100644
index 0000000..8fa7f26
--- /dev/null
+++ b/backend/requests/webhook.http
@@ -0,0 +1 @@
+POST localhost:8080/webhook
diff --git a/backend/src/main/java/de/szut/casino/deposit/DepositController.java b/backend/src/main/java/de/szut/casino/deposit/DepositController.java
index 6bc0fbc..b2918cf 100644
--- a/backend/src/main/java/de/szut/casino/deposit/DepositController.java
+++ b/backend/src/main/java/de/szut/casino/deposit/DepositController.java
@@ -6,10 +6,20 @@ import com.stripe.model.checkout.Session;
import com.stripe.param.checkout.SessionCreateParams;
import de.szut.casino.deposit.dto.AmountDto;
import de.szut.casino.deposit.dto.SessionIdDto;
+import de.szut.casino.user.UserEntity;
+import de.szut.casino.user.UserRepository;
+import de.szut.casino.user.UserService;
+import de.szut.casino.user.dto.KeycloakUserDto;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Optional;
@RestController
public class DepositController {
@@ -20,10 +30,26 @@ public class DepositController {
@Value("${app.frontend-host}")
private String frontendHost;
+ private final TransactionService transactionService;
+
+ private final RestTemplate restTemplate;
+
+ private final UserRepository userRepository;
+
+
+ public DepositController(TransactionService transactionService, RestTemplate restTemplate, UserRepository userRepository) {
+ this.transactionService = transactionService;
+ this.restTemplate = restTemplate;
+ this.userRepository = userRepository;
+ }
+
@PostMapping("/deposit/checkout")
- public ResponseEntity checkout(@RequestBody @Valid AmountDto amountDto) throws StripeException {
+ public ResponseEntity checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
Stripe.apiKey = stripeKey;
+ KeycloakUserDto userData = getKeycloakUserInfo(token);
+ Optional optionalUserEntity = this.userRepository.findOneByKeycloakId(userData.getSub());
+
SessionCreateParams params = SessionCreateParams.builder()
.addLineItem(SessionCreateParams.LineItem.builder()
.setAmount((long) amountDto.getAmount() * 100)
@@ -38,7 +64,21 @@ public class DepositController {
Session session = Session.create(params);
+ if (optionalUserEntity.isEmpty()) {
+ throw new RuntimeException("User doesnt exist");
+ }
+
+ transactionService.createTransaction(optionalUserEntity.get(), session.getId(), amountDto.getAmount());
+
return ResponseEntity.ok(new SessionIdDto(session.getId()));
}
+
+ private KeycloakUserDto getKeycloakUserInfo(String token) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Authorization", token);
+ ResponseEntity response = this.restTemplate.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
+
+ return response.getBody();
+ }
}
diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java b/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java
new file mode 100644
index 0000000..e3fc412
--- /dev/null
+++ b/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java
@@ -0,0 +1,29 @@
+package de.szut.casino.deposit;
+
+import de.szut.casino.user.UserEntity;
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Setter
+@Getter
+@Entity
+public class TransactionEntity {
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @ManyToOne
+ @JoinColumn(name = "user_id", nullable = false)
+ private UserEntity user;
+
+ @Column(unique = true)
+ private String sessionId = null;
+
+ private double amount = 0;
+
+ @Enumerated(EnumType.STRING)
+ private TransactionStatus status = TransactionStatus.PROCESSING;
+}
diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java b/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java
new file mode 100644
index 0000000..94b9b6b
--- /dev/null
+++ b/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java
@@ -0,0 +1,14 @@
+package de.szut.casino.deposit;
+
+import de.szut.casino.user.UserEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+public interface TransactionRepository extends JpaRepository {
+ @Query("SELECT t FROM TransactionEntity t WHERE t.sessionId = ?1")
+ Optional findOneBySessionID(String sessionId);
+}
diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionService.java b/backend/src/main/java/de/szut/casino/deposit/TransactionService.java
new file mode 100644
index 0000000..24d4382
--- /dev/null
+++ b/backend/src/main/java/de/szut/casino/deposit/TransactionService.java
@@ -0,0 +1,62 @@
+package de.szut.casino.deposit;
+
+import com.stripe.exception.StripeException;
+import com.stripe.model.checkout.Session;
+import com.stripe.param.checkout.SessionRetrieveParams;
+import de.szut.casino.user.UserEntity;
+import de.szut.casino.user.UserRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@Service
+public class TransactionService {
+ private final TransactionRepository transactionRepository;
+ private final UserRepository userRepository;
+
+ public TransactionService(TransactionRepository transactionRepository, UserRepository userRepository) {
+ this.transactionRepository = transactionRepository;
+ this.userRepository = userRepository;
+ }
+
+ public void createTransaction(
+ UserEntity user,
+ String sessionID,
+ Double amount
+ ) {
+ TransactionEntity transaction = new TransactionEntity();
+
+ transaction.setUser(user);
+ transaction.setSessionId(sessionID);
+ transaction.setAmount(amount);
+
+ transactionRepository.save(transaction);
+ }
+
+ public void fulfillCheckout(String sessionID) throws StripeException {
+ SessionRetrieveParams params = SessionRetrieveParams.builder()
+ .addExpand("line_items")
+ .build();
+ Session checkoutSession = Session.retrieve(sessionID, params, null);
+
+ if (!Objects.equals(checkoutSession.getPaymentStatus(), "paid")) {
+ return;
+ }
+
+ Optional optionalTransaction = transactionRepository.findOneBySessionID(sessionID);
+ if (optionalTransaction.isEmpty()) {
+ throw new RuntimeException("Transaction not found");
+ }
+
+ TransactionEntity transaction = optionalTransaction.get();
+ transaction.setStatus(TransactionStatus.SUCCEEDED);
+
+ UserEntity user = transaction.getUser();
+ user.addBalance(checkoutSession.getAmountTotal());
+
+ userRepository.save(user);
+ transactionRepository.save(transaction);
+ }
+
+}
diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionStatus.java b/backend/src/main/java/de/szut/casino/deposit/TransactionStatus.java
new file mode 100644
index 0000000..93ae142
--- /dev/null
+++ b/backend/src/main/java/de/szut/casino/deposit/TransactionStatus.java
@@ -0,0 +1,6 @@
+package de.szut.casino.deposit;
+
+public enum TransactionStatus {
+ PROCESSING,
+ SUCCEEDED,
+}
diff --git a/backend/src/main/java/de/szut/casino/deposit/WebhookController.java b/backend/src/main/java/de/szut/casino/deposit/WebhookController.java
new file mode 100644
index 0000000..976df5b
--- /dev/null
+++ b/backend/src/main/java/de/szut/casino/deposit/WebhookController.java
@@ -0,0 +1,64 @@
+package de.szut.casino.deposit;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+import com.stripe.Stripe;
+import com.stripe.exception.SignatureVerificationException;
+import com.stripe.exception.StripeException;
+import com.stripe.model.*;
+import com.stripe.model.checkout.Session;
+import com.stripe.net.Webhook;
+import com.stripe.param.checkout.SessionRetrieveParams;
+import de.szut.casino.user.UserEntity;
+import de.szut.casino.user.UserRepository;
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+import java.util.Optional;
+
+@RestController
+public class WebhookController {
+ private static final Logger logger = LoggerFactory.getLogger(WebhookController.class);
+ @Value("${stripe.secret.key}")
+ private String stripeSecretKey;
+
+ @Value("${stripe.webhook.secret}")
+ private String webhookSecret;
+
+ private final TransactionService transactionService;
+
+ public WebhookController(TransactionService transactionService) {
+ this.transactionService = transactionService;
+ }
+
+ @PostConstruct
+ public void init() {
+ Stripe.apiKey = stripeSecretKey;
+ }
+
+ @PostMapping("/webhook")
+ public ResponseEntity webhook(@RequestBody String payload, @RequestHeader("Stripe-Signature") String sigHeader) throws StripeException {
+ Event event = Webhook.constructEvent(payload, sigHeader, webhookSecret);
+
+ System.out.println(event.getType());
+
+ switch (event.getType()) {
+ case "checkout.session.completed":
+ case "checkout.session.async_payment_succeeded":
+ Session session = (Session) event.getData().getObject();
+
+ this.transactionService.fulfillCheckout(session.getId());
+ break;
+ default:
+
+ }
+
+ return ResponseEntity.ok().body(null);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java b/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java
index 6be8b83..48c04aa 100644
--- a/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java
+++ b/backend/src/main/java/de/szut/casino/security/KeycloakSecurityConfig.java
@@ -2,6 +2,7 @@ package de.szut.casino.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -47,12 +48,16 @@ class KeycloakSecurityConfig {
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
- http.authorizeHttpRequests(auth -> auth
- .requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**", "/health").permitAll()
- .anyRequest().authenticated()
- )
- .oauth2ResourceServer(spec -> spec.jwt(Customizer.withDefaults()));
-
+ http.csrf(csrf -> csrf
+ .ignoringRequestMatchers("/webhook")
+ )
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers(HttpMethod.POST, "/webhook").permitAll()
+ .requestMatchers("/swagger", "/swagger-ui/**", "/v3/api-docs/**", "/health").permitAll()
+ .anyRequest().authenticated()
+ )
+ .oauth2ResourceServer(spec -> spec.jwt(Customizer.withDefaults()));
+
return http.build();
}
diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java
index 83d8f44..c0cc92f 100644
--- a/backend/src/main/java/de/szut/casino/user/UserEntity.java
+++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java
@@ -7,6 +7,7 @@ import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+
import java.math.BigDecimal;
@Setter
@@ -29,4 +30,14 @@ public class UserEntity {
this.username = username;
this.balance = balance;
}
+
+ public void addBalance(long amountInCents) {
+ BigDecimal amountToAdd = BigDecimal.valueOf(amountInCents).movePointLeft(2);
+
+ if (this.balance == null) {
+ this.balance = amountToAdd;
+ } else {
+ this.balance = this.balance.add(amountToAdd);
+ }
+ }
}
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index c7ccadf..00b9f86 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -3,7 +3,8 @@ spring.datasource.username=postgres_user
spring.datasource.password=postgres_pass
server.port=8080
spring.jpa.hibernate.ddl-auto=create-drop
-stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
+stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51PI7FRJX3NPIs0QEHQfzDLhEVoRluIhKw2CfpH2OCUOQI8oNrAKzgHy4eTvdoMdB0Tj5RV3cfSMoifFWy1EWySxO00JViZ0kl2}
+stripe.webhook.secret=whsec_600e1e31dc04d7b0510b904dd352cadbc84dc73659730709ee81997657397b91
app.frontend-host=http://localhost:4200
spring.application.name=lf12_starter
diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts
index 53866cc..c27aa44 100644
--- a/frontend/src/environments/environment.ts
+++ b/frontend/src/environments/environment.ts
@@ -1,4 +1,4 @@
export const environment = {
STRIPE_KEY:
- 'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG',
+ 'pk_test_51PI7FRJX3NPIs0QEHnKXgCHTUnCp72omcZhOB4VXPvMOUlj1bsldptRTocGzcQ9WNWxNVfCKJoNbNDWM0peUQLLW007VFLlXAI',
};