feat: Save balance after successful payment
Some checks failed
CI / Get Changed Files (pull_request) Successful in 9s
CI / eslint (pull_request) Successful in 39s
CI / prettier (pull_request) Successful in 37s
CI / test-build (pull_request) Successful in 42s
CI / Checkstyle Main (pull_request) Failing after 1m22s

This commit is contained in:
Phan Huy Tran 2025-03-13 15:02:32 +01:00
parent ce7c30b9ae
commit d25b894f38
11 changed files with 242 additions and 9 deletions

View file

@ -0,0 +1 @@
POST localhost:8080/webhook

View file

@ -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<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto) throws StripeException {
public ResponseEntity<SessionIdDto> checkout(@RequestBody @Valid AmountDto amountDto, @RequestHeader("Authorization") String token) throws StripeException {
Stripe.apiKey = stripeKey;
KeycloakUserDto userData = getKeycloakUserInfo(token);
Optional<UserEntity> 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<KeycloakUserDto> response = this.restTemplate.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class);
return response.getBody();
}
}

View file

@ -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;
}

View file

@ -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<TransactionEntity, Long> {
@Query("SELECT t FROM TransactionEntity t WHERE t.sessionId = ?1")
Optional<TransactionEntity> findOneBySessionID(String sessionId);
}

View file

@ -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<TransactionEntity> 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);
}
}

View file

@ -0,0 +1,6 @@
package de.szut.casino.deposit;
public enum TransactionStatus {
PROCESSING,
SUCCEEDED,
}

View file

@ -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<String> 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);
}
}

View file

@ -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,11 +48,15 @@ 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();
}

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -1,4 +1,4 @@
export const environment = {
STRIPE_KEY:
'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG',
'pk_test_51PI7FRJX3NPIs0QEHnKXgCHTUnCp72omcZhOB4VXPvMOUlj1bsldptRTocGzcQ9WNWxNVfCKJoNbNDWM0peUQLLW007VFLlXAI',
};