Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ PGPASSWORD=root
SERVER=http://localhost:8080
CLIENT=http://localhost:3000
SECRET=vasco-da-gama
GHCI=ID
GHCS=SECRET
GHCI=GHCI
GHCS=GHCS
SWK=SWK
SPK=SPK
SSK=SSK
RABBITMQ_USER=user
RABBITMQ_PASSWORD=root
RABBITMQ_ADDRESSES=amqp://user:root@rabbitmq:5672
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ RUN ./mvnw clean install

FROM eclipse-temurin:21-jdk-alpine

COPY --from=build ./target/NoteHub-1.7.0.jar app.jar
COPY --from=build ./target/NoteHub-2.0.0.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
</div>
<br>
<div align="center">
<a href="https://github.com/notehubbr/notehub-api/releases/tag/v1.7">
<img width="100px" height="25px" src="https://img.shields.io/badge/notehub-1.7-7c3aed">
<a href="https://github.com/notehubbr/notehub-api/releases/tag/v2.0">
<img width="100px" height="25px" src="https://img.shields.io/badge/notehub-2.0-7c3aed">
</a>
</div>

Expand Down
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>
<groupId>br.com.notehub</groupId>
<artifactId>NoteHub</artifactId>
<version>1.7.0</version>
<version>2.0.0</version>
<name>NoteHub</name>
<description>https://notehub.com.br</description>
<url/>
Expand Down Expand Up @@ -119,6 +119,11 @@
<version>5.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>31.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package br.com.notehub.application.controller.payment;

import br.com.notehub.application.dto.payment.SponsorshipREQ;
import br.com.notehub.application.dto.payment.SponsorshipRES;
import br.com.notehub.application.dto.payment.SponsorshipStatusRES;
import br.com.notehub.application.payment.StripeService;
import com.auth0.jwt.JWT;
import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.stripe.net.Webhook;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@CrossOrigin(origins = {"https://notehub.com.br"})
@RequestMapping("/api/v1/payment")
@Tag(name = "Payment Controller", description = "Endpoints for managing payments")
@RequiredArgsConstructor
public class PaymentController {

@Value("${payment.stripe.webhook.key}")
private String secret;

private final StripeService stripeService;

private String getSubject(String bearerToken) {
if (bearerToken == null) return null;
return JWT.decode(bearerToken.replace("Bearer ", "")).getSubject();
}

@Operation(
summary = "Initiates the Stripe sponsorship checkout process",
description = "Creates a Stripe Checkout Session for a sponsorship with the specified currency and amount."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Checkout session created successfully."),
@ApiResponse(responseCode = "400", description = "Invalid input data.", content = @Content(mediaType = "application/json")),
@ApiResponse(responseCode = "403", description = "Invalid token."),
@ApiResponse(responseCode = "500", description = "Internal server error.", content = @Content(examples = {}))
})
@PostMapping("/stripe/sponsorship")
public ResponseEntity<SponsorshipRES> sponsorshipCheckout(
@Parameter(hidden = true) @RequestHeader("Authorization") String accessToken,
@Valid @RequestBody SponsorshipREQ dto
) {
String idFromToken = getSubject(accessToken);
SponsorshipRES res = stripeService.sponsorshipCheckout(idFromToken, dto.locale(), dto.currency(), dto.amount());
return ResponseEntity.status(HttpStatus.OK).body(res);
}

@Operation(
summary = "Verifies the status of a Stripe sponsorship checkout session",
description = "Queries Stripe to determine if the payment for the specified session has been successfully completed."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Session status returned successfully."),
@ApiResponse(responseCode = "403", description = "Invalid token."),
@ApiResponse(responseCode = "500", description = "Internal server error.", content = @Content(examples = {}))
})
@GetMapping("/stripe/sponsorship/verify/{sessionId}")
public ResponseEntity<SponsorshipStatusRES> verifySession(
@PathVariable String sessionId,
@Parameter(hidden = true) @RequestHeader("Authorization") String accessToken
) {
String uIdFromToken = getSubject(accessToken);
return ResponseEntity.status(HttpStatus.OK).body(stripeService.verifySession(sessionId, uIdFromToken));
}

@Hidden
@PostMapping("/stripe/sponsorship/webhook")
public ResponseEntity<Void> handleWebhook(
@RequestBody String payload,
@RequestHeader("Stripe-Signature") String sigHeader
) {
Event event;
try {
event = Webhook.constructEvent(payload, sigHeader, secret);
} catch (SignatureVerificationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
if ("checkout.session.completed".equals(event.getType())) {
stripeService.handleCheckoutSessionCompleted(event);
return ResponseEntity.status(HttpStatus.OK).build();
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package br.com.notehub.application.dto.payment;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record SponsorshipREQ(

@NotBlank(message = "Não pode ser vazia.")
String locale,

@NotBlank(message = "Não pode ser vazia.")
String currency,

@NotNull(message = "Não pode ser nulo.")
@Min(value = 1, message = "Deve ser maior que 0.")
Long amount

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package br.com.notehub.application.dto.payment;

import lombok.Builder;

@Builder
public record SponsorshipRES(
String status,
String message,
String sessionId,
String sessionUrl,
String uId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package br.com.notehub.application.dto.payment;

public record SponsorshipStatusRES(
String sessionId,
String paymentStatus,
String status,
String locale,
String currency,
Long amountTotal
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ public void activate(UUID idFromToken) {
changeField(idFromToken, "active", User::isActive, user -> user.setActive(true));
}

@Transactional
@Override
public void promote(UUID idFromToken) {
changeField(idFromToken, "sponsor", User::isSponsor, user -> user.setSponsor(true));
}

@Transactional
@Override
public void changePassword(String email, String newPassword) {
Expand Down
116 changes: 116 additions & 0 deletions src/main/java/br/com/notehub/application/payment/StripeService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package br.com.notehub.application.payment;

import br.com.notehub.application.dto.payment.SponsorshipRES;
import br.com.notehub.application.dto.payment.SponsorshipStatusRES;
import br.com.notehub.domain.user.UserService;
import br.com.notehub.infra.exception.CustomExceptions;
import com.stripe.Stripe;
import com.stripe.exception.StripeException;
import com.stripe.model.Event;
import com.stripe.model.checkout.Session;
import com.stripe.param.checkout.SessionCreateParams;
import jakarta.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class StripeService {

@Value("${payment.stripe.secret.key}")
private String secret;

@Value("${api.client.host}")
private String client;

private final UserService userService;

private static final String SPONSORSHIP_NAME = "Patrocínio";
private static final Long SPONSORSHIP_QUANTITY = 1L;

private void validateAccess(@Nullable String idFromToken, String idFromSession) {
if (Objects.equals(idFromToken, idFromSession)) return;
throw new AccessDeniedException("Usuário sem permissão.");
}

public SponsorshipRES sponsorshipCheckout(String idFromToken, String locale, String currency, Long amount) {

Stripe.apiKey = secret;

try {

var productData = SessionCreateParams.LineItem.PriceData.ProductData.builder()
.setName(SPONSORSHIP_NAME)
.build();

var priceData = SessionCreateParams.LineItem.PriceData.builder()
.setCurrency(currency)
.setUnitAmount(amount)
.setProductData(productData)
.build();

var lineItem = SessionCreateParams.LineItem.builder()
.setQuantity(SPONSORSHIP_QUANTITY)
.setPriceData(priceData)
.build();

var params = SessionCreateParams.builder()
.setMode(SessionCreateParams.Mode.PAYMENT)
.setLocale(SessionCreateParams.Locale.valueOf(locale))
.setSuccessUrl(String.format("%s/sponsorship/success?session_id={CHECKOUT_SESSION_ID}", client))
.setCancelUrl(String.format("%s/sponsorship", client))
.addLineItem(lineItem)
.putMetadata("purchaseType", "sponsorship")
.putMetadata("uId", idFromToken)
.build();

Session session = Session.create(params);
return new SponsorshipRES(
"SUCCESS",
"Payment session created",
session.getId(),
session.getUrl(),
idFromToken
);

} catch (StripeException e) {
throw new CustomExceptions.CustomStripeException(e);
}

}

public SponsorshipStatusRES verifySession(String sessionId, String uIdFromToken) {
try {
Stripe.apiKey = secret;
Session session = Session.retrieve(sessionId);
String uIdFromSession = session.getMetadata().get("uId");
validateAccess(uIdFromToken, uIdFromSession);
return new SponsorshipStatusRES(
session.getId(),
session.getPaymentStatus(),
session.getStatus(),
session.getLocale(),
session.getCurrency(),
session.getAmountTotal()
);
} catch (StripeException e) {
throw new CustomExceptions.CustomStripeException(e);
}
}

@Transactional
public void handleCheckoutSessionCompleted(Event event) {
Session session = (Session) event.getDataObjectDeserializer()
.getObject()
.orElseThrow(() -> new RuntimeException("Failed to deserialize session"));
UUID uId = UUID.fromString(session.getMetadata().get("uId"));
if ("paid".equals(session.getPaymentStatus())) userService.promote(uId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class NoForbiddenWordsValidator implements ConstraintValidator<NoForbidde
"null", "undefined", "admin", "user", "guest",
"toe", "terms", "termos", "policies", "policy", "cookies", "cookie", "privacy", "legal", "legals", "language", "idioma",
"signup", "signin", "recover", "sent", "activate", "search", "settings", "new", "help", "changelog", "dashboard",
"sponsorship", "success",
"crf", "crfla", "crflamengo", "flamengo", "fla", "tjf", "t.j.f", "jovemfla", "jovem.fla", "jovem_fla",
"ffc", "flufc", "fluminensefc", "fluminese", "flu", "tyf", "t.y.f", "youngflu", "young.flu", "young_flu",
"tcp", "t.c.p", "tcpuro",
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/br/com/notehub/domain/user/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public interface UserService {

void activate(UUID idFromToken);

void promote(UUID idFromToken);

void changePassword(String email, String newPassword);

void changeEmail(String oldEmail, String newEmail);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.stripe.exception.StripeException;
import jakarta.persistence.EntityExistsException;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.dao.DataIntegrityViolationException;
Expand Down Expand Up @@ -39,6 +40,13 @@ private ResponseEntity<List<CustomResponse>> handleMethdArgumentNotValidExceptio
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}

@ExceptionHandler(CustomExceptions.CustomStripeException.class)
private ResponseEntity<List<CustomResponse>> handleStripeException(CustomExceptions.CustomStripeException ex) {
List<FieldError> errors = new ArrayList<>();
errors.add(new FieldError("Payment", "Payment", ex.getMessage()));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors.stream().map(CustomResponse::new).toList());
}

@ExceptionHandler(CustomExceptions.InvalidSecretException.class)
private ResponseEntity<List<CustomResponse>> handleInvalidSecretException(CustomExceptions.InvalidSecretException ex) {
List<FieldError> errors = new ArrayList<>();
Expand Down
Loading