diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java new file mode 100644 index 0000000..7200379 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -0,0 +1,33 @@ +package ru.soune.nocopy.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.service.payment.PaymentService; + +import java.util.Map; + +@RestController +@RequestMapping("/api/payments") +@RequiredArgsConstructor +@Slf4j +public class PaymentController { + + private final PaymentService paymentService; + + @PostMapping("/create") + public Payment createPayment(@RequestParam String email, @RequestParam Long tariffId, + @RequestParam String operationType, @RequestParam String operationUuid) { + return paymentService.createPayment(email, tariffId, operationType, operationUuid); + } + + @PostMapping("webhook/yookassa") + public void handleYooKassaNotification(@RequestBody Map notification) { + try { + paymentService.handlePaymentNotification(notification); + } catch (Exception e) { + log.error("Error processing payment notification", e); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/Payment.java b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java new file mode 100644 index 0000000..720037c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java @@ -0,0 +1,61 @@ +package ru.soune.nocopy.entity.payment; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.user.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "payment") +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@EntityListeners(AuditingEntityListener.class) +public class Payment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private PaymentStatus status; + + @Column(name = "payment_uuid", unique = true) + private String paymentUuid; + + @Column(name = "amount") + private Double amount; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Enumerated(EnumType.STRING) + @Column(name = "operation_type", nullable = false) + private PaymentOperationType operationType; + + @Column(name = "cancellation_reason") + private String cancellationReason; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne + @JoinColumn(name = "tariff_id") + private Tariff tariff; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java new file mode 100644 index 0000000..2119238 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java @@ -0,0 +1,6 @@ +package ru.soune.nocopy.entity.payment; + +public enum PaymentOperationType { + TARIFF, + TOKEN_REFILL +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java new file mode 100644 index 0000000..0e9478e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java @@ -0,0 +1,10 @@ +package ru.soune.nocopy.entity.payment; + + +public enum PaymentStatus { + PENDING, + WAITING, + SUCCEEDED, + CANCELED, + FAILED +} diff --git a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java new file mode 100644 index 0000000..2873fda --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.payment.Payment; + +import java.util.Optional; + +@Repository +public interface PaymentRepository extends JpaRepository { + Optional findByPaymentUuid(String paymentUuid); +} diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java new file mode 100644 index 0000000..b637512 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -0,0 +1,101 @@ +package ru.soune.nocopy.service.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.entity.payment.PaymentOperationType; +import ru.soune.nocopy.entity.payment.PaymentStatus; +import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.tarif.TariffInfo; +import ru.soune.nocopy.entity.tarif.TariffStatus; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.PaymentRepository; +import ru.soune.nocopy.repository.TariffInfoRepository; +import ru.soune.nocopy.repository.TariffRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.tariff.TariffInfoService; + +import java.time.LocalDateTime; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class PaymentService { + private final PaymentRepository paymentRepository; + + private final UserRepository userRepository; + + private final TariffRepository tariffRepository; + + private final TariffInfoRepository tariffInfoRepository; + + private final TariffInfoService tariffInfoService; + + @Transactional + public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) { + User user = userRepository.findByEmail(email); + Tariff tariff = tariffRepository.findById(tariffId).orElseThrow(); + + Payment payment = new Payment(); + payment.setStatus(PaymentStatus.PENDING); + payment.setAmount(tariff.getPrice()); + payment.setOperationType(PaymentOperationType.valueOf(operationType)); + payment.setUser(user); + payment.setTariff(tariff); + payment.setPaymentUuid(paymentUuid); + + return paymentRepository.save(payment); + } + + @Transactional + public void handlePaymentNotification(Map notification) { + String eventType = (String) notification.get("event"); + Map object = (Map) notification.get("object"); + String paymentUuid = (String) object.get("id"); + + Payment payment = paymentRepository.findByPaymentUuid(paymentUuid) + .orElseThrow(() -> new RuntimeException("Payment not found")); + + if ("payment.succeeded".equals(eventType)) { + payment.setStatus(PaymentStatus.SUCCEEDED); + payment.setCancellationReason(null); + activateTariffForUser(payment.getUser(), payment.getTariff()); + + } else if ("payment.canceled".equals(eventType)) { + payment.setStatus(PaymentStatus.CANCELED); + + Map cancellationDetails = (Map) object.get("cancellation_details"); + if (cancellationDetails != null) { + String reason = (String) cancellationDetails.get("reason"); + payment.setCancellationReason(reason); + } + + } else if ("payment.waiting_for_capture".equals(eventType)) { + payment.setStatus(PaymentStatus.WAITING); + payment.setCancellationReason(null); + + } else { + payment.setStatus(PaymentStatus.FAILED); + + Map cancellationDetails = (Map) object.get("cancellation_details"); + if (cancellationDetails != null) { + String reason = (String) cancellationDetails.get("reason"); + payment.setCancellationReason(reason); + } + } + + paymentRepository.save(payment); + } + + private void activateTariffForUser(User user, Tariff tariff) { + TariffInfo tariffInfo = new TariffInfo(); + tariffInfo.setStatus(TariffStatus.ACTIVE); + tariffInfo.setStartTariff(LocalDateTime.now()); + tariffInfo.setEndTariff(LocalDateTime.now().plusMonths(1)); + tariffInfo.setTokens(tariff.getTokens()); + tariffInfo.setTariff(tariff); + + tariffInfoService.linkUserTariffInfo(user, tariffInfoService.addTariffInfo(tariffInfo)); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java index 951f7c3..ac28249 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java @@ -78,8 +78,8 @@ public class TariffInfoService { return tariffInfoRepository.save(tariffInfo); } - public void addTariffInfo(TariffInfo tariffInfo) { - tariffInfoRepository.save(tariffInfo); + public TariffInfo addTariffInfo(TariffInfo tariffInfo) { + return tariffInfoRepository.save(tariffInfo); } public List getAllTariffInfos() { @@ -130,6 +130,13 @@ public class TariffInfoService { tariffInfo.getTariff().getMaxFilesCount()); } + @Transactional + public void linkUserTariffInfo(User user, TariffInfo tariffInfo) { + user.setPersonalTariffInfo(tariffInfo); + + userRepository.save(user); + } + private void updateTariffInfoFromDTO(TariffInfo tariffInfo, TariffInfoDTO dto) { tariffInfo.setStatus(TariffStatus.valueOf(dto.getStatus())); tariffInfo.setStartTariff(dto.getStartTariff());