diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/base/entity/BaseLocation.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/base/entity/BaseLocation.java new file mode 100644 index 0000000..fc05bf8 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/base/entity/BaseLocation.java @@ -0,0 +1,21 @@ +package com.lcaohoanq.shoppe.base.entity; + +import jakarta.persistence.MappedSuperclass; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@MappedSuperclass +public class BaseLocation { + + private String name; + private String address; + private String city; + private String country; + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/config/VNPayConfig.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/config/VNPayConfig.java new file mode 100755 index 0000000..2ee6c7f --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/config/VNPayConfig.java @@ -0,0 +1,98 @@ +package com.lcaohoanq.shoppe.config; + +import jakarta.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "vnpay.api") +@Getter +@Setter +public class VNPayConfig { + + private String vnp_Version; + private String vnp_Command; + private String vnp_OrderType; + private String vnp_PayUrl; + private String vnp_Returnurl; + private String vnp_TmnCode; // kiểm tra email sau + private String vnp_HashSecret; // khi đăng ký Test + private String vnp_apiUrl; + + public String hashAllFields(Map fields) { + List fieldNames = new ArrayList(fields.keySet()); + Collections.sort(fieldNames); + StringBuilder sb = new StringBuilder(); + Iterator itr = fieldNames.iterator(); + while (itr.hasNext()) { + String fieldName = (String) itr.next(); + String fieldValue = (String) fields.get(fieldName); + if ((fieldValue != null) && (fieldValue.length() > 0)) { + sb.append(fieldName); + sb.append("="); + sb.append(fieldValue); + } + if (itr.hasNext()) { + sb.append("&"); + } + } + return hmacSHA512(getVnp_HashSecret(), sb.toString()); + } + + public static String hmacSHA512(final String key, final String data) { + try { + + if (key == null || data == null) { + throw new NullPointerException(); + } + final Mac hmac512 = Mac.getInstance("HmacSHA512"); + byte[] hmacKeyBytes = key.getBytes(); + final SecretKeySpec secretKey = new SecretKeySpec(hmacKeyBytes, "HmacSHA512"); + hmac512.init(secretKey); + byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); + byte[] result = hmac512.doFinal(dataBytes); + StringBuilder sb = new StringBuilder(2 * result.length); + for (byte b : result) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + + } catch (Exception ex) { + return ""; + } + } + + public static String getIpAddress(HttpServletRequest request) { + String ipAdress; + try { + ipAdress = request.getHeader("X-FORWARDED-FOR"); + if (ipAdress == null) { + ipAdress = request.getLocalAddr(); + } + } catch (Exception e) { + ipAdress = "Invalid IP:" + e.getMessage(); + } + return ipAdress; + } + + public static String getRandomNumber(int len) { + Random rnd = new Random(); + String chars = "0123456789"; + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + sb.append(chars.charAt(rnd.nextInt(chars.length()))); + } + return sb.toString(); + } +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/config/WebSecurityConfig.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/config/WebSecurityConfig.java index b427c57..35ec3c9 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/config/WebSecurityConfig.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/config/WebSecurityConfig.java @@ -61,6 +61,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { String.format("%s/orders/**", apiPrefix), String.format("%s/assets/**", apiPrefix), String.format("%s/carts/**", apiPrefix), + String.format("%s/wallets/**", apiPrefix), + String.format("%s/inventories/**", apiPrefix), + String.format("%s/payments/**", apiPrefix), "/error" ).permitAll() // Swagger UI with basic auth diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/Category.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/Category.java index 3be15aa..b872854 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/Category.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/Category.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.lcaohoanq.shoppe.base.entity.BaseEntity; +import com.lcaohoanq.shoppe.domain.subcategory.Subcategory; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryController.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryController.java index 712a286..ec3819f 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryController.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryController.java @@ -2,6 +2,9 @@ import com.lcaohoanq.shoppe.component.LocalizationUtils; import com.lcaohoanq.shoppe.api.ApiResponse; +import com.lcaohoanq.shoppe.domain.subcategory.CreateNewSubcategoryResponse; +import com.lcaohoanq.shoppe.domain.subcategory.SubcategoryDTO; +import com.lcaohoanq.shoppe.domain.subcategory.SubcategoryResponse; import com.lcaohoanq.shoppe.exception.MethodArgumentNotValidException; import com.lcaohoanq.shoppe.util.DTOConverter; import jakarta.validation.Valid; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryResponse.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryResponse.java index 3965613..9e354d8 100755 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryResponse.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryResponse.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.lcaohoanq.shoppe.domain.subcategory.Subcategory; import java.time.LocalDateTime; import java.util.Set; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryService.java index 750f66b..5327ebf 100755 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryService.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CategoryService.java @@ -1,5 +1,10 @@ package com.lcaohoanq.shoppe.domain.category; +import com.lcaohoanq.shoppe.domain.subcategory.CreateNewSubcategoryResponse; +import com.lcaohoanq.shoppe.domain.subcategory.Subcategory; +import com.lcaohoanq.shoppe.domain.subcategory.SubcategoryDTO; +import com.lcaohoanq.shoppe.domain.subcategory.SubcategoryRepository; +import com.lcaohoanq.shoppe.domain.subcategory.SubcategoryResponse; import com.lcaohoanq.shoppe.exception.CategoryAlreadyExistException; import com.lcaohoanq.shoppe.exception.CategoryNotFoundException; import com.lcaohoanq.shoppe.base.exception.DataAlreadyExistException; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/ICategoryService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/ICategoryService.java index 1c9e9f8..c241aff 100755 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/ICategoryService.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/ICategoryService.java @@ -2,6 +2,9 @@ import com.lcaohoanq.shoppe.base.exception.DataAlreadyExistException; import com.lcaohoanq.shoppe.base.exception.DataNotFoundException; +import com.lcaohoanq.shoppe.domain.subcategory.CreateNewSubcategoryResponse; +import com.lcaohoanq.shoppe.domain.subcategory.SubcategoryDTO; +import com.lcaohoanq.shoppe.domain.subcategory.SubcategoryResponse; import java.util.List; public interface ICategoryService { diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/IInventoryService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/IInventoryService.java new file mode 100644 index 0000000..d0d6eda --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/IInventoryService.java @@ -0,0 +1,5 @@ +package com.lcaohoanq.shoppe.domain.inventory; + +public interface IInventoryService { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/Inventory.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/Inventory.java new file mode 100644 index 0000000..e6cad95 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/Inventory.java @@ -0,0 +1,56 @@ +package com.lcaohoanq.shoppe.domain.inventory; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.lcaohoanq.shoppe.base.entity.BaseEntity; +import com.lcaohoanq.shoppe.domain.product.Product; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import java.util.List; +import java.util.Set; +import javax.print.event.PrintJobAttributeEvent; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "inventories") +@Entity +public class Inventory extends BaseEntity { + + @Id + @SequenceGenerator(name = "inventories_seq", sequenceName = "inventories_id_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "inventories_seq") + @Column(name="id", unique=true, nullable=false) + @JsonProperty("id") + private Long id; + + @JsonManagedReference + @OneToMany(mappedBy = "inventory") + @JsonProperty("inventory_locations") + private Set inventoryLocations; + + @OneToMany(mappedBy = "inventory") + @JsonManagedReference + private Set products; + + private Long quantity; + + private Long reserved; //Reserved quantity for orders + + @Column(name = "reorder_point") + private Long reorderPoint; //The quantity that triggers a reorder + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryController.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryController.java new file mode 100644 index 0000000..7056898 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryController.java @@ -0,0 +1,16 @@ +package com.lcaohoanq.shoppe.domain.inventory; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("${api.prefix}/inventories") +@RequiredArgsConstructor +@Slf4j +public class InventoryController { + + + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryLocation.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryLocation.java new file mode 100644 index 0000000..64a74d5 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryLocation.java @@ -0,0 +1,43 @@ +package com.lcaohoanq.shoppe.domain.inventory; + + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.lcaohoanq.shoppe.base.entity.BaseLocation; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "inventory_locations") +@Entity +public class InventoryLocation extends BaseLocation { + + @Id + @SequenceGenerator(name = "inventory_locations_seq", sequenceName = "inventory_locations_id_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "inventory_locations_seq") + @Column(name="id", unique=true, nullable=false) + @JsonProperty("id") + private Long id; + + @ManyToOne + @JsonBackReference + @JoinColumn(name = "inventory_id", nullable = false) + private Inventory inventory; + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryRepository.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryRepository.java new file mode 100644 index 0000000..0c59ef8 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryRepository.java @@ -0,0 +1,7 @@ +package com.lcaohoanq.shoppe.domain.inventory; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InventoryRepository extends JpaRepository { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryService.java new file mode 100644 index 0000000..1c9b330 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/inventory/InventoryService.java @@ -0,0 +1,8 @@ +package com.lcaohoanq.shoppe.domain.inventory; + +import org.springframework.stereotype.Service; + +@Service +public class InventoryService implements IInventoryService { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/IOrderShippingService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/IOrderShippingService.java new file mode 100644 index 0000000..81ef3c4 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/IOrderShippingService.java @@ -0,0 +1,5 @@ +package com.lcaohoanq.shoppe.domain.logistic; + +public interface IOrderShippingService { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/IShippingCarrierService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/IShippingCarrierService.java new file mode 100644 index 0000000..6564e3a --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/IShippingCarrierService.java @@ -0,0 +1,5 @@ +package com.lcaohoanq.shoppe.domain.logistic; + +public interface IShippingCarrierService { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShipping.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShipping.java new file mode 100644 index 0000000..b08a01a --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShipping.java @@ -0,0 +1,61 @@ +package com.lcaohoanq.shoppe.domain.logistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.lcaohoanq.shoppe.base.entity.BaseEntity; +import com.lcaohoanq.shoppe.domain.order.Order; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "orders_shipping") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class OrderShipping extends BaseEntity { + + @Id + @SequenceGenerator(name = "orders_shipping_seq", sequenceName = "orders_shipping_id_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "orders_shipping_seq") + @Column(name="id", unique=true, nullable=false) + @JsonProperty("id") + private Long id; + + @ManyToOne + @JoinColumn(name = "order_id", nullable = false) + private Order order; + + @ManyToOne + @JoinColumn(name = "shipping_carrier_id", nullable = false) + private ShippingCarrier shippingCarrier; + + @Column(name = "tracking_number") + private String trackingNumber; + + @Column(name = "shipping_fee") + private Float shippingFee; + + @Column(name = "shipping_status") + private String shippingStatus; + + @Column(name = "shipping_date") + private Date shippingDate; + + @Column(name = "estimated_delivery") + private Date estimatedDelivery; + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShippingRepository.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShippingRepository.java new file mode 100644 index 0000000..4bac54e --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShippingRepository.java @@ -0,0 +1,7 @@ +package com.lcaohoanq.shoppe.domain.logistic; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OrderShippingRepository extends JpaRepository { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShippingService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShippingService.java new file mode 100644 index 0000000..51539e7 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/OrderShippingService.java @@ -0,0 +1,8 @@ +package com.lcaohoanq.shoppe.domain.logistic; + +import org.springframework.stereotype.Service; + +@Service +public class OrderShippingService { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/shippingcarrier/ShippingCarrier.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrier.java similarity index 76% rename from SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/shippingcarrier/ShippingCarrier.java rename to SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrier.java index a045122..710a70f 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/shippingcarrier/ShippingCarrier.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrier.java @@ -1,4 +1,4 @@ -package com.lcaohoanq.shoppe.domain.shippingcarrier; +package com.lcaohoanq.shoppe.domain.logistic; import com.fasterxml.jackson.annotation.JsonProperty; import com.lcaohoanq.shoppe.enums.ShippingCarrierName; @@ -28,8 +28,8 @@ public class ShippingCarrier extends BaseEntity { @Id - @SequenceGenerator(name = "orders_seq", sequenceName = "orders_id_seq", allocationSize = 1) - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "orders_seq") + @SequenceGenerator(name = "shipping_carriers_seq", sequenceName = "shipping_carriers_id_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "shipping_carriers_seq") @Column(name="id", unique=true, nullable=false) @JsonProperty("id") private Long id; @@ -37,5 +37,11 @@ public class ShippingCarrier extends BaseEntity { @Enumerated(EnumType.STRING) @Column(name = "name", length = 100) private ShippingCarrierName name; + + @Column(name = "tracking_url") + private String trackingUrl; + + @Column(name = "contact_number") + private String contactNumber; } diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrierRepository.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrierRepository.java new file mode 100644 index 0000000..7b67384 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrierRepository.java @@ -0,0 +1,7 @@ +package com.lcaohoanq.shoppe.domain.logistic; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ShippingCarrierRepository extends JpaRepository { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrierService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrierService.java new file mode 100644 index 0000000..d23e41b --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/logistic/ShippingCarrierService.java @@ -0,0 +1,8 @@ +package com.lcaohoanq.shoppe.domain.logistic; + +import org.springframework.stereotype.Service; + +@Service +public class ShippingCarrierService { + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/IPaymentService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/IPaymentService.java new file mode 100755 index 0000000..612d56d --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/IPaymentService.java @@ -0,0 +1,32 @@ +package com.lcaohoanq.shoppe.domain.payment; + +import com.lcaohoanq.shoppe.enums.EPaymentStatus; +import java.io.UnsupportedEncodingException; +import java.util.Map; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface IPaymentService { + Map createDepositPayment(PaymentDTO paymentDTO, String ipAddress) + throws UnsupportedEncodingException; + + Map createOrderPayment(PaymentDTO paymentDTO, String ipAddress) + throws UnsupportedEncodingException; + + Map handlePaymentReturn(Map requestParams); + + Payment createPayment(PaymentDTO payment); + + Optional getPaymentByOrderID(Long id); + + Map createPaymentAndUpdateOrder(PaymentDTO paymentDTO) throws Exception; + + Map createDrawOutRequest(PaymentDTO paymentDrawOutDTO) throws Exception; + + Page getPaymentsByUserId(Long userId, Pageable pageable); + Page getPaymentsByUserIdAndStatus(Long userId, EPaymentStatus status, Pageable pageable); + Page getPaymentsByKeywordAndStatus(String keyword, EPaymentStatus status, Pageable pageable); + Page getPaymentsByKeyword(String keyword, Pageable pageable); + Payment updatePaymentStatus(Long id, EPaymentStatus status) throws Exception; +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/Payment.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/Payment.java new file mode 100755 index 0000000..e5a2d5a --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/Payment.java @@ -0,0 +1,90 @@ +package com.lcaohoanq.shoppe.domain.payment; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.lcaohoanq.shoppe.domain.order.Order; +import com.lcaohoanq.shoppe.domain.user.User; +import com.lcaohoanq.shoppe.enums.EPaymentStatus; +import com.lcaohoanq.shoppe.enums.EPaymentType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "payments") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Payment { + + @Id + @SequenceGenerator(name = "payments_seq", sequenceName = "payments_id_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "payments_seq") + @Column(name="id", unique=true, nullable=false) + @JsonProperty("id") + private Long id; + + @Column(name = "payment_amount", nullable = false) + private Float paymentAmount; + + @Column(name = "payment_date", nullable = false) + private LocalDateTime paymentDate; + + @Column(name = "payment_method", nullable = false) + private String paymentMethod; + + @Enumerated(EnumType.STRING) + @Column(name = "payment_status", nullable = false) + private EPaymentStatus paymentStatus; // e.g., SUCCESS, PENDING, REFUNDED + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "order_id") + private Order order; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(name = "payment_type", nullable = false) + private EPaymentType paymentType; // e.g., 'DEPOSIT', 'ORDER', 'DRAW_OUT' + + @Column(name = "bank_number") + private String bankNumber; + + @Column(name = "bank_name") + private String bankName; + + // Track refunds related to this payment +// @OneToMany(mappedBy = "payment", cascade = CascadeType.ALL) +// private List refunds; + + // Calculate total refunded amount +// public Float getTotalRefundedAmount() { +// return refunds.stream() +// .map(Refund::getRefundAmount) +// .reduce(0.0f, Float::sum); +// } +// +// // Check if the payment is refundable +// public boolean isRefundable(Float refundAmount) { +// return paymentAmount >= getTotalRefundedAmount() + refundAmount; +// } +} + diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentController.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentController.java new file mode 100755 index 0000000..5271e13 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentController.java @@ -0,0 +1,163 @@ +package com.lcaohoanq.shoppe.domain.payment; + +import com.lcaohoanq.shoppe.config.VNPayConfig; +import com.lcaohoanq.shoppe.enums.EPaymentStatus; +import com.lcaohoanq.shoppe.util.DTOConverter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +@RestController +@RequestMapping("${api.prefix}/payments") +@RequiredArgsConstructor +public class PaymentController implements DTOConverter { + + private final IPaymentService paymentService; + + @PostMapping("/create_deposit_payment") + public ResponseEntity createDepositPayment( + @Valid @RequestBody PaymentDTO paymentDTO, + HttpServletRequest request) throws UnsupportedEncodingException { + + String vnp_IpAddr = VNPayConfig.getIpAddress(request); + Map response = paymentService.createDepositPayment(paymentDTO, + vnp_IpAddr); + return ResponseEntity.ok(response); + } + + @GetMapping("/vnpay/payment_return") + public ResponseEntity handleVNPayReturn(@RequestParam Map requestParams) { + Map result = paymentService.handlePaymentReturn(requestParams); + + String frontendUrl_dev = "http://localhost:3000/payments/vnpay-payment-return"; + String frontendUrl_prod = "https://fkoi88.me/payments/vnpay-payment-return"; + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(frontendUrl_dev); + + for (Map.Entry entry : result.entrySet()) { + builder.queryParam(entry.getKey(), entry.getValue().toString()); + } + + String redirectUrl = builder.toUriString(); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); + } + + @PostMapping("/create_order_payment") + public ResponseEntity createOrderPayment( + @Valid @RequestBody PaymentDTO paymentDTO, + HttpServletRequest request) throws Exception { + + if ("Cash".equals(paymentDTO.getPaymentMethod())) { + Map response = paymentService.createPaymentAndUpdateOrder( + paymentDTO); + return ResponseEntity.ok(response); + } else { + String vnp_IpAddr = VNPayConfig.getIpAddress(request); + Map response = paymentService.createOrderPayment(paymentDTO, + vnp_IpAddr); + return ResponseEntity.ok(response); + } + + } + + @PostMapping("/create_drawout_request") + public ResponseEntity createDrawOutRequest( + @Valid @RequestBody PaymentDTO paymentDTO + ) throws Exception { + Map response = paymentService.createDrawOutRequest(paymentDTO); + return ResponseEntity.ok(response); + } + +// @GetMapping("/user/{user_id}/get-sorted-payments") +// @PreAuthorize("hasAnyRole('ROLE_MEMBER', 'ROLE_BREEDER')") +// public ResponseEntity getPaymentByUserIdAndStatus( +// @PathVariable("user_id") Long userId, +// @RequestParam("status") EPaymentStatus status, +// @RequestParam(defaultValue = "0") int page, +// @RequestParam(defaultValue = "10") int limit) { +// +// PageRequest pageRequest = PageRequest.of(page, limit); +// +// Page payments; +// PaymentPaginationResponse response = new PaymentPaginationResponse(); +// if (String.valueOf(status).equals("ALL")) { +// payments = paymentService.getPaymentsByUserId(userId, pageRequest) +// .map(DTOConverter::fromPayment); +// } else { +// payments = paymentService.getPaymentsByUserIdAndStatus(userId, status, pageRequest) +// .map(DTOConverter::fromPayment); +// } +// response.setItem(payments.getContent()); +// response.setTotalItem(payments.getTotalElements()); +// response.setTotalPage(payments.getTotalPages()); +// return ResponseEntity.ok(response); +// +// } + +// @GetMapping("/get-payments-by-keyword-and-status") +// @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_STAFF')") +// public ResponseEntity getPaymentsByStatusAndKeyword( +// @RequestParam(defaultValue = "", required = false) String keyword, +// @RequestParam EPaymentStatus status, +// @RequestParam(defaultValue = "0") int page, +// @RequestParam(defaultValue = "10") int limit) { +// try { +// PageRequest pageRequest = PageRequest.of(page, limit); +// Page payments; +// if (String.valueOf(status).equals("ALL")) { +// payments = paymentService.getPaymentsByKeyword(keyword, pageRequest) +// .map(DTOConverter::fromPayment); +// } else { +// payments = paymentService.getPaymentsByKeywordAndStatus(keyword, status, +// pageRequest) +// .map(DTOConverter::fromPayment); +// } +// PaymentPaginationResponse response = new PaymentPaginationResponse(); +// response.setItem(payments.getContent()); +// response.setTotalItem(payments.getTotalElements()); +// response.setTotalPage(payments.getTotalPages()); +// return ResponseEntity.ok(response); +// } catch (Exception e) { +// BaseResponse response = new BaseResponse<>(); +// response.setMessage("Failed to get payments"); +// response.setReason(e.getMessage()); +// return ResponseEntity.badRequest().body(response); +// } +// } + +// @PutMapping("/{id}/update-payment-status") +// @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_STAFF')") +// public ResponseEntity updatePaymentStatus( +// @PathVariable Long id, +// @Valid @RequestBody PaymentStatusUpdateDTO paymentStatusUpdateDTO) { +// try { +// Payment payment = paymentService.updatePaymentStatus(id, +// EPaymentStatus.valueOf( +// paymentStatusUpdateDTO.getStatus())); +// return ResponseEntity.ok(DTOConverter.fromPayment(payment)); +// } catch (Exception e) { +// BaseResponse response = new BaseResponse<>(); +// response.setMessage("Failed to update payment status"); +// response.setReason(e.getMessage()); +// return ResponseEntity.badRequest().body(response); +// } +// } +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentDTO.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentDTO.java new file mode 100755 index 0000000..9be7f97 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentDTO.java @@ -0,0 +1,48 @@ +package com.lcaohoanq.shoppe.domain.payment; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PaymentDTO { + + @JsonProperty("payment_amount") + @Min(value = 1, message = "Payment amount must be greater than 0") + private Float paymentAmount; + + @JsonProperty("payment_method") + @NotNull(message = "Payment method is required") + private String paymentMethod; + + @JsonProperty("payment_type") + @NotNull(message = "Payment type is required") + private String paymentType; + + @JsonProperty("order_id") + private Long orderId; + + @JsonProperty("payment_status") + private String paymentStatus; + + @JsonProperty("user_id") + @NotNull(message = "User id is required") + @Min(value = 1, message = "User id must be greater than 0") + private Long userId; + + @JsonProperty("bank_number") + private String bankNumber; + + @JsonProperty("bank_name") + private String bankName; + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentDrawOutDTO.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentDrawOutDTO.java new file mode 100755 index 0000000..9c0f001 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentDrawOutDTO.java @@ -0,0 +1,26 @@ +package com.lcaohoanq.shoppe.domain.payment; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PaymentDrawOutDTO { + @JsonProperty("payment_amount") + @Min(value = 1, message = "Payment amount must be greater than 0") + private Float paymentAmount; + + @JsonProperty("user_id") + @NotNull(message = "User id is required") + @Min(value = 1, message = "User id must be greater than 0") + private Long userId; +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentRepository.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentRepository.java new file mode 100644 index 0000000..7a201b5 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentRepository.java @@ -0,0 +1,46 @@ +package com.lcaohoanq.shoppe.domain.payment; + +import com.lcaohoanq.shoppe.domain.order.Order; +import com.lcaohoanq.shoppe.enums.EPaymentStatus; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface PaymentRepository extends JpaRepository { + + Optional findByOrder(Order order); + + Page findPaymentsByUserId(Long userId, Pageable pageable); + + Page findPaymentsByUserIdAndPaymentStatus(Long userId, EPaymentStatus status, + Pageable pageable); + + @Query( + "SELECT p FROM Payment p WHERE " + + "(:status IS NULL OR p.paymentStatus = :status) " + + "AND (:keyword IS NULL OR :keyword = '' " + + "OR p.bankNumber LIKE LOWER(CONCAT('%', :keyword, '%')) " + + " OR LOWER(p.user.name) LIKE LOWER(CONCAT('%', :keyword, '%'))" + + " OR CAST(p.id AS string) LIKE LOWER(CONCAT('%', :keyword, '%'))" + + " OR CAST(p.paymentAmount AS string) LIKE LOWER(CONCAT('%', :keyword, '%'))" + + " OR CAST(p.user.id AS string) LIKE LOWER(CONCAT('%', :keyword, '%')))" + + "ORDER BY p.paymentDate DESC" + ) + Page findPaymentsByStatusAndKeyWord(String keyword, EPaymentStatus status, + Pageable pageable); + + @Query( + "SELECT p FROM Payment p WHERE " + + "(:keyword IS NULL OR :keyword = '' OR " + + "p.bankNumber LIKE LOWER(CONCAT('%', :keyword, '%')) " + + "OR LOWER(p.user.name) LIKE LOWER(CONCAT('%', :keyword, '%')) " + + " OR CAST(p.id AS string) LIKE LOWER(CONCAT('%', :keyword, '%'))" + + " OR CAST(p.paymentAmount AS string) LIKE LOWER(CONCAT('%', :keyword, '%'))" + + "OR CAST(p.user.id AS string) LIKE LOWER(CONCAT('%', :keyword, '%')))" + ) + Page findPaymentsByKeyword(String keyword, Pageable pageable); + + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentService.java new file mode 100644 index 0000000..2e9059e --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentService.java @@ -0,0 +1,335 @@ +package com.lcaohoanq.shoppe.domain.payment; + +import com.lcaohoanq.shoppe.base.exception.DataAlreadyExistException; +import com.lcaohoanq.shoppe.domain.mail.IMailService; +import com.lcaohoanq.shoppe.domain.order.IOrderService; +import com.lcaohoanq.shoppe.domain.order.Order; +import com.lcaohoanq.shoppe.domain.order.OrderRepository; +import com.lcaohoanq.shoppe.domain.user.IUserService; +import com.lcaohoanq.shoppe.domain.user.User; +import com.lcaohoanq.shoppe.domain.user.UserRepository; +import com.lcaohoanq.shoppe.domain.wallet.WalletService; +import com.lcaohoanq.shoppe.enums.EPaymentStatus; +import com.lcaohoanq.shoppe.enums.EPaymentType; +import com.lcaohoanq.shoppe.enums.EmailCategoriesEnum; +import com.lcaohoanq.shoppe.enums.OrderStatus; +import com.lcaohoanq.shoppe.exception.MalformDataException; +import jakarta.mail.MessagingException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TimeZone; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thymeleaf.context.Context; + +@Service +@RequiredArgsConstructor +public class PaymentService implements IPaymentService { + + private final PaymentRepository paymentRepository; + private final IUserService userService; + private final IOrderService orderService; + private final UserRepository userRepository; + private final OrderRepository orderRepository; + private final com.lcaohoanq.shoppe.config.VNPayConfig VNPayConfig; + private final IMailService mailService; + private final WalletService walletService; + + @Override + public Map createDepositPayment(PaymentDTO paymentDTO, String ipAddress) + throws UnsupportedEncodingException { + return createVNPayPayment(paymentDTO, ipAddress, "Deposit to account:"); + } + + @Override + public Map createOrderPayment(PaymentDTO paymentDTO, String ipAddress) + throws UnsupportedEncodingException { + return createVNPayPayment(paymentDTO, ipAddress, "Payment for order:"); + } + + private Map createVNPayPayment(PaymentDTO paymentDTO, String ipAddress, String orderInfoPrefix) + throws UnsupportedEncodingException { + long amount = (long) (paymentDTO.getPaymentAmount() * 100); + String id = orderInfoPrefix.startsWith("Deposit") ? paymentDTO.getUserId().toString() + : paymentDTO.getOrderId().toString(); + String vnp_TxnRef = com.lcaohoanq.shoppe.config.VNPayConfig.getRandomNumber(8); + + Map vnp_Params = createBaseVnpParams(amount, vnp_TxnRef, ipAddress); + vnp_Params.put("vnp_OrderInfo", orderInfoPrefix + id); + + String paymentUrl = createPaymentUrl(vnp_Params); + + Map response = new HashMap<>(); + response.put("paymentUrl", paymentUrl); + return response; + } + + private Map createBaseVnpParams(long amount, String vnp_TxnRef, String vnp_IpAddr) { + Map vnp_Params = new HashMap<>(); + vnp_Params.put("vnp_Version", "2.1.0"); + vnp_Params.put("vnp_Command", "pay"); + vnp_Params.put("vnp_TmnCode", VNPayConfig.getVnp_TmnCode()); + vnp_Params.put("vnp_Amount", String.valueOf(amount)); + vnp_Params.put("vnp_CurrCode", "VND"); + vnp_Params.put("vnp_BankCode", "NCB"); + vnp_Params.put("vnp_TxnRef", vnp_TxnRef); + vnp_Params.put("vnp_OrderType", "other"); + vnp_Params.put("vnp_Locale", "vn"); + vnp_Params.put("vnp_ReturnUrl", VNPayConfig.getVnp_Returnurl()); + vnp_Params.put("vnp_IpAddr", vnp_IpAddr); + + Calendar cld = Calendar.getInstance(TimeZone.getTimeZone("Etc/GMT+7")); + SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss"); + String vnp_CreateDate = formatter.format(cld.getTime()); + vnp_Params.put("vnp_CreateDate", vnp_CreateDate); + + cld.add(Calendar.MINUTE, 15); + String vnp_ExpireDate = formatter.format(cld.getTime()); + vnp_Params.put("vnp_ExpireDate", vnp_ExpireDate); + + return vnp_Params; + } + + private String createPaymentUrl(Map vnp_Params) throws UnsupportedEncodingException { + List fieldNames = new ArrayList(vnp_Params.keySet()); + Collections.sort(fieldNames); + StringBuilder hashData = new StringBuilder(); + StringBuilder query = new StringBuilder(); + Iterator itr = fieldNames.iterator(); + while (itr.hasNext()) { + String fieldName = (String) itr.next(); + String fieldValue = vnp_Params.get(fieldName); + if ((fieldValue != null) && (fieldValue.length() > 0)) { + hashData.append(fieldName); + hashData.append('='); + hashData.append(URLEncoder.encode(fieldValue, StandardCharsets.US_ASCII)); + query.append(URLEncoder.encode(fieldName, StandardCharsets.US_ASCII)); + query.append('='); + query.append(URLEncoder.encode(fieldValue, StandardCharsets.US_ASCII)); + if (itr.hasNext()) { + query.append('&'); + hashData.append('&'); + } + } + } + String queryUrl = query.toString(); + String vnp_SecureHash = com.lcaohoanq.shoppe.config.VNPayConfig.hmacSHA512(VNPayConfig.getVnp_HashSecret(), hashData.toString()); + queryUrl += "&vnp_SecureHash=" + vnp_SecureHash; + return VNPayConfig.getVnp_PayUrl() + "?" + queryUrl; + } + + @Override + public Map handlePaymentReturn(Map requestParams) { + String vnp_ResponseCode = requestParams.get("vnp_ResponseCode"); + String vnp_TxnRef = requestParams.get("vnp_TxnRef"); + String vnp_Amount = requestParams.get("vnp_Amount"); + String vnp_OrderInfo = requestParams.get("vnp_OrderInfo"); + + Map result = new HashMap<>(); + result.put("success", "00".equals(vnp_ResponseCode)); + + if ("00".equals(vnp_ResponseCode)) { + processSuccessfulPayment(result, vnp_Amount, vnp_OrderInfo); + } else { + result.put("message", "Payment failed"); + result.put("responseCode", vnp_ResponseCode); + } + + return result; + } + + private void processSuccessfulPayment(Map result, String vnp_Amount, String vnp_OrderInfo) { + long amount = Long.parseLong(vnp_Amount) / 100; + float amountFloat = Float.parseFloat(vnp_Amount) / 100; + Payment payment = createPaymentObject(amountFloat, vnp_OrderInfo); + + if (vnp_OrderInfo.startsWith("Deposit to account:")) { + processDepositPayment(result, amount, payment); + } else { + processOrderPayment(result, amount, payment); + } + } + + private Payment createPaymentObject(float amount, String orderInfo) { + return Payment.builder() + .paymentAmount(amount) + .paymentMethod("VNPAY") + .paymentDate(LocalDateTime.now()) + .paymentType(orderInfo.startsWith("Deposit") ? EPaymentType.DEPOSIT : EPaymentType.ORDER) + .paymentStatus(EPaymentStatus.SUCCESS) + .user(orderInfo.startsWith("Deposit") + ? userRepository.findById(Long.parseLong(orderInfo.split(":")[1])).orElse(null) + : orderRepository.findById(Long.parseLong(orderInfo.split(":")[1])) + .orElseThrow(() -> new MalformDataException("Order not found")).getUser()) + .order(orderInfo.startsWith("Payment for order") + ? orderRepository.findById(Long.parseLong(orderInfo.split(":")[1])) + .orElseThrow(() -> new MalformDataException("Order not found")) + : null) + .build(); + } + + private void processDepositPayment(Map result, long amount, Payment payment) { + String userId = payment.getUser().getId().toString(); + try { + payment.setUser(userRepository.findById(Long.parseLong(userId)).orElse(null)); + paymentRepository.save(payment); + walletService.updateAccountBalance(Long.parseLong(userId), amount); + result.put("message", "Deposit successful"); + result.put("userId", userId); + result.put("amount", amount); + result.put("paymentType", "deposit"); + } catch (Exception e) { + result.put("success", false); + result.put("message", "Error updating balance"); + } + } + + private void processOrderPayment(Map result, long amount, Payment payment) { + result.put("message", "Order payment successful"); + result.put("amount", amount); + result.put("paymentType", "order"); + String orderId = payment.getOrder().getId().toString(); + orderRepository.findById(Long.parseLong(orderId)).ifPresent(order -> { + orderService.updateOrderStatus(order.getId(), OrderStatus.PROCESSING); + orderRepository.save(order); + payment.setOrder(order); + paymentRepository.save(payment); + }); + } + + @Override + public Payment createPayment(PaymentDTO paymentDTO) { + Payment payment = Payment.builder() + .paymentAmount(paymentDTO.getPaymentAmount()) + .paymentMethod(paymentDTO.getPaymentMethod()) + .paymentType(EPaymentType.valueOf(paymentDTO.getPaymentType())) + .order(paymentDTO.getOrderId() != null ? orderRepository.findById(paymentDTO.getOrderId()) + .orElseThrow(() -> new MalformDataException("Order not found")) : null) + .user(userRepository.findById(paymentDTO.getUserId()).orElse(null)) + .paymentStatus(EPaymentStatus.valueOf(paymentDTO.getPaymentStatus())) + .bankNumber(paymentDTO.getBankNumber()) + .bankName(paymentDTO.getBankName()) + .paymentDate(LocalDateTime.now()) + .build(); + return paymentRepository.save(payment); + } + + @Override + public Optional getPaymentByOrderID(Long id) { + Order existingOrder = orderRepository.findById(id).orElse(null); + return paymentRepository.findByOrder(existingOrder); + } + + @Override + @Transactional + public Map createPaymentAndUpdateOrder(PaymentDTO paymentDTO) throws Exception { + if (getPaymentByOrderID(paymentDTO.getOrderId()).isPresent()) { + throw new DataAlreadyExistException("Payment already exists for order"); + } + + paymentDTO.setPaymentStatus(EPaymentStatus.PENDING.toString()); + Payment payment = createPayment(paymentDTO); + if (payment == null) { + throw new Exception("Failed to create payment"); + } + orderService.updateOrderStatus(paymentDTO.getOrderId(), OrderStatus.PROCESSING); + Map response = new HashMap<>(); + response.put("payment", payment); + response.put("orderStatus", OrderStatus.PROCESSING); + return response; + } + + @Override + @Transactional + public Map createDrawOutRequest(PaymentDTO paymentDrawOutDTO) throws Exception { + User user = userRepository.findById(paymentDrawOutDTO.getUserId()) + .orElseThrow(() -> new MalformDataException("User not found")); + + Float userBalance = user.getWallet().getBalance(); + + if (userBalance < paymentDrawOutDTO.getPaymentAmount()) { + throw new MalformDataException("Insufficient balance"); + } + + paymentDrawOutDTO.setPaymentType(EPaymentType.DRAW_OUT.toString()); + paymentDrawOutDTO.setPaymentStatus(EPaymentStatus.PENDING.toString()); + Payment payment = createPayment(paymentDrawOutDTO); + + walletService.updateAccountBalance(paymentDrawOutDTO.getUserId(), -paymentDrawOutDTO.getPaymentAmount().longValue()); + + Map response = new HashMap<>(); + response.put("payment", payment); + response.put("newBalance", userBalance); + return response; + } + + @Override + public Page getPaymentsByUserId(Long userId, Pageable pageable) { + userRepository.findById(userId).orElseThrow(() -> new MalformDataException("User not found")); + return paymentRepository.findPaymentsByUserId(userId, pageable); + } + + @Override + public Page getPaymentsByUserIdAndStatus(Long userId, EPaymentStatus status, Pageable pageable) { + userRepository.findById(userId).orElseThrow(() -> new MalformDataException("User not found")); + return paymentRepository.findPaymentsByUserIdAndPaymentStatus(userId, status, pageable); + } + + @Override + public Page getPaymentsByKeywordAndStatus(String keyword, EPaymentStatus status, Pageable pageable) { + return paymentRepository.findPaymentsByStatusAndKeyWord(keyword, status, pageable); + } + + @Override + public Page getPaymentsByKeyword(String keyword, Pageable pageable) { + return paymentRepository.findPaymentsByKeyword(keyword, pageable); + } + + @Override + public Payment updatePaymentStatus(Long id, EPaymentStatus status) throws Exception { + Payment payment = paymentRepository.findById(id).orElseThrow(() -> new MalformDataException("Payment not found")); + if (payment.getPaymentStatus().equals(EPaymentStatus.PENDING)) { + if (payment.getPaymentType().equals(EPaymentType.DRAW_OUT)) { + if (status.equals(EPaymentStatus.REFUNDED)) { + User user = payment.getUser(); + walletService.updateAccountBalance(user.getId(), payment.getPaymentAmount().longValue()); + sendMail(payment, status); + } else if (status.equals(EPaymentStatus.SUCCESS)) { + sendMail(payment, status); + } + } + } + payment.setPaymentStatus(status); + return paymentRepository.save(payment); + } + + private void sendMail(Payment payment, EPaymentStatus paymentStatus) throws MessagingException { + Context context = new Context(); + context.setVariable("name", payment.getUser().getName()); + context.setVariable("payment_amount", payment.getPaymentAmount()); + context.setVariable("payment_id", payment.getId()); + context.setVariable("payment_status", payment.getPaymentStatus()); + context.setVariable("payment_date", payment.getPaymentDate()); + context.setVariable("payment_type", payment.getPaymentMethod()); + + if (paymentStatus.equals(EPaymentStatus.SUCCESS)) { + mailService.sendMail(payment.getUser().getEmail(), "Payment successful", EmailCategoriesEnum.PAYMENT_SUCCESS.getType(), context); + } else if (paymentStatus.equals(EPaymentStatus.REFUNDED)) { + mailService.sendMail(payment.getUser().getEmail(), "Payment failed", EmailCategoriesEnum.PAYMENT_REFUND.getType(), context); + } + } +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentStatusUpdateDTO.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentStatusUpdateDTO.java new file mode 100755 index 0000000..34d1c5f --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/payment/PaymentStatusUpdateDTO.java @@ -0,0 +1,22 @@ +package com.lcaohoanq.shoppe.domain.payment; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PaymentStatusUpdateDTO { + @NotBlank(message = "Payment status is required") + @Pattern(regexp = "SUCCESS|REFUNDED|PENDING", message = "Status must be either SUCCESS, REFUNDED or PENDING") + @JsonProperty("status") + String status; +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/product/Product.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/product/Product.java index a69acd0..cdfe424 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/product/Product.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/product/Product.java @@ -1,8 +1,10 @@ package com.lcaohoanq.shoppe.domain.product; +import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonProperty; import com.lcaohoanq.shoppe.domain.cart.CartItem; import com.lcaohoanq.shoppe.domain.category.Category; +import com.lcaohoanq.shoppe.domain.inventory.Inventory; import com.lcaohoanq.shoppe.enums.ProductStatus; import com.lcaohoanq.shoppe.domain.user.User; import com.lcaohoanq.shoppe.base.entity.BaseEntity; @@ -69,6 +71,11 @@ public class Product extends BaseEntity { @JoinColumn(name = "shop_owner_id") private User shopOwner; + @ManyToOne + @JoinColumn(name = "inventory_id") + @JsonBackReference + private Inventory inventory; + @OneToMany(mappedBy = "product") private List images = new ArrayList<>(); diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CreateNewSubcategoryResponse.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/CreateNewSubcategoryResponse.java similarity index 60% rename from SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CreateNewSubcategoryResponse.java rename to SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/CreateNewSubcategoryResponse.java index 9368a87..5f52b7c 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/CreateNewSubcategoryResponse.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/CreateNewSubcategoryResponse.java @@ -1,6 +1,7 @@ -package com.lcaohoanq.shoppe.domain.category; +package com.lcaohoanq.shoppe.domain.subcategory; import com.fasterxml.jackson.annotation.JsonProperty; +import com.lcaohoanq.shoppe.domain.category.CategoryResponse; public record CreateNewSubcategoryResponse( @JsonProperty("category") CategoryResponse categoryResponse diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/Subcategory.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/Subcategory.java similarity index 93% rename from SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/Subcategory.java rename to SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/Subcategory.java index b7b5bb0..9bbfde4 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/Subcategory.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/Subcategory.java @@ -1,9 +1,10 @@ -package com.lcaohoanq.shoppe.domain.category; +package com.lcaohoanq.shoppe.domain.subcategory; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.lcaohoanq.shoppe.base.entity.BaseEntity; +import com.lcaohoanq.shoppe.domain.category.Category; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryDTO.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryDTO.java similarity index 88% rename from SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryDTO.java rename to SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryDTO.java index fc95611..d9502c2 100755 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryDTO.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryDTO.java @@ -1,4 +1,4 @@ -package com.lcaohoanq.shoppe.domain.category; +package com.lcaohoanq.shoppe.domain.subcategory; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotEmpty; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryRepository.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryRepository.java similarity index 89% rename from SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryRepository.java rename to SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryRepository.java index c2f8aac..8b429b4 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryRepository.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryRepository.java @@ -1,4 +1,4 @@ -package com.lcaohoanq.shoppe.domain.category; +package com.lcaohoanq.shoppe.domain.subcategory; import java.util.List; import java.util.Optional; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryResponse.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryResponse.java similarity index 75% rename from SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryResponse.java rename to SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryResponse.java index c9a5312..b40c199 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/category/SubcategoryResponse.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/subcategory/SubcategoryResponse.java @@ -1,4 +1,4 @@ -package com.lcaohoanq.shoppe.domain.category; +package com.lcaohoanq.shoppe.domain.subcategory; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/IUserService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/IUserService.java index 283ca06..cb81c61 100755 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/IUserService.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/IUserService.java @@ -31,9 +31,7 @@ public interface IUserService { @Transactional User updateUserBalance(Long userId, Long payment) throws Exception; - - void updateAccountBalance(Long userId, Long payment) throws Exception; - + void bannedUser(Long userId) throws DataNotFoundException; void updatePassword(UpdatePasswordDTO updatePasswordDTO) throws Exception; diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/UserController.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/UserController.java index a929907..eeaefa0 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/UserController.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/UserController.java @@ -69,24 +69,6 @@ public ResponseEntity takeUserDetailsFromToken() throws Exception toUserResponse(userService.findByUsername(userDetails.getUsername()))); } - // PUT: localhost:4000/api/v1/users/4/deposit/100 -// Header: Authorization Bearer token - @PutMapping("/{userId}/deposit/{payment}") - @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_MEMBER', 'ROLE_STAFF', 'ROLE_SHOP_OWNER')") - public ResponseEntity deposit( - @PathVariable long userId, - @PathVariable long payment - ) throws Exception { - - if (payment <= 0) { - throw new MalformDataException("Payment must be greater than 0."); - } - - userService.updateAccountBalance(userId, payment); - return ResponseEntity.ok().body("Successfully deposited."); - - } - @PutMapping("/details/{userId}") @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_MEMBER', 'ROLE_STAFF', 'ROLE_SHOP_OWNER')") public ResponseEntity> updateUserDetails( diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/UserService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/UserService.java index 09043e4..b086bcb 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/UserService.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/user/UserService.java @@ -225,43 +225,6 @@ public User updateUserBalance(Long userId, Long payment) throws Exception { return null; } - @Transactional - @Retryable( - retryFor = {MessagingException.class}, // Retry only for specific exceptions - maxAttempts = 3, // Maximum retry attempts - backoff = @Backoff(delay = 2000) // 2 seconds delay between retries - ) - @Override - public void updateAccountBalance(Long userId, Long payment) throws Exception { - User existingUser = userRepository.findById(userId) - .orElseThrow(() -> new DataNotFoundException( - localizationUtils.getLocalizedMessage(MessageKey.USER_NOT_FOUND) - )); - existingUser.getWallet().setBalance(existingUser.getWallet().getBalance() + payment); - - Context context = new Context(); - context.setVariable("name", existingUser.getName()); - context.setVariable("amount", payment); - context.setVariable("balance", existingUser.getWallet().getBalance()); - - try { - mailService.sendMail( - existingUser.getEmail(), - "Account balance updated", - EmailCategoriesEnum.BALANCE_FLUCTUATION.getType(), - context - ); - } catch (MessagingException e) { - log.error("Failed to send email to {}", existingUser.getEmail(), e); - throw new MessagingException(String.format("Failed to send email to %s", existingUser.getEmail())); - } - - log.info("User {} balance updated. New balance: {}", userId, existingUser.getWallet().getBalance()); - userRepository.save(existingUser); - } - - - @Override public void bannedUser(Long userId) throws DataNotFoundException { User user = userRepository.findById(userId) diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/IWalletService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/IWalletService.java index 0396b17..8521891 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/IWalletService.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/IWalletService.java @@ -1,5 +1,10 @@ package com.lcaohoanq.shoppe.domain.wallet; +import com.lcaohoanq.shoppe.domain.wallet.WalletDTO.WalletResponse; + public interface IWalletService { + WalletResponse getByUserId(Long userId); + void updateAccountBalance(Long userId, Long payment) throws Exception; + } diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletController.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletController.java new file mode 100644 index 0000000..9bbaaf2 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletController.java @@ -0,0 +1,71 @@ +package com.lcaohoanq.shoppe.domain.wallet; + +import com.lcaohoanq.shoppe.api.ApiResponse; +import com.lcaohoanq.shoppe.domain.user.IUserService; +import com.lcaohoanq.shoppe.domain.user.User; +import com.lcaohoanq.shoppe.domain.wallet.WalletDTO.WalletResponse; +import com.lcaohoanq.shoppe.exception.MalformDataException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("${api.prefix}/wallets") +@RequiredArgsConstructor +public class WalletController { + + private final IWalletService walletService; + private final IUserService userService; + + @GetMapping("/users") + @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_MEMBER', 'ROLE_STAFF', 'ROLE_SHOP_OWNER')") + public ResponseEntity> getWalletByUserId() { + + UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext() + .getAuthentication().getPrincipal(); + User user = userService.findByUsername(userDetails.getUsername()); + + return ResponseEntity.ok().body( + ApiResponse.builder() + .message("Get wallet by user id successfully") + .statusCode(HttpStatus.OK.value()) + .isSuccess(true) + .data(walletService.getByUserId(user.getId())) + .build() + ); + } + + // PUT: localhost:4000/api/v1/users/4/deposit/100 +// Header: Authorization Bearer token + @PutMapping("/{userId}/deposit/{payment}") + @PreAuthorize("hasAnyRole('ROLE_MANAGER', 'ROLE_MEMBER', 'ROLE_STAFF', 'ROLE_SHOP_OWNER')") + public ResponseEntity> deposit( + @PathVariable long userId, + @PathVariable long payment + ) throws Exception { + + if (payment <= 0) { + throw new MalformDataException("Payment must be greater than 0."); + } + + walletService.updateAccountBalance(userId, payment); + + return ResponseEntity.ok().body( + ApiResponse.builder() + .message("Deposit successfully") + .statusCode(HttpStatus.OK.value()) + .isSuccess(true) + .build() + ); + + } + +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletDTO.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletDTO.java index 2eb1910..9ebc3a4 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletDTO.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletDTO.java @@ -1,18 +1,28 @@ package com.lcaohoanq.shoppe.domain.wallet; import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.time.LocalDateTime; public interface WalletDTO { @JsonPropertyOrder({ "id", - "balance" + "balance", + "created_at", + "updated_at" }) record WalletResponse( Long id, Float balance, - @JsonBackReference Long userId + @JsonProperty("created_at") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSSSSS") + LocalDateTime createdAt, + @JsonProperty("updated_at") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSSSSS") + LocalDateTime updatedAt ) {} record WalletRequest( diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletRepository.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletRepository.java index 49e7e8f..0db9795 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletRepository.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletRepository.java @@ -4,4 +4,6 @@ public interface WalletRepository extends JpaRepository { + Wallet findByUserId(Long userId); + } diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletService.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletService.java index dd01933..5acdc7f 100644 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletService.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/domain/wallet/WalletService.java @@ -1,5 +1,74 @@ package com.lcaohoanq.shoppe.domain.wallet; -public class WalletService implements IWalletService{ +import com.lcaohoanq.shoppe.base.exception.DataNotFoundException; +import com.lcaohoanq.shoppe.component.LocalizationUtils; +import com.lcaohoanq.shoppe.constant.MessageKey; +import com.lcaohoanq.shoppe.domain.mail.IMailService; +import com.lcaohoanq.shoppe.domain.user.IUserService; +import com.lcaohoanq.shoppe.domain.user.User; +import com.lcaohoanq.shoppe.domain.user.UserRepository; +import com.lcaohoanq.shoppe.domain.wallet.WalletDTO.WalletResponse; +import com.lcaohoanq.shoppe.enums.EmailCategoriesEnum; +import com.lcaohoanq.shoppe.util.DTOConverter; +import jakarta.mail.MessagingException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thymeleaf.context.Context; +@Service +@Slf4j +@RequiredArgsConstructor +public class WalletService implements IWalletService, DTOConverter { + + private final WalletRepository walletRepository; + private final IUserService userService; + private final UserRepository userRepository; + private final LocalizationUtils localizationUtils; + private final IMailService mailService; + + @Override + public WalletResponse getByUserId(Long userId) { + User existedUser = userService.findUserById(userId); + return toWalletResponse(walletRepository.findByUserId(existedUser.getId())); + } + + @Transactional + @Retryable( + retryFor = {MessagingException.class}, // Retry only for specific exceptions + maxAttempts = 3, // Maximum retry attempts + backoff = @Backoff(delay = 2000) // 2 seconds delay between retries + ) + @Override + public void updateAccountBalance(Long userId, Long payment) throws Exception { + User existingUser = userRepository.findById(userId) + .orElseThrow(() -> new DataNotFoundException( + localizationUtils.getLocalizedMessage(MessageKey.USER_NOT_FOUND) + )); + existingUser.getWallet().setBalance(existingUser.getWallet().getBalance() + payment); + + Context context = new Context(); + context.setVariable("name", existingUser.getName()); + context.setVariable("amount", payment); + context.setVariable("balance", existingUser.getWallet().getBalance()); + + try { + mailService.sendMail( + existingUser.getEmail(), + "Account balance updated", + EmailCategoriesEnum.BALANCE_FLUCTUATION.getType(), + context + ); + } catch (MessagingException e) { + log.error("Failed to send email to {}", existingUser.getEmail(), e); + throw new MessagingException(String.format("Failed to send email to %s", existingUser.getEmail())); + } + + log.info("User {} balance updated. New balance: {}", userId, existingUser.getWallet().getBalance()); + userRepository.save(existingUser); + } + } diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/enums/EPaymentStatus.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/enums/EPaymentStatus.java new file mode 100755 index 0000000..b0aa776 --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/enums/EPaymentStatus.java @@ -0,0 +1,15 @@ +package com.lcaohoanq.shoppe.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum EPaymentStatus { + PENDING("PENDING"), + SUCCESS("SUCCESS"), + REFUNDED("REFUNDED"), + ALL("ALL"); //for searching + + private final String status; +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/enums/EPaymentType.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/enums/EPaymentType.java new file mode 100755 index 0000000..43b720e --- /dev/null +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/enums/EPaymentType.java @@ -0,0 +1,13 @@ +package com.lcaohoanq.shoppe.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum EPaymentType { + DEPOSIT("DEPOSIT"), + ORDER("ORDER"), + DRAW_OUT("DRAW_OUT"); + private final String type; +} diff --git a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/util/DTOConverter.java b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/util/DTOConverter.java index 1cd3667..1be82ee 100755 --- a/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/util/DTOConverter.java +++ b/SPCServer/springboot/src/main/java/com/lcaohoanq/shoppe/util/DTOConverter.java @@ -6,7 +6,7 @@ import com.lcaohoanq.shoppe.domain.cart.CartResponse; import com.lcaohoanq.shoppe.domain.category.Category; import com.lcaohoanq.shoppe.domain.category.CategoryResponse; -import com.lcaohoanq.shoppe.domain.category.Subcategory; +import com.lcaohoanq.shoppe.domain.subcategory.Subcategory; import com.lcaohoanq.shoppe.domain.order.Order; import com.lcaohoanq.shoppe.domain.order.OrderDetail; import com.lcaohoanq.shoppe.domain.order.OrderDetailResponse; @@ -19,6 +19,8 @@ import com.lcaohoanq.shoppe.domain.role.RoleResponse; import com.lcaohoanq.shoppe.domain.user.User; import com.lcaohoanq.shoppe.domain.user.UserResponse; +import com.lcaohoanq.shoppe.domain.wallet.Wallet; +import com.lcaohoanq.shoppe.domain.wallet.WalletDTO.WalletResponse; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -162,5 +164,14 @@ default CartItemResponse toCartItemResponse(CartItem cartItem){ cartItem.getUpdatedAt() ); } + + default WalletResponse toWalletResponse(Wallet wallet){ + return new WalletResponse( + wallet.getId(), + wallet.getBalance(), + wallet.getCreatedAt(), + wallet.getUpdatedAt() + ); + } } diff --git a/SPCServer/springboot/src/main/resources/application.yml b/SPCServer/springboot/src/main/resources/application.yml index c636380..afcc28a 100644 --- a/SPCServer/springboot/src/main/resources/application.yml +++ b/SPCServer/springboot/src/main/resources/application.yml @@ -125,4 +125,15 @@ management: metrics: enabled: true prometheus: - enabled: true \ No newline at end of file + enabled: true + +vnpay: + api: + vnp_Version: ${VNPAY_VERSION} + vnp_Command: ${VNPAY_COMMAND} + vnp_OrderType: ${VNPAY_ORDER_TYPE} + vnp_PayUrl: ${VNPAY_PAY_URL} + vnp_Returnurl: ${VNPAY_RETURN_URL} + vnp_TmnCode: ${VNPAY_TMN_CODE} + vnp_HashSecret: ${VNPAY_HASH_SECRET} + vnp_apiUrl: ${VNPAY_API_URL} \ No newline at end of file