Merge pull request 'feat: Save balance after successful payment' (#81) from feature/save-balance into main
All checks were successful
Release / Release (push) Successful in 45s
All checks were successful
Release / Release (push) Successful in 45s
Reviewed-on: #81 Reviewed-by: Jan Gleytenhoover <jan@kjan.email>
This commit is contained in:
commit
fbaa612980
11 changed files with 242 additions and 9 deletions
1
backend/requests/webhook.http
Normal file
1
backend/requests/webhook.http
Normal file
|
@ -0,0 +1 @@
|
|||
POST localhost:8080/webhook
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package de.szut.casino.deposit;
|
||||
|
||||
public enum TransactionStatus {
|
||||
PROCESSING,
|
||||
SUCCEEDED,
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const environment = {
|
||||
STRIPE_KEY:
|
||||
'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG',
|
||||
'pk_test_51PI7FRJX3NPIs0QEHnKXgCHTUnCp72omcZhOB4VXPvMOUlj1bsldptRTocGzcQ9WNWxNVfCKJoNbNDWM0peUQLLW007VFLlXAI',
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue