diff --git a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar index afe7e7a..ba35941 100644 Binary files a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar and b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar differ diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/PaymentController.java b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/PaymentController.java index 6a83586..a88a92b 100644 --- a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/PaymentController.java +++ b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/PaymentController.java @@ -62,4 +62,13 @@ public class PaymentController { paymentService.removePayment(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + + @GetMapping("/getAllByUserId/{userId}") + @Operation(summary = "Get all payments by user ID") + public ResponseEntity> getAllByUserId(@PathVariable Integer userId) { + List dtos = paymentService.findAllByUserId(userId) + .stream().map(PaymentDTO::new).collect(Collectors.toList()); + return new ResponseEntity<>(dtos, HttpStatus.OK); + } + } diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/repositories/finance_repositories/PaymentRepository.java b/memberflow-data/src/main/java/com/denniseckerskorn/repositories/finance_repositories/PaymentRepository.java index 5ffd2f7..f49de6d 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/repositories/finance_repositories/PaymentRepository.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/repositories/finance_repositories/PaymentRepository.java @@ -1,8 +1,12 @@ package com.denniseckerskorn.repositories.finance_repositories; +import com.denniseckerskorn.entities.finance.Invoice; import com.denniseckerskorn.entities.finance.Payment; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; public interface PaymentRepository extends JpaRepository { boolean existsByInvoiceId(Integer invoiceId); + + List findByInvoice_User_Id(Integer userId); } diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/services/finance_services/PaymentService.java b/memberflow-data/src/main/java/com/denniseckerskorn/services/finance_services/PaymentService.java index dbfd7dd..e2626be 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/services/finance_services/PaymentService.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/services/finance_services/PaymentService.java @@ -8,7 +8,6 @@ import com.denniseckerskorn.exceptions.EntityNotFoundException; import com.denniseckerskorn.exceptions.InvalidDataException; import com.denniseckerskorn.repositories.finance_repositories.PaymentRepository; import com.denniseckerskorn.services.AbstractService; -import com.denniseckerskorn.services.finance_services.InvoiceService; import jakarta.transaction.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,16 +32,29 @@ public class PaymentService extends AbstractService { @Override @Transactional - public Payment save(Payment entity) throws DuplicateEntityException { - logger.info("Saving payment: {}", entity); - validate(entity); - if (paymentRepository.existsByInvoiceId(entity.getInvoice().getId())) { + public Payment save(Payment payment) throws DuplicateEntityException { + logger.info("Saving payment: {}", payment); + validate(payment); + + Integer invoiceId = payment.getInvoice().getId(); + if (paymentRepository.existsByInvoiceId(invoiceId)) { throw new DuplicateEntityException("A payment already exists for this invoice"); } - updateInvoiceStatus(entity); - return super.save(entity); + + Invoice invoice = invoiceService.findById(invoiceId); + if (invoice == null) { + throw new EntityNotFoundException("Invoice not found"); + } + + payment.setInvoice(invoice); + invoice.setPayment(payment); + + invoice.setStatus(StatusValues.PAID); + invoiceService.update(invoice); + return super.save(payment); } + @Override public Payment update(Payment entity) throws EntityNotFoundException, InvalidDataException { logger.info("Updating payment: {}", entity); @@ -100,4 +112,8 @@ public class PaymentService extends AbstractService { invoiceService.update(invoice); } + public List findAllByUserId(Integer userId) { + return paymentRepository.findByInvoice_User_Id(userId); + } + } diff --git a/memberflow-data/src/test/java/com/denniseckerskorn/services/finance_management_services/PaymentServiceTest.java b/memberflow-data/src/test/java/com/denniseckerskorn/services/finance_management_services/PaymentServiceTest.java index e286e3f..987de4e 100644 --- a/memberflow-data/src/test/java/com/denniseckerskorn/services/finance_management_services/PaymentServiceTest.java +++ b/memberflow-data/src/test/java/com/denniseckerskorn/services/finance_management_services/PaymentServiceTest.java @@ -29,9 +29,6 @@ class PaymentServiceTest { @Mock private InvoiceService invoiceService; - @Mock - private EntityManager entityManager; - @InjectMocks private PaymentService paymentService; @@ -44,59 +41,64 @@ class PaymentServiceTest { invoice = new Invoice(); invoice.setId(1); - invoice.setTotal(new BigDecimal("59.99")); + invoice.setTotal(new BigDecimal("140.00")); invoice.setStatus(StatusValues.NOT_PAID); payment = new Payment(); payment.setId(101); payment.setInvoice(invoice); - payment.setAmount(new BigDecimal("59.99")); + payment.setAmount(new BigDecimal("140.00")); payment.setPaymentDate(LocalDateTime.now().minusDays(1)); - payment.setPaymentMethod(PaymentMethodValues.CREDIT_CARD); + payment.setPaymentMethod(PaymentMethodValues.CASH); payment.setStatus(StatusValues.PAID); - - Field emField = PaymentService.class.getSuperclass().getDeclaredField("entityManager"); - emField.setAccessible(true); - emField.set(paymentService, entityManager); } @Test void save_ShouldUpdateInvoiceStatusAndSavePayment() { when(paymentRepository.existsByInvoiceId(1)).thenReturn(false); + when(invoiceService.findById(1)).thenReturn(invoice); when(paymentRepository.save(any())).thenReturn(payment); Payment saved = paymentService.save(payment); - assertEquals(StatusValues.PAID, payment.getInvoice().getStatus()); + + assertEquals(StatusValues.PAID, saved.getInvoice().getStatus()); verify(invoiceService).update(invoice); + verify(paymentRepository).save(payment); } @Test - void save_DuplicatePayment_ShouldThrow() { + void save_WhenDuplicate_ShouldThrow() { when(paymentRepository.existsByInvoiceId(1)).thenReturn(true); + assertThrows(DuplicateEntityException.class, () -> paymentService.save(payment)); + verify(invoiceService, never()).update(any()); } @Test - void save_InvalidAmount_ShouldThrow() { - payment.setAmount(BigDecimal.ZERO); - assertThrows(InvalidDataException.class, () -> paymentService.save(payment)); - } - - @Test - void save_InvalidPaymentDate_ShouldThrow() { - payment.setPaymentDate(LocalDateTime.now().plusDays(1)); - assertThrows(InvalidDataException.class, () -> paymentService.save(payment)); - } - - @Test - void save_InvalidMethod_ShouldThrow() { - payment.setPaymentMethod(null); - assertThrows(InvalidDataException.class, () -> paymentService.save(payment)); - } - - @Test - void save_InvoiceNull_ShouldThrow() { + void save_WhenInvoiceNull_ShouldThrow() { payment.setInvoice(null); + + assertThrows(InvalidDataException.class, () -> paymentService.save(payment)); + } + + @Test + void save_WhenInvalidAmount_ShouldThrow() { + payment.setAmount(BigDecimal.ZERO); + + assertThrows(InvalidDataException.class, () -> paymentService.save(payment)); + } + + @Test + void save_WhenFutureDate_ShouldThrow() { + payment.setPaymentDate(LocalDateTime.now().plusDays(1)); + + assertThrows(InvalidDataException.class, () -> paymentService.save(payment)); + } + + @Test + void save_WhenMissingMethod_ShouldThrow() { + payment.setPaymentMethod(null); + assertThrows(InvalidDataException.class, () -> paymentService.save(payment)); } } diff --git a/memberflow-frontend/src/components/forms/PaymentForm.jsx b/memberflow-frontend/src/components/forms/PaymentForm.jsx new file mode 100644 index 0000000..a86ec68 --- /dev/null +++ b/memberflow-frontend/src/components/forms/PaymentForm.jsx @@ -0,0 +1,142 @@ +import React, { useEffect, useState } from "react"; +import api from "../../api/axiosConfig"; +import ErrorMessage from "../common/ErrorMessage"; +import "../styles/ContentArea.css"; + +const PaymentForm = () => { + const [students, setStudents] = useState([]); + const [selectedUserId, setSelectedUserId] = useState(""); + const [invoices, setInvoices] = useState([]); + const [selectedInvoiceId, setSelectedInvoiceId] = useState(""); + const [amount, setAmount] = useState(""); + const [paymentMethod, setPaymentMethod] = useState("CASH"); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + useEffect(() => { + api.get("/students/getAll").then((res) => setStudents(res.data)); + }, []); + + const fetchInvoices = async (userId) => { + try { + const res = await api.get(`/invoices/getAllInvoicesByUserId/${userId}`); + const notPaid = res.data.filter((invoice) => invoice.status === "NOT_PAID"); + setInvoices(notPaid); + } catch (err) { + console.error(err); + setError("Error al cargar facturas."); + } + }; + + const handleStudentChange = (e) => { + const userId = e.target.value; + setSelectedUserId(userId); + setSelectedInvoiceId(""); + setInvoices([]); + if (userId) { + fetchInvoices(userId); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(""); + setSuccess(""); + + if (!selectedInvoiceId || !amount || !paymentMethod) { + setError("Completa todos los campos."); + return; + } + + try { + await api.post("/payments/create", { + invoiceId: parseInt(selectedInvoiceId), + paymentDate: new Date().toISOString(), + amount: parseFloat(amount), + paymentMethod, + status: "PAID", + }); + + setSuccess("✅ Pago registrado correctamente."); + setSelectedInvoiceId(""); + setAmount(""); + setPaymentMethod("CASH"); + fetchInvoices(selectedUserId); // actualizar facturas pendientes + } catch (err) { + console.error(err); + setError("❌ Error al registrar el pago."); + } + }; + + return ( +
+
+

Registrar Pago

+ + + + + {invoices.length > 0 && ( + <> + + + + )} + + {selectedInvoiceId && ( + <> + + setAmount(e.target.value)} + /> + + + + + + + + + + )} +
+
+ ); +}; + +export default PaymentForm; diff --git a/memberflow-frontend/src/components/layout/MainLayout.jsx b/memberflow-frontend/src/components/layout/MainLayout.jsx index feda04b..93b6872 100644 --- a/memberflow-frontend/src/components/layout/MainLayout.jsx +++ b/memberflow-frontend/src/components/layout/MainLayout.jsx @@ -24,6 +24,8 @@ import MembershipForm from '../forms/MembershipCreateForm'; import MembershipList from '../lists/MembershipList'; import InvoiceForm from '../forms/InvoiceForm'; import InvoiceList from '../lists/InvoiceList'; +import PaymentForm from '../forms/PaymentForm'; +import PaymentList from '../lists/PaymentList'; import '../styles/MainLayout.css'; @@ -69,6 +71,10 @@ const MainLayout = () => { } /> } /> + } /> + } /> + + {/* Profile Page*/} } /> diff --git a/memberflow-frontend/src/components/layout/SidebarAdmin.jsx b/memberflow-frontend/src/components/layout/SidebarAdmin.jsx index 2dda034..b36588b 100644 --- a/memberflow-frontend/src/components/layout/SidebarAdmin.jsx +++ b/memberflow-frontend/src/components/layout/SidebarAdmin.jsx @@ -39,7 +39,8 @@ const SidebarAdmin = () => {

💵 Finanzas

- + + diff --git a/memberflow-frontend/src/components/lists/PaymentList.jsx b/memberflow-frontend/src/components/lists/PaymentList.jsx new file mode 100644 index 0000000..3168e01 --- /dev/null +++ b/memberflow-frontend/src/components/lists/PaymentList.jsx @@ -0,0 +1,106 @@ +import React, { useState, useEffect } from "react"; +import api from "../../api/axiosConfig"; +import ErrorMessage from "../common/ErrorMessage"; +import '../styles/ContentArea.css'; + +const PaymentList = () => { + const [students, setStudents] = useState([]); + const [selectedUserId, setSelectedUserId] = useState(""); + const [payments, setPayments] = useState([]); + const [error, setError] = useState(""); + + useEffect(() => { + api.get("/students/getAll").then((res) => setStudents(res.data)); + }, []); + + const fetchPayments = async (userId) => { + try { + const res = await api.get(`/payments/getAllByUserId/${userId}`); + setPayments(res.data); + } catch (err) { + console.error(err); + setError("Error al cargar pagos."); + } + }; + + const handleStudentChange = (e) => { + const userId = e.target.value; + setSelectedUserId(userId); + if (userId) { + fetchPayments(userId); + } else { + setPayments([]); + } + }; + + const handleDelete = async (id) => { + if (window.confirm("¿Seguro que quieres eliminar este pago?")) { + try { + await api.delete(`/payments/deleteById/${id}`); + fetchPayments(selectedUserId); + } catch (err) { + console.error(err); + setError("Error al eliminar el pago."); + } + } + }; + + return ( +
+
+

Listado de Pagos

+ + + + + + + {payments.length > 0 && ( + + + + + + + + + + + + + + {payments.map((p) => ( + + + + + + + + + + ))} + +
IDFacturaFechaMonto (€)MétodoEstadoAcciones
{p.id}#{p.invoiceId}{new Date(p.paymentDate).toLocaleDateString()}{p.amount.toFixed(2)}{p.paymentMethod}{p.status} + {/* Botón editar lo montamos luego */} + {/* */} + +
+ )} +
+
+ ); +}; + +export default PaymentList;