From 45060e1cfb90485404b47a0d7da8ed7cc87e93a0 Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 30 Dec 2024 19:31:25 +0100 Subject: [PATCH 01/57] total project restructure --- .../CommercifyApplication.java | 2 +- .../orders/OrderStatusUpdateRequest.java | 4 - .../repository/AddressRepository.java | 7 -- .../ConfirmationTokenRepository.java | 13 --- .../repository/OrderLineRepository.java | 54 ---------- .../repository/OrderRepository.java | 47 -------- .../repository/PaymentRepository.java | 14 --- .../repository/ProductVariantRepository.java | 10 -- .../commercify/repository/UserRepository.java | 12 --- .../order/OrderCalculationService.java | 17 --- .../commercify/viewmodel/PriceViewModel.java | 6 -- .../component/AdminDataLoader.java | 12 +-- .../factory/ProductFactory.java | 10 +- .../flow/OrderStateFlow.java | 4 +- .../flow/PaymentStateFlow.java | 4 +- .../config/ApplicationConfiguration.java | 4 +- .../config/JwtAuthenticationFilter.java | 6 +- .../config/SecurityConfig.java | 2 +- .../enums}/OrderStatus.java | 2 +- .../enums}/PaymentProvider.java | 2 +- .../enums}/PaymentStatus.java | 2 +- .../model/Address.java} | 4 +- .../model/ConfirmationToken.java} | 6 +- .../model/Order.java} | 10 +- .../model/OrderLine.java} | 10 +- .../model}/OrderShippingInfo.java | 2 +- .../model/Payment.java} | 10 +- .../model/Product.java} | 8 +- .../model/ProductVariant.java} | 10 +- .../model/User.java} | 8 +- .../model/VariantOption.java} | 6 +- .../exception/GlobalExceptionHandler.java | 2 +- .../exception/InsufficientStockException.java | 2 +- .../exception/InvalidSortFieldException.java | 2 +- .../exception/OrderNotFoundException.java | 2 +- .../exception/OrderValidationException.java | 2 +- .../exception/PaymentProcessingException.java | 2 +- .../exception/PriceNotFoundException.java | 2 +- .../exception/ProductDeletionException.java | 4 +- .../exception/ProductNotFoundException.java | 2 +- .../exception/ProductValidationException.java | 2 +- .../exception/StripeOperationException.java | 2 +- .../repository/AddressRepository.java | 7 ++ .../ConfirmationTokenRepository.java | 13 +++ .../repository/OrderLineRepository.java | 32 ++++++ .../repository/OrderRepository.java | 14 +++ .../OrderShippingInfoRepository.java | 4 +- .../repository/PaymentRepository.java | 14 +++ .../repository/ProductRepository.java | 8 +- .../repository/ProductVariantRepository.java | 10 ++ .../commercify/repository/UserRepository.java | 12 +++ .../ProductDeletionService.java | 24 ++--- .../ProductVariantService.java | 59 ++++++----- .../service/StockManagementService.java | 30 +++--- .../AuthenticationService.java | 28 ++--- .../authentication}/JwtService.java | 4 +- .../order => service/core}/OrderService.java | 100 ++++++++++-------- .../core}/PaymentService.java | 21 ++-- .../core}/ProductService.java | 41 +++---- .../core}/UserManagementService.java | 34 +++--- .../email/EmailConfirmationService.java | 22 ++-- .../service/email/EmailService.java | 12 +-- .../mobilepay/MobilePayController.java | 6 +- .../mobilepay/MobilePayService.java | 36 +++---- .../mobilepay/MobilePayTokenService.java | 4 +- .../integration/stripe/StripeConfig.java | 2 +- .../integration/stripe/StripeController.java | 6 +- .../integration/stripe/StripeService.java | 42 ++++---- .../stripe/StripeWebhookHandler.java | 22 ++-- .../validations}/OrderValidationService.java | 20 ++-- .../ProductValidationService.java | 26 ++--- .../controller/AuthenticationController.java | 14 +-- .../controller/OrderController.java | 26 ++--- .../controller/PaymentController.java | 6 +- .../controller/ProductController.java | 36 +++---- .../controller/UserManagementController.java | 8 +- .../dto => web/dto/common}/AddressDTO.java | 2 +- .../dto/common}/CustomerDetailsDTO.java | 2 +- .../dto => web/dto/common}/OrderDTO.java | 4 +- .../dto/common}/OrderDetailsDTO.java | 2 +- .../dto => web/dto/common}/OrderLineDTO.java | 2 +- .../dto => web/dto/common}/ProductDTO.java | 2 +- .../ProductDeletionValidationResult.java | 2 +- .../dto/common}/ProductUpdateResult.java | 2 +- .../dto/common}/ProductVariantEntityDto.java | 5 +- .../dto => web/dto/common}/UserDTO.java | 2 +- .../dto/common}/VariantOptionEntityDto.java | 5 +- .../dto/mapper/AddressMapper.java | 10 +- .../dto/mapper/OrderLineMapper.java | 10 +- .../dto/mapper/OrderMapper.java | 10 +- .../dto/mapper/ProductMapper.java | 12 +-- .../dto/mapper/ProductVariantMapper.java | 12 +-- .../dto/mapper/UserMapper.java | 10 +- .../dto/mapper/VariantOptionMapper.java | 10 +- .../dto/request/auth}/LoginUserRequest.java | 2 +- .../request/auth}/RegisterUserRequest.java | 4 +- .../order}/CreateOrderLineRequest.java | 2 +- .../request/order}/CreateOrderRequest.java | 6 +- .../order/OrderStatusUpdateRequest.java | 4 + .../dto/request/payment}/PaymentRequest.java | 2 +- .../product}/CreateVariantOptionRequest.java | 2 +- .../dto/request/product}/PriceRequest.java | 2 +- .../dto/request/product}/ProductRequest.java | 2 +- .../product}/ProductVariantRequest.java | 2 +- .../dto/response}/ErrorResponse.java | 2 +- .../response}/ValidationErrorResponse.java | 2 +- .../dto/response/auth}/AuthResponse.java | 4 +- .../dto/response/auth}/JwtErrorResponse.java | 2 +- .../response/auth}/RegisterUserResponse.java | 4 +- .../response/order}/CreateOrderResponse.java | 4 +- .../dto/response/order}/GetOrderResponse.java | 10 +- .../payment}/CancelPaymentResponse.java | 2 +- .../response/payment}/PaymentResponse.java | 4 +- .../ProductDeletionErrorResponse.java | 4 +- .../product}/ProductUpdateResponse.java | 4 +- .../viewmodel/OrderLineViewModel.java | 10 +- .../viewmodel/OrderViewModel.java | 6 +- .../web/viewmodel/PriceViewModel.java | 6 ++ .../viewmodel/ProductVariantViewModel.java | 6 +- .../viewmodel/ProductViewModel.java | 10 +- .../viewmodel/VariantOptionViewModel.java | 4 +- .../dto/mapper/OrderMapperTest.java | 47 ++++---- .../entity/OrderEntityTest.java | 16 +-- .../entity/ProductEntityTest.java | 9 +- .../factory/ProductFactoryTest.java | 30 ++---- .../flow/OrderStateFlowTest.java | 8 +- .../mobilepay/MobilePayServiceTest.java | 20 ++-- .../service/AuthenticationServiceTest.java | 57 +++++----- .../service/PaymentServiceTest.java | 31 +++--- .../service/UserAddressServiceTest.java | 39 +++---- .../service/order/OrderServiceTest.java | 82 +++++++------- .../service/product/ProductServiceTest.java | 32 +++--- 132 files changed, 800 insertions(+), 862 deletions(-) rename src/main/java/com/zenfulcode/commercify/{commercify => }/CommercifyApplication.java (86%) delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/OrderStatusUpdateRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/repository/AddressRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/repository/ConfirmationTokenRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/repository/OrderLineRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/repository/OrderRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/repository/PaymentRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/repository/ProductVariantRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/repository/UserRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderCalculationService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/commercify/viewmodel/PriceViewModel.java rename src/main/java/com/zenfulcode/commercify/{commercify => }/component/AdminDataLoader.java (81%) rename src/main/java/com/zenfulcode/commercify/{commercify => component}/factory/ProductFactory.java (61%) rename src/main/java/com/zenfulcode/commercify/{commercify => component}/flow/OrderStateFlow.java (93%) rename src/main/java/com/zenfulcode/commercify/{commercify => component}/flow/PaymentStateFlow.java (93%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/config/ApplicationConfiguration.java (93%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/config/JwtAuthenticationFilter.java (94%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/config/SecurityConfig.java (98%) rename src/main/java/com/zenfulcode/commercify/{commercify => domain/enums}/OrderStatus.java (88%) rename src/main/java/com/zenfulcode/commercify/{commercify => domain/enums}/PaymentProvider.java (53%) rename src/main/java/com/zenfulcode/commercify/{commercify => domain/enums}/PaymentStatus.java (74%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/AddressEntity.java => domain/model/Address.java} (91%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/ConfirmationTokenEntity.java => domain/model/ConfirmationToken.java} (89%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/OrderEntity.java => domain/model/Order.java} (90%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/OrderLineEntity.java => domain/model/OrderLine.java} (90%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity => domain/model}/OrderShippingInfo.java (96%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/PaymentEntity.java => domain/model/Payment.java} (88%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/ProductEntity.java => domain/model/Product.java} (88%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/ProductVariantEntity.java => domain/model/ProductVariant.java} (82%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/UserEntity.java => domain/model/User.java} (91%) rename src/main/java/com/zenfulcode/commercify/{commercify/entity/VariantOptionEntity.java => domain/model/VariantOption.java} (85%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/GlobalExceptionHandler.java (97%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/InsufficientStockException.java (73%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/InvalidSortFieldException.java (84%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/OrderNotFoundException.java (75%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/OrderValidationException.java (84%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/PaymentProcessingException.java (76%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/PriceNotFoundException.java (75%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/ProductDeletionException.java (78%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/ProductNotFoundException.java (76%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/ProductValidationException.java (85%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/exception/StripeOperationException.java (75%) create mode 100644 src/main/java/com/zenfulcode/commercify/repository/AddressRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/repository/ConfirmationTokenRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/repository/OrderLineRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/repository/OrderRepository.java rename src/main/java/com/zenfulcode/commercify/{commercify => }/repository/OrderShippingInfoRepository.java (55%) create mode 100644 src/main/java/com/zenfulcode/commercify/repository/PaymentRepository.java rename src/main/java/com/zenfulcode/commercify/{commercify => }/repository/ProductRepository.java (58%) create mode 100644 src/main/java/com/zenfulcode/commercify/repository/ProductVariantRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/repository/UserRepository.java rename src/main/java/com/zenfulcode/commercify/{commercify/service/product => service}/ProductDeletionService.java (68%) rename src/main/java/com/zenfulcode/commercify/{commercify/service/product => service}/ProductVariantService.java (62%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/service/StockManagementService.java (57%) rename src/main/java/com/zenfulcode/commercify/{commercify/service => service/authentication}/AuthenticationService.java (75%) rename src/main/java/com/zenfulcode/commercify/{commercify/service => service/authentication}/JwtService.java (95%) rename src/main/java/com/zenfulcode/commercify/{commercify/service/order => service/core}/OrderService.java (73%) rename src/main/java/com/zenfulcode/commercify/{commercify/service => service/core}/PaymentService.java (73%) rename src/main/java/com/zenfulcode/commercify/{commercify/service/product => service/core}/ProductService.java (70%) rename src/main/java/com/zenfulcode/commercify/{commercify/service => service/core}/UserManagementService.java (74%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/service/email/EmailConfirmationService.java (71%) rename src/main/java/com/zenfulcode/commercify/{commercify => }/service/email/EmailService.java (93%) rename src/main/java/com/zenfulcode/commercify/{commercify => service}/integration/mobilepay/MobilePayController.java (90%) rename src/main/java/com/zenfulcode/commercify/{commercify => service}/integration/mobilepay/MobilePayService.java (85%) rename src/main/java/com/zenfulcode/commercify/{commercify => service}/integration/mobilepay/MobilePayTokenService.java (96%) rename src/main/java/com/zenfulcode/commercify/{commercify => service}/integration/stripe/StripeConfig.java (90%) rename src/main/java/com/zenfulcode/commercify/{commercify => service}/integration/stripe/StripeController.java (84%) rename src/main/java/com/zenfulcode/commercify/{commercify => service}/integration/stripe/StripeService.java (70%) rename src/main/java/com/zenfulcode/commercify/{commercify => service}/integration/stripe/StripeWebhookHandler.java (70%) rename src/main/java/com/zenfulcode/commercify/{commercify/service/order => service/validations}/OrderValidationService.java (83%) rename src/main/java/com/zenfulcode/commercify/{commercify/service/product => service/validations}/ProductValidationService.java (68%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/controller/AuthenticationController.java (82%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/controller/OrderController.java (91%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/controller/PaymentController.java (87%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/controller/ProductController.java (90%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/controller/UserManagementController.java (93%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/AddressDTO.java (85%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/CustomerDetailsDTO.java (83%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/OrderDTO.java (78%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/OrderDetailsDTO.java (88%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/OrderLineDTO.java (88%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/ProductDTO.java (89%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/ProductDeletionValidationResult.java (87%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/ProductUpdateResult.java (88%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/ProductVariantEntityDto.java (68%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/UserDTO.java (88%) rename src/main/java/com/zenfulcode/commercify/{commercify/dto => web/dto/common}/VariantOptionEntityDto.java (61%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/dto/mapper/AddressMapper.java (58%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/dto/mapper/OrderLineMapper.java (56%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/dto/mapper/OrderMapper.java (71%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/dto/mapper/ProductMapper.java (69%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/dto/mapper/ProductVariantMapper.java (70%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/dto/mapper/UserMapper.java (75%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/dto/mapper/VariantOptionMapper.java (59%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests => web/dto/request/auth}/LoginUserRequest.java (53%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests => web/dto/request/auth}/RegisterUserRequest.java (80%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests/orders => web/dto/request/order}/CreateOrderLineRequest.java (63%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests/orders => web/dto/request/order}/CreateOrderRequest.java (57%) create mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/order/OrderStatusUpdateRequest.java rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests => web/dto/request/payment}/PaymentRequest.java (80%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests/products => web/dto/request/product}/CreateVariantOptionRequest.java (56%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests/products => web/dto/request/product}/PriceRequest.java (54%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests/products => web/dto/request/product}/ProductRequest.java (78%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/requests/products => web/dto/request/product}/ProductVariantRequest.java (75%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses => web/dto/response}/ErrorResponse.java (70%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses => web/dto/response}/ValidationErrorResponse.java (75%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses => web/dto/response/auth}/AuthResponse.java (77%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses/products => web/dto/response/auth}/JwtErrorResponse.java (88%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses => web/dto/response/auth}/RegisterUserResponse.java (75%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses/orders => web/dto/response/order}/CreateOrderResponse.java (72%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses/orders => web/dto/response/order}/GetOrderResponse.java (73%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses => web/dto/response/payment}/CancelPaymentResponse.java (91%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses => web/dto/response/payment}/PaymentResponse.java (64%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses/products => web/dto/response/product}/ProductDeletionErrorResponse.java (66%) rename src/main/java/com/zenfulcode/commercify/{commercify/api/responses/products => web/dto/response/product}/ProductUpdateResponse.java (52%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/viewmodel/OrderLineViewModel.java (75%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/viewmodel/OrderViewModel.java (79%) create mode 100644 src/main/java/com/zenfulcode/commercify/web/viewmodel/PriceViewModel.java rename src/main/java/com/zenfulcode/commercify/{commercify => web}/viewmodel/ProductVariantViewModel.java (82%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/viewmodel/ProductViewModel.java (85%) rename src/main/java/com/zenfulcode/commercify/{commercify => web}/viewmodel/VariantOptionViewModel.java (76%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/dto/mapper/OrderMapperTest.java (60%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/entity/OrderEntityTest.java (83%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/entity/ProductEntityTest.java (86%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/factory/ProductFactoryTest.java (75%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/flow/OrderStateFlowTest.java (95%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/integration/mobilepay/MobilePayServiceTest.java (82%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/service/AuthenticationServiceTest.java (77%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/service/PaymentServiceTest.java (84%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/service/UserAddressServiceTest.java (74%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/service/order/OrderServiceTest.java (74%) rename src/test/java/com/zenfulcode/commercify/{commercify => }/service/product/ProductServiceTest.java (87%) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/CommercifyApplication.java b/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java similarity index 86% rename from src/main/java/com/zenfulcode/commercify/commercify/CommercifyApplication.java rename to src/main/java/com/zenfulcode/commercify/CommercifyApplication.java index 96b8800..6e73c3e 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/CommercifyApplication.java +++ b/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify; +package com.zenfulcode.commercify; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/OrderStatusUpdateRequest.java b/src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/OrderStatusUpdateRequest.java deleted file mode 100644 index df6b590..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/OrderStatusUpdateRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.zenfulcode.commercify.commercify.api.requests.orders; - -public record OrderStatusUpdateRequest(String status) { -} diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/AddressRepository.java b/src/main/java/com/zenfulcode/commercify/commercify/repository/AddressRepository.java deleted file mode 100644 index b9ee77e..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/AddressRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.commercify.repository; - -import com.zenfulcode.commercify.commercify.entity.AddressEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface AddressRepository extends JpaRepository { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/ConfirmationTokenRepository.java b/src/main/java/com/zenfulcode/commercify/commercify/repository/ConfirmationTokenRepository.java deleted file mode 100644 index 22c6116..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/ConfirmationTokenRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.zenfulcode.commercify.commercify.repository; - -import com.zenfulcode.commercify.commercify.entity.ConfirmationTokenEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface ConfirmationTokenRepository extends JpaRepository { - Optional findByToken(String token); - Optional findByUserIdAndConfirmedFalse(Long userId); -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/OrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/commercify/repository/OrderLineRepository.java deleted file mode 100644 index dbc4637..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/OrderLineRepository.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.zenfulcode.commercify.commercify.repository; - -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.OrderLineEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.Collection; -import java.util.List; -import java.util.Set; - -@Repository -public interface OrderLineRepository extends JpaRepository { - @Query("SELECT DISTINCT ol.order FROM OrderLineEntity ol " + - "WHERE ol.productId = :productId " + - "AND ol.order.status IN :statuses") - Set findActiveOrdersForProduct( - @Param("productId") Long productId, - @Param("statuses") Collection statuses - ); - - @Query("SELECT DISTINCT ol.order FROM OrderLineEntity ol " + - "JOIN ol.productVariant v " + - "WHERE v.id = :variantId " + - "AND ol.order.status IN :statuses") - Set findActiveOrdersForVariant( - @Param("variantId") Long variantId, - @Param("statuses") Collection statuses - ); - - @Query("SELECT ol FROM OrderLineEntity ol " + - "WHERE ol.order.id = :orderId") - List findByOrderId(@Param("orderId") Long orderId); - - @Query("SELECT COUNT(ol) > 0 FROM OrderLineEntity ol " + - "WHERE ol.productId = :productId " + - "AND ol.order.status IN :statuses") - boolean hasActiveOrders( - @Param("productId") Long productId, - @Param("statuses") Collection statuses - ); - - @Query("SELECT COUNT(ol) > 0 FROM OrderLineEntity ol " + - "JOIN ol.productVariant v " + - "WHERE v.id = :variantId " + - "AND ol.order.status IN :statuses") - boolean hasActiveOrdersForVariant( - @Param("variantId") Long variantId, - @Param("statuses") Collection statuses - ); -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/OrderRepository.java b/src/main/java/com/zenfulcode/commercify/commercify/repository/OrderRepository.java deleted file mode 100644 index 6115ecd..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/OrderRepository.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.zenfulcode.commercify.commercify.repository; - -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -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; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; -import org.springframework.web.bind.annotation.PathVariable; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -@Repository -public interface OrderRepository extends JpaRepository { - Page findByUserId(Long userId, Pageable pageable); - - boolean existsByIdAndUserId(Long id, Long userId); - - @Query("SELECT DISTINCT o FROM OrderEntity o " + - "JOIN FETCH o.orderLines ol " + - "WHERE o.id = :orderId") - Optional findByIdWithOrderLines(@PathVariable Long orderId); - - @Query("SELECT o FROM OrderEntity o " + - "WHERE o.userId = :userId " + - "AND o.status IN :statuses") - List findByUserIdAndStatusIn( - @Param("userId") Long userId, - @Param("statuses") Collection statuses - ); - - @Query("SELECT o FROM OrderEntity o " + - "WHERE o.status IN :statuses") - List findByStatusIn(@Param("statuses") Collection statuses); - - @Query("SELECT COUNT(o) > 0 FROM OrderEntity o " + - "WHERE o.status IN :statuses " + - "AND o.userId = :userId") - boolean hasOrdersInStatus( - @Param("userId") Long userId, - @Param("statuses") Collection statuses - ); -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/PaymentRepository.java b/src/main/java/com/zenfulcode/commercify/commercify/repository/PaymentRepository.java deleted file mode 100644 index 5fe09fb..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/PaymentRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.zenfulcode.commercify.commercify.repository; - -import com.zenfulcode.commercify.commercify.entity.PaymentEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface PaymentRepository extends JpaRepository { - Optional findByOrderId(Long orderId); - - Optional findByMobilePayReference(String mobilePayReference); -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/ProductVariantRepository.java b/src/main/java/com/zenfulcode/commercify/commercify/repository/ProductVariantRepository.java deleted file mode 100644 index aedd2de..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/ProductVariantRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.zenfulcode.commercify.commercify.repository; - -import com.zenfulcode.commercify.commercify.entity.ProductVariantEntity; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductVariantRepository extends JpaRepository { - Page findByProductId(Long productId, Pageable pageable); -} diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/UserRepository.java b/src/main/java/com/zenfulcode/commercify/commercify/repository/UserRepository.java deleted file mode 100644 index fb08fa2..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/UserRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zenfulcode.commercify.commercify.repository; - -import com.zenfulcode.commercify.commercify.entity.UserEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderCalculationService.java b/src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderCalculationService.java deleted file mode 100644 index b304745..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderCalculationService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.zenfulcode.commercify.commercify.service.order; - -import com.zenfulcode.commercify.commercify.entity.OrderLineEntity; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.Set; - -@Service -@AllArgsConstructor -class OrderCalculationService { - public double calculateTotalAmount(Set orderLines) { - return orderLines.stream() - .mapToDouble(line -> line.getUnitPrice() * line.getQuantity()) - .sum(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/PriceViewModel.java b/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/PriceViewModel.java deleted file mode 100644 index 93f2143..0000000 --- a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/PriceViewModel.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.zenfulcode.commercify.commercify.viewmodel; - -public record PriceViewModel(String currency, - Double amount) { - -} diff --git a/src/main/java/com/zenfulcode/commercify/commercify/component/AdminDataLoader.java b/src/main/java/com/zenfulcode/commercify/component/AdminDataLoader.java similarity index 81% rename from src/main/java/com/zenfulcode/commercify/commercify/component/AdminDataLoader.java rename to src/main/java/com/zenfulcode/commercify/component/AdminDataLoader.java index 75ad3f5..2b3b475 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/component/AdminDataLoader.java +++ b/src/main/java/com/zenfulcode/commercify/component/AdminDataLoader.java @@ -1,8 +1,8 @@ -package com.zenfulcode.commercify.commercify.component; +package com.zenfulcode.commercify.component; -import com.zenfulcode.commercify.commercify.entity.AddressEntity; -import com.zenfulcode.commercify.commercify.entity.UserEntity; -import com.zenfulcode.commercify.commercify.repository.UserRepository; +import com.zenfulcode.commercify.domain.model.Address; +import com.zenfulcode.commercify.domain.model.User; +import com.zenfulcode.commercify.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; @@ -28,7 +28,7 @@ public class AdminDataLoader { public CommandLineRunner loadData() { return args -> { if (userRepository.findByEmail(email).isEmpty()) { - AddressEntity defaultAddress = AddressEntity.builder() + Address defaultAddress = Address.builder() .street("123 Main St") .city("Springfield") .state("IL") @@ -36,7 +36,7 @@ public CommandLineRunner loadData() { .country("US") .build(); - UserEntity adminUser = UserEntity.builder() + User adminUser = User.builder() .email(email) .password(passwordEncoder.encode(password)) .firstName("Admin") diff --git a/src/main/java/com/zenfulcode/commercify/commercify/factory/ProductFactory.java b/src/main/java/com/zenfulcode/commercify/component/factory/ProductFactory.java similarity index 61% rename from src/main/java/com/zenfulcode/commercify/commercify/factory/ProductFactory.java rename to src/main/java/com/zenfulcode/commercify/component/factory/ProductFactory.java index 076da7e..c0cf26c 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/factory/ProductFactory.java +++ b/src/main/java/com/zenfulcode/commercify/component/factory/ProductFactory.java @@ -1,15 +1,15 @@ -package com.zenfulcode.commercify.commercify.factory; +package com.zenfulcode.commercify.component.factory; -import com.zenfulcode.commercify.commercify.api.requests.products.ProductRequest; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; +import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; +import com.zenfulcode.commercify.domain.model.Product; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Component @Slf4j public class ProductFactory { - public ProductEntity createFromRequest(ProductRequest request) { - return ProductEntity.builder() + public Product createFromRequest(ProductRequest request) { + return com.zenfulcode.commercify.domain.model.Product.builder() .name(request.name()) .description(request.description()) .stock(request.stock() != null ? request.stock() : 0) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlow.java b/src/main/java/com/zenfulcode/commercify/component/flow/OrderStateFlow.java similarity index 93% rename from src/main/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlow.java rename to src/main/java/com/zenfulcode/commercify/component/flow/OrderStateFlow.java index 8041fd6..649b4d8 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlow.java +++ b/src/main/java/com/zenfulcode/commercify/component/flow/OrderStateFlow.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.flow; +package com.zenfulcode.commercify.component.flow; -import com.zenfulcode.commercify.commercify.OrderStatus; +import com.zenfulcode.commercify.domain.enums.OrderStatus; import org.springframework.stereotype.Component; import java.util.EnumMap; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/flow/PaymentStateFlow.java b/src/main/java/com/zenfulcode/commercify/component/flow/PaymentStateFlow.java similarity index 93% rename from src/main/java/com/zenfulcode/commercify/commercify/flow/PaymentStateFlow.java rename to src/main/java/com/zenfulcode/commercify/component/flow/PaymentStateFlow.java index 058f5e9..b4f8d27 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/flow/PaymentStateFlow.java +++ b/src/main/java/com/zenfulcode/commercify/component/flow/PaymentStateFlow.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.flow; +package com.zenfulcode.commercify.component.flow; -import com.zenfulcode.commercify.commercify.PaymentStatus; +import com.zenfulcode.commercify.domain.enums.PaymentStatus; import org.springframework.stereotype.Component; import java.util.EnumMap; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/config/ApplicationConfiguration.java b/src/main/java/com/zenfulcode/commercify/config/ApplicationConfiguration.java similarity index 93% rename from src/main/java/com/zenfulcode/commercify/commercify/config/ApplicationConfiguration.java rename to src/main/java/com/zenfulcode/commercify/config/ApplicationConfiguration.java index b4b61a3..ce9cca6 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/config/ApplicationConfiguration.java +++ b/src/main/java/com/zenfulcode/commercify/config/ApplicationConfiguration.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.config; +package com.zenfulcode.commercify.config; -import com.zenfulcode.commercify.commercify.repository.UserRepository; +import com.zenfulcode.commercify.repository.UserRepository; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/config/JwtAuthenticationFilter.java b/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java similarity index 94% rename from src/main/java/com/zenfulcode/commercify/commercify/config/JwtAuthenticationFilter.java rename to src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java index 68649c8..2fb368a 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java @@ -1,9 +1,9 @@ -package com.zenfulcode.commercify.commercify.config; +package com.zenfulcode.commercify.config; import com.fasterxml.jackson.databind.ObjectMapper; -import com.zenfulcode.commercify.commercify.api.responses.products.JwtErrorResponse; -import com.zenfulcode.commercify.commercify.service.JwtService; +import com.zenfulcode.commercify.web.dto.response.auth.JwtErrorResponse; +import com.zenfulcode.commercify.service.authentication.JwtService; import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java similarity index 98% rename from src/main/java/com/zenfulcode/commercify/commercify/config/SecurityConfig.java rename to src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java index 29119c2..7fd57ef 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.config; +package com.zenfulcode.commercify.config; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/OrderStatus.java b/src/main/java/com/zenfulcode/commercify/domain/enums/OrderStatus.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/commercify/OrderStatus.java rename to src/main/java/com/zenfulcode/commercify/domain/enums/OrderStatus.java index c3f6dd9..e780758 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/OrderStatus.java +++ b/src/main/java/com/zenfulcode/commercify/domain/enums/OrderStatus.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify; +package com.zenfulcode.commercify.domain.enums; public enum OrderStatus { PENDING, // Order has been created but not yet confirmed diff --git a/src/main/java/com/zenfulcode/commercify/commercify/PaymentProvider.java b/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentProvider.java similarity index 53% rename from src/main/java/com/zenfulcode/commercify/commercify/PaymentProvider.java rename to src/main/java/com/zenfulcode/commercify/domain/enums/PaymentProvider.java index 5498baf..9c75ca5 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/PaymentProvider.java +++ b/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentProvider.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify; +package com.zenfulcode.commercify.domain.enums; public enum PaymentProvider { STRIPE, MOBILEPAY diff --git a/src/main/java/com/zenfulcode/commercify/commercify/PaymentStatus.java b/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentStatus.java similarity index 74% rename from src/main/java/com/zenfulcode/commercify/commercify/PaymentStatus.java rename to src/main/java/com/zenfulcode/commercify/domain/enums/PaymentStatus.java index 49d2cdb..cbf40c5 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/PaymentStatus.java +++ b/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentStatus.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify; +package com.zenfulcode.commercify.domain.enums; public enum PaymentStatus { PENDING, diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/AddressEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/Address.java similarity index 91% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/AddressEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/Address.java index 72d4e32..0fe3f32 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/AddressEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/Address.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; import jakarta.persistence.*; import lombok.*; @@ -14,7 +14,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class AddressEntity { +public class Address { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/ConfirmationTokenEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/ConfirmationToken.java similarity index 89% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/ConfirmationTokenEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/ConfirmationToken.java index 1d5ba16..11e0019 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/ConfirmationTokenEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/ConfirmationToken.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; import jakarta.persistence.*; import lombok.*; @@ -14,7 +14,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class ConfirmationTokenEntity { +public class ConfirmationToken { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,7 +30,7 @@ public class ConfirmationTokenEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) - private UserEntity user; + private User user; @CreationTimestamp @Column(nullable = false, name = "created_at") diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/OrderEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/Order.java similarity index 90% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/OrderEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/Order.java index b94ee21..2c019b6 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/OrderEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/Order.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; -import com.zenfulcode.commercify.commercify.OrderStatus; +import com.zenfulcode.commercify.domain.enums.OrderStatus; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.CreationTimestamp; @@ -24,7 +24,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class OrderEntity { +public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) @@ -36,7 +36,7 @@ public class OrderEntity { @ToString.Exclude @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default - private Set orderLines = new LinkedHashSet<>(); + private Set orderLines = new LinkedHashSet<>(); @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false) @@ -66,7 +66,7 @@ public final boolean equals(Object o) { Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; - OrderEntity that = (OrderEntity) o; + Order that = (Order) o; return getId() != null && Objects.equals(getId(), that.getId()); } diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/OrderLineEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/OrderLine.java similarity index 90% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/OrderLineEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/OrderLine.java index ccaac1d..a400df5 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/OrderLineEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/OrderLine.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; import jakarta.persistence.*; import lombok.*; @@ -13,7 +13,7 @@ @Table(name = "order_lines") @NoArgsConstructor @AllArgsConstructor -public class OrderLineEntity { +public class OrderLine { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false) @@ -33,11 +33,11 @@ public class OrderLineEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_variant_id") - private ProductVariantEntity productVariant; + private ProductVariant productVariant; @ManyToOne(optional = false) @JoinColumn(name = "order_id") - private OrderEntity order; + private Order order; @Override public final boolean equals(Object o) { @@ -46,7 +46,7 @@ public final boolean equals(Object o) { Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; - OrderLineEntity that = (OrderLineEntity) o; + OrderLine that = (OrderLine) o; return getId() != null && Objects.equals(getId(), that.getId()); } diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/OrderShippingInfo.java b/src/main/java/com/zenfulcode/commercify/domain/model/OrderShippingInfo.java similarity index 96% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/OrderShippingInfo.java rename to src/main/java/com/zenfulcode/commercify/domain/model/OrderShippingInfo.java index ba9ed24..35b0a37 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/OrderShippingInfo.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/OrderShippingInfo.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/PaymentEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/Payment.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/PaymentEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/Payment.java index 9c1c51d..3aaa9f2 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/PaymentEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/Payment.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; -import com.zenfulcode.commercify.commercify.PaymentProvider; -import com.zenfulcode.commercify.commercify.PaymentStatus; +import com.zenfulcode.commercify.domain.enums.PaymentProvider; +import com.zenfulcode.commercify.domain.enums.PaymentStatus; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.CreationTimestamp; @@ -19,7 +19,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class PaymentEntity { +public class Payment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -55,7 +55,7 @@ public final boolean equals(Object o) { Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; - PaymentEntity that = (PaymentEntity) o; + Payment that = (Payment) o; return getId() != null && Objects.equals(getId(), that.getId()); } diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/ProductEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/Product.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/ProductEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/Product.java index a828025..eeadfb6 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/ProductEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/Product.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; import jakarta.persistence.*; import lombok.*; @@ -16,7 +16,7 @@ @Setter @NoArgsConstructor @AllArgsConstructor -public class ProductEntity { +public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -35,7 +35,7 @@ public class ProductEntity { @OneToMany(mappedBy = "product", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Builder.Default - private Set variants = new HashSet<>(); + private Set variants = new HashSet<>(); @Column(name = "created_at", nullable = false) @CreationTimestamp @@ -45,7 +45,7 @@ public class ProductEntity { @UpdateTimestamp private Instant updatedAt; - public void addVariant(ProductVariantEntity variant) { + public void addVariant(ProductVariant variant) { variants.add(variant); variant.setProduct(this); } diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/ProductVariantEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/ProductVariant.java similarity index 82% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/ProductVariantEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/ProductVariant.java index bd9968f..66e2fae 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/ProductVariantEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/ProductVariant.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; import jakarta.persistence.*; @@ -17,7 +17,7 @@ @Table(name = "product_variants") @AllArgsConstructor @NoArgsConstructor -public class ProductVariantEntity { +public class ProductVariant { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -34,11 +34,11 @@ public class ProductVariantEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) - private ProductEntity product; + private Product product; @OneToMany(mappedBy = "productVariant", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default - private Set options = new HashSet<>(); + private Set options = new HashSet<>(); @Column(name = "created_at", nullable = false) @CreationTimestamp @@ -48,7 +48,7 @@ public class ProductVariantEntity { @UpdateTimestamp private Instant updatedAt; - public void addOption(VariantOptionEntity option) { + public void addOption(VariantOption option) { options.add(option); option.setProductVariant(this); } diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/UserEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/User.java similarity index 91% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/UserEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/User.java index d4f418c..ec3a2f6 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/UserEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/User.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; import jakarta.persistence.*; import lombok.*; @@ -23,7 +23,7 @@ @ToString @NoArgsConstructor @AllArgsConstructor -public class UserEntity implements UserDetails { +public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -43,7 +43,7 @@ public class UserEntity implements UserDetails { @ToString.Exclude @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "default_address_id", unique = true) - private AddressEntity defaultAddress; + private Address defaultAddress; @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "id")) @@ -55,7 +55,7 @@ public class UserEntity implements UserDetails { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @ToString.Exclude - private Set confirmationTokens = new HashSet<>(); + private Set confirmationTokens = new HashSet<>(); @Column(name = "created_at", nullable = false) @CreationTimestamp diff --git a/src/main/java/com/zenfulcode/commercify/commercify/entity/VariantOptionEntity.java b/src/main/java/com/zenfulcode/commercify/domain/model/VariantOption.java similarity index 85% rename from src/main/java/com/zenfulcode/commercify/commercify/entity/VariantOptionEntity.java rename to src/main/java/com/zenfulcode/commercify/domain/model/VariantOption.java index 2a7fbd3..5eb8713 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/entity/VariantOptionEntity.java +++ b/src/main/java/com/zenfulcode/commercify/domain/model/VariantOption.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.domain.model; import jakarta.persistence.*; import lombok.*; @@ -14,7 +14,7 @@ @Table(name = "variant_option_entity") @NoArgsConstructor @AllArgsConstructor -public class VariantOptionEntity { +public class VariantOption { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -27,7 +27,7 @@ public class VariantOptionEntity { @ManyToOne @JoinColumn(name = "product_variant_id") - private ProductVariantEntity productVariant; + private ProductVariant productVariant; @Column(name = "created_at", nullable = false) @CreationTimestamp diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/GlobalExceptionHandler.java b/src/main/java/com/zenfulcode/commercify/exception/GlobalExceptionHandler.java similarity index 97% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/GlobalExceptionHandler.java rename to src/main/java/com/zenfulcode/commercify/exception/GlobalExceptionHandler.java index 5334676..5211833 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/zenfulcode/commercify/exception/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; import io.jsonwebtoken.ExpiredJwtException; import org.springframework.http.HttpStatusCode; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/InsufficientStockException.java b/src/main/java/com/zenfulcode/commercify/exception/InsufficientStockException.java similarity index 73% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/InsufficientStockException.java rename to src/main/java/com/zenfulcode/commercify/exception/InsufficientStockException.java index d3d92ce..4a89ceb 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/InsufficientStockException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/InsufficientStockException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; public class InsufficientStockException extends RuntimeException { public InsufficientStockException(String message) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/InvalidSortFieldException.java b/src/main/java/com/zenfulcode/commercify/exception/InvalidSortFieldException.java similarity index 84% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/InvalidSortFieldException.java rename to src/main/java/com/zenfulcode/commercify/exception/InvalidSortFieldException.java index 40e4b48..bea6e33 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/InvalidSortFieldException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/InvalidSortFieldException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/OrderNotFoundException.java b/src/main/java/com/zenfulcode/commercify/exception/OrderNotFoundException.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/OrderNotFoundException.java rename to src/main/java/com/zenfulcode/commercify/exception/OrderNotFoundException.java index 5040039..c73c8f2 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/OrderNotFoundException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/OrderNotFoundException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; public class OrderNotFoundException extends RuntimeException { public OrderNotFoundException(Long orderId) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/OrderValidationException.java b/src/main/java/com/zenfulcode/commercify/exception/OrderValidationException.java similarity index 84% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/OrderValidationException.java rename to src/main/java/com/zenfulcode/commercify/exception/OrderValidationException.java index 14fd35b..443edc0 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/OrderValidationException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/OrderValidationException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/PaymentProcessingException.java b/src/main/java/com/zenfulcode/commercify/exception/PaymentProcessingException.java similarity index 76% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/PaymentProcessingException.java rename to src/main/java/com/zenfulcode/commercify/exception/PaymentProcessingException.java index ee2b152..5064195 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/PaymentProcessingException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/PaymentProcessingException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; public class PaymentProcessingException extends RuntimeException { public PaymentProcessingException(String message, Throwable cause) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/PriceNotFoundException.java b/src/main/java/com/zenfulcode/commercify/exception/PriceNotFoundException.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/PriceNotFoundException.java rename to src/main/java/com/zenfulcode/commercify/exception/PriceNotFoundException.java index 13ccb98..406b628 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/PriceNotFoundException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/PriceNotFoundException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; public class PriceNotFoundException extends RuntimeException { public PriceNotFoundException(Long priceId) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/ProductDeletionException.java b/src/main/java/com/zenfulcode/commercify/exception/ProductDeletionException.java similarity index 78% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/ProductDeletionException.java rename to src/main/java/com/zenfulcode/commercify/exception/ProductDeletionException.java index 7785ebe..506cdf5 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/ProductDeletionException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/ProductDeletionException.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; import lombok.Getter; import java.util.List; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/ProductNotFoundException.java b/src/main/java/com/zenfulcode/commercify/exception/ProductNotFoundException.java similarity index 76% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/ProductNotFoundException.java rename to src/main/java/com/zenfulcode/commercify/exception/ProductNotFoundException.java index 1439b68..dd73ab9 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/ProductNotFoundException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/ProductNotFoundException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; public class ProductNotFoundException extends RuntimeException { public ProductNotFoundException(Long productId) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/ProductValidationException.java b/src/main/java/com/zenfulcode/commercify/exception/ProductValidationException.java similarity index 85% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/ProductValidationException.java rename to src/main/java/com/zenfulcode/commercify/exception/ProductValidationException.java index 26c9b4f..5878372 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/ProductValidationException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/ProductValidationException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; import lombok.Getter; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/exception/StripeOperationException.java b/src/main/java/com/zenfulcode/commercify/exception/StripeOperationException.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/exception/StripeOperationException.java rename to src/main/java/com/zenfulcode/commercify/exception/StripeOperationException.java index 5d9e0b5..fb72e11 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/exception/StripeOperationException.java +++ b/src/main/java/com/zenfulcode/commercify/exception/StripeOperationException.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.exception; +package com.zenfulcode.commercify.exception; public class StripeOperationException extends RuntimeException { diff --git a/src/main/java/com/zenfulcode/commercify/repository/AddressRepository.java b/src/main/java/com/zenfulcode/commercify/repository/AddressRepository.java new file mode 100644 index 0000000..57e4651 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/repository/AddressRepository.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.repository; + +import com.zenfulcode.commercify.domain.model.Address; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AddressRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/ConfirmationTokenRepository.java b/src/main/java/com/zenfulcode/commercify/repository/ConfirmationTokenRepository.java new file mode 100644 index 0000000..698455f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/repository/ConfirmationTokenRepository.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.repository; + +import com.zenfulcode.commercify.domain.model.ConfirmationToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ConfirmationTokenRepository extends JpaRepository { + Optional findByToken(String token); + Optional findByUserIdAndConfirmedFalse(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/OrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/repository/OrderLineRepository.java new file mode 100644 index 0000000..0f0a7d1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/repository/OrderLineRepository.java @@ -0,0 +1,32 @@ +package com.zenfulcode.commercify.repository; + +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.OrderLine; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.Set; + +@Repository +public interface OrderLineRepository extends JpaRepository { + @Query("SELECT DISTINCT ol.order FROM OrderLine ol " + + "WHERE ol.productId = :productId " + + "AND ol.order.status IN :statuses") + Set findActiveOrdersForProduct( + @Param("productId") Long productId, + @Param("statuses") Collection statuses + ); + + @Query("SELECT DISTINCT ol.order FROM OrderLine ol " + + "JOIN ol.productVariant v " + + "WHERE v.id = :variantId " + + "AND ol.order.status IN :statuses") + Set findActiveOrdersForVariant( + @Param("variantId") Long variantId, + @Param("statuses") Collection statuses + ); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/OrderRepository.java b/src/main/java/com/zenfulcode/commercify/repository/OrderRepository.java new file mode 100644 index 0000000..5225ec4 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/repository/OrderRepository.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.repository; + +import com.zenfulcode.commercify.domain.model.Order; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface OrderRepository extends JpaRepository { + Page findByUserId(Long userId, Pageable pageable); + + boolean existsByIdAndUserId(Long id, Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/OrderShippingInfoRepository.java b/src/main/java/com/zenfulcode/commercify/repository/OrderShippingInfoRepository.java similarity index 55% rename from src/main/java/com/zenfulcode/commercify/commercify/repository/OrderShippingInfoRepository.java rename to src/main/java/com/zenfulcode/commercify/repository/OrderShippingInfoRepository.java index 1785ee5..e162ba6 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/OrderShippingInfoRepository.java +++ b/src/main/java/com/zenfulcode/commercify/repository/OrderShippingInfoRepository.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.repository; +package com.zenfulcode.commercify.repository; -import com.zenfulcode.commercify.commercify.entity.OrderShippingInfo; +import com.zenfulcode.commercify.domain.model.OrderShippingInfo; import org.springframework.data.jpa.repository.JpaRepository; public interface OrderShippingInfoRepository extends JpaRepository { diff --git a/src/main/java/com/zenfulcode/commercify/repository/PaymentRepository.java b/src/main/java/com/zenfulcode/commercify/repository/PaymentRepository.java new file mode 100644 index 0000000..69289d3 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/repository/PaymentRepository.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.repository; + +import com.zenfulcode.commercify.domain.model.Payment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface PaymentRepository extends JpaRepository { + Optional findByOrderId(Long orderId); + + Optional findByMobilePayReference(String mobilePayReference); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/repository/ProductRepository.java b/src/main/java/com/zenfulcode/commercify/repository/ProductRepository.java similarity index 58% rename from src/main/java/com/zenfulcode/commercify/commercify/repository/ProductRepository.java rename to src/main/java/com/zenfulcode/commercify/repository/ProductRepository.java index e9f1665..e38e029 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/repository/ProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/repository/ProductRepository.java @@ -1,12 +1,12 @@ -package com.zenfulcode.commercify.commercify.repository; +package com.zenfulcode.commercify.repository; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; +import com.zenfulcode.commercify.domain.model.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface ProductRepository extends JpaRepository { - Page queryAllByActiveTrue(Pageable pageable); +public interface ProductRepository extends JpaRepository { + Page queryAllByActiveTrue(Pageable pageable); } diff --git a/src/main/java/com/zenfulcode/commercify/repository/ProductVariantRepository.java b/src/main/java/com/zenfulcode/commercify/repository/ProductVariantRepository.java new file mode 100644 index 0000000..24344fa --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/repository/ProductVariantRepository.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.repository; + +import com.zenfulcode.commercify.domain.model.ProductVariant; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductVariantRepository extends JpaRepository { + Page findByProductId(Long productId, Pageable pageable); +} diff --git a/src/main/java/com/zenfulcode/commercify/repository/UserRepository.java b/src/main/java/com/zenfulcode/commercify/repository/UserRepository.java new file mode 100644 index 0000000..bd68644 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.repository; + +import com.zenfulcode.commercify.domain.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductDeletionService.java b/src/main/java/com/zenfulcode/commercify/service/ProductDeletionService.java similarity index 68% rename from src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductDeletionService.java rename to src/main/java/com/zenfulcode/commercify/service/ProductDeletionService.java index 084003b..1f08ee9 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductDeletionService.java +++ b/src/main/java/com/zenfulcode/commercify/service/ProductDeletionService.java @@ -1,13 +1,13 @@ -package com.zenfulcode.commercify.commercify.service.product; - -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; -import com.zenfulcode.commercify.commercify.dto.ProductDeletionValidationResult; -import com.zenfulcode.commercify.commercify.dto.mapper.OrderMapper; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; -import com.zenfulcode.commercify.commercify.exception.ProductDeletionException; -import com.zenfulcode.commercify.commercify.repository.OrderLineRepository; -import com.zenfulcode.commercify.commercify.repository.ProductRepository; +package com.zenfulcode.commercify.service; + +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.domain.model.Product; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; +import com.zenfulcode.commercify.web.dto.common.ProductDeletionValidationResult; +import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; +import com.zenfulcode.commercify.exception.ProductDeletionException; +import com.zenfulcode.commercify.repository.OrderLineRepository; +import com.zenfulcode.commercify.repository.ProductRepository; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; @@ -21,7 +21,7 @@ public class ProductDeletionService { private final OrderMapper orderMapper; private final ProductRepository productRepository; - public void validateAndDelete(ProductEntity product) { + public void validateAndDelete(Product product) { ProductDeletionValidationResult validationResult = validateDeletion(product); if (!validationResult.canDelete()) { @@ -35,7 +35,7 @@ public void validateAndDelete(ProductEntity product) { productRepository.delete(product); } - private ProductDeletionValidationResult validateDeletion(ProductEntity product) { + private ProductDeletionValidationResult validateDeletion(Product product) { List activeOrders = orderLineRepository .findActiveOrdersForProduct( product.getId(), diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductVariantService.java b/src/main/java/com/zenfulcode/commercify/service/ProductVariantService.java similarity index 62% rename from src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductVariantService.java rename to src/main/java/com/zenfulcode/commercify/service/ProductVariantService.java index 19e1f6e..c59bffa 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductVariantService.java +++ b/src/main/java/com/zenfulcode/commercify/service/ProductVariantService.java @@ -1,15 +1,16 @@ -package com.zenfulcode.commercify.commercify.service.product; - -import com.zenfulcode.commercify.commercify.api.requests.products.CreateVariantOptionRequest; -import com.zenfulcode.commercify.commercify.api.requests.products.ProductVariantRequest; -import com.zenfulcode.commercify.commercify.dto.ProductVariantEntityDto; -import com.zenfulcode.commercify.commercify.dto.mapper.ProductVariantMapper; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; -import com.zenfulcode.commercify.commercify.entity.ProductVariantEntity; -import com.zenfulcode.commercify.commercify.entity.VariantOptionEntity; -import com.zenfulcode.commercify.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.commercify.repository.ProductVariantRepository; +package com.zenfulcode.commercify.service; + +import com.zenfulcode.commercify.web.dto.request.product.CreateVariantOptionRequest; +import com.zenfulcode.commercify.web.dto.request.product.ProductVariantRequest; +import com.zenfulcode.commercify.domain.model.Product; +import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; +import com.zenfulcode.commercify.web.dto.mapper.ProductVariantMapper; +import com.zenfulcode.commercify.domain.model.ProductVariant; +import com.zenfulcode.commercify.domain.model.VariantOption; +import com.zenfulcode.commercify.exception.ProductNotFoundException; +import com.zenfulcode.commercify.repository.ProductRepository; +import com.zenfulcode.commercify.repository.ProductVariantRepository; +import com.zenfulcode.commercify.service.validations.ProductValidationService; import lombok.AllArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -31,12 +32,12 @@ public class ProductVariantService { @Transactional public ProductVariantEntityDto addVariant(Long productId, ProductVariantRequest request) { validationService.validateVariantRequest(request); - ProductEntity product = getProduct(productId); + Product product = getProduct(productId); - ProductVariantEntity variant = createVariantFromRequest(request, product); + ProductVariant variant = createVariantFromRequest(request, product); product.addVariant(variant); - ProductVariantEntity savedVariant = variantRepository.save(variant); + ProductVariant savedVariant = variantRepository.save(variant); return variantMapper.apply(savedVariant); } @@ -44,16 +45,16 @@ public ProductVariantEntityDto addVariant(Long productId, ProductVariantRequest public ProductVariantEntityDto updateVariant(Long productId, Long variantId, ProductVariantRequest request) { validationService.validateVariantRequest(request); - ProductVariantEntity variant = getVariant(productId, variantId); + ProductVariant variant = getVariant(productId, variantId); updateVariantDetails(variant, request); - ProductVariantEntity savedVariant = variantRepository.save(variant); + ProductVariant savedVariant = variantRepository.save(variant); return variantMapper.apply(savedVariant); } @Transactional public void deleteVariant(Long productId, Long variantId) { - ProductVariantEntity variant = getVariant(productId, variantId); + ProductVariant variant = getVariant(productId, variantId); validationService.validateVariantDeletion(variant); variantRepository.delete(variant); } @@ -67,8 +68,8 @@ public Page getProductVariants(Long productId, PageRequ } public ProductVariantEntityDto getVariantDto(Long productId, Long variantId) { - ProductVariantEntity variant = getVariant(productId, variantId); - ProductEntity product = variant.getProduct(); + ProductVariant variant = getVariant(productId, variantId); + Product product = variant.getProduct(); ProductVariantEntityDto dto = variantMapper.apply(variant); @@ -81,19 +82,19 @@ public ProductVariantEntityDto getVariantDto(Long productId, Long variantId) { } @Transactional(readOnly = true) - Set createVariantsFromRequest(List requests, ProductEntity product) { + public Set createVariantsFromRequest(List requests, Product product) { return requests.stream() .map(request -> createVariantFromRequest(request, product)) .collect(Collectors.toSet()); } - private ProductEntity getProduct(Long productId) { + private Product getProduct(Long productId) { return productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException(productId)); } - private ProductVariantEntity getVariant(Long productId, Long variantId) { - ProductEntity product = getProduct(productId); + private ProductVariant getVariant(Long productId, Long variantId) { + Product product = getProduct(productId); return product.getVariants().stream() .filter(variant -> variant.getId().equals(variantId)) .findFirst() @@ -102,14 +103,14 @@ private ProductVariantEntity getVariant(Long productId, Long variantId) { )); } - private ProductVariantEntity createVariantFromRequest(ProductVariantRequest request, ProductEntity product) { - ProductVariantEntity variant = new ProductVariantEntity(); + private ProductVariant createVariantFromRequest(ProductVariantRequest request, Product product) { + ProductVariant variant = new ProductVariant(); updateVariantDetails(variant, request); variant.setProduct(product); return variant; } - private void updateVariantDetails(ProductVariantEntity variant, ProductVariantRequest request) { + private void updateVariantDetails(ProductVariant variant, ProductVariantRequest request) { variant.setSku(request.sku()); variant.setStock(request.stock()); variant.setImageUrl(request.imageUrl()); @@ -117,10 +118,10 @@ private void updateVariantDetails(ProductVariantEntity variant, ProductVariantRe updateVariantOptions(variant, request.options()); } - private void updateVariantOptions(ProductVariantEntity variant, List options) { + private void updateVariantOptions(ProductVariant variant, List options) { variant.getOptions().clear(); options.forEach(optionRequest -> { - VariantOptionEntity option = VariantOptionEntity.builder() + VariantOption option = VariantOption.builder() .name(optionRequest.name()) .value(optionRequest.value()) .productVariant(variant) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/StockManagementService.java b/src/main/java/com/zenfulcode/commercify/service/StockManagementService.java similarity index 57% rename from src/main/java/com/zenfulcode/commercify/commercify/service/StockManagementService.java rename to src/main/java/com/zenfulcode/commercify/service/StockManagementService.java index 0643320..f2faf49 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/StockManagementService.java +++ b/src/main/java/com/zenfulcode/commercify/service/StockManagementService.java @@ -1,11 +1,11 @@ -package com.zenfulcode.commercify.commercify.service; +package com.zenfulcode.commercify.service; -import com.zenfulcode.commercify.commercify.entity.OrderLineEntity; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; -import com.zenfulcode.commercify.commercify.entity.ProductVariantEntity; -import com.zenfulcode.commercify.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.commercify.repository.ProductVariantRepository; +import com.zenfulcode.commercify.domain.model.OrderLine; +import com.zenfulcode.commercify.domain.model.Product; +import com.zenfulcode.commercify.domain.model.ProductVariant; +import com.zenfulcode.commercify.exception.ProductNotFoundException; +import com.zenfulcode.commercify.repository.ProductRepository; +import com.zenfulcode.commercify.repository.ProductVariantRepository; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,16 +19,16 @@ public class StockManagementService { private final ProductVariantRepository variantRepository; @Transactional - public void updateStockLevels(Set orderLines) { - for (OrderLineEntity line : orderLines) { + public void updateStockLevels(Set orderLines) { + for (OrderLine line : orderLines) { if (line.getProductVariant() != null) { // Update variant stock - ProductVariantEntity variant = line.getProductVariant(); + ProductVariant variant = line.getProductVariant(); variant.setStock(variant.getStock() - line.getQuantity()); variantRepository.save(variant); } else { // Update product stock - ProductEntity product = productRepository.findById(line.getProductId()) + Product product = productRepository.findById(line.getProductId()) .orElseThrow(() -> new ProductNotFoundException(line.getProductId())); product.setStock(product.getStock() - line.getQuantity()); productRepository.save(product); @@ -37,16 +37,16 @@ public void updateStockLevels(Set orderLines) { } @Transactional - public void restoreStockLevels(Set orderLines) { - for (OrderLineEntity line : orderLines) { + public void restoreStockLevels(Set orderLines) { + for (OrderLine line : orderLines) { if (line.getProductVariant() != null) { // Restore variant stock - ProductVariantEntity variant = line.getProductVariant(); + ProductVariant variant = line.getProductVariant(); variant.setStock(variant.getStock() + line.getQuantity()); variantRepository.save(variant); } else { // Restore product stock - ProductEntity product = productRepository.findById(line.getProductId()) + Product product = productRepository.findById(line.getProductId()) .orElseThrow(() -> new ProductNotFoundException(line.getProductId())); product.setStock(product.getStock() + line.getQuantity()); productRepository.save(product); diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/AuthenticationService.java b/src/main/java/com/zenfulcode/commercify/service/authentication/AuthenticationService.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/service/AuthenticationService.java rename to src/main/java/com/zenfulcode/commercify/service/authentication/AuthenticationService.java index 1f0b6ae..8dcd5cf 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/AuthenticationService.java +++ b/src/main/java/com/zenfulcode/commercify/service/authentication/AuthenticationService.java @@ -1,13 +1,13 @@ -package com.zenfulcode.commercify.commercify.service; +package com.zenfulcode.commercify.service.authentication; -import com.zenfulcode.commercify.commercify.api.requests.LoginUserRequest; -import com.zenfulcode.commercify.commercify.api.requests.RegisterUserRequest; -import com.zenfulcode.commercify.commercify.dto.UserDTO; -import com.zenfulcode.commercify.commercify.dto.mapper.UserMapper; -import com.zenfulcode.commercify.commercify.entity.AddressEntity; -import com.zenfulcode.commercify.commercify.entity.UserEntity; -import com.zenfulcode.commercify.commercify.repository.UserRepository; +import com.zenfulcode.commercify.web.dto.request.auth.LoginUserRequest; +import com.zenfulcode.commercify.web.dto.request.auth.RegisterUserRequest; +import com.zenfulcode.commercify.web.dto.common.UserDTO; +import com.zenfulcode.commercify.web.dto.mapper.UserMapper; +import com.zenfulcode.commercify.domain.model.Address; +import com.zenfulcode.commercify.domain.model.User; +import com.zenfulcode.commercify.repository.UserRepository; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; @@ -31,16 +31,16 @@ public class AuthenticationService { @Transactional public UserDTO registerUser(RegisterUserRequest registerRequest) { - Optional existing = userRepository.findByEmail(registerRequest.email()); + Optional existing = userRepository.findByEmail(registerRequest.email()); if (existing.isPresent()) { throw new RuntimeException("User with email " + registerRequest.email() + " already exists"); } - AddressEntity shippingAddress = null; + Address shippingAddress = null; if (registerRequest.defaultAddress() != null) { - shippingAddress = AddressEntity.builder() + shippingAddress = Address.builder() .street(registerRequest.defaultAddress().getStreet()) .city(registerRequest.defaultAddress().getCity()) .state(registerRequest.defaultAddress().getState()) @@ -49,7 +49,7 @@ public UserDTO registerUser(RegisterUserRequest registerRequest) { .build(); } - UserEntity user = UserEntity.builder() + User user = User.builder() .firstName(registerRequest.firstName()) .lastName(registerRequest.lastName()) .email(registerRequest.email()) @@ -59,7 +59,7 @@ public UserDTO registerUser(RegisterUserRequest registerRequest) { .emailConfirmed(false) .build(); - UserEntity savedUser = userRepository.save(user); + User savedUser = userRepository.save(user); // TODO: Send user confirmation email @@ -74,7 +74,7 @@ public UserDTO authenticate(LoginUserRequest login) { ) ); - UserEntity user = userRepository.findByEmail(login.email()).orElseThrow(); + User user = userRepository.findByEmail(login.email()).orElseThrow(); if (!passwordEncoder.matches(login.password(), user.getPassword())) return null; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/JwtService.java b/src/main/java/com/zenfulcode/commercify/service/authentication/JwtService.java similarity index 95% rename from src/main/java/com/zenfulcode/commercify/commercify/service/JwtService.java rename to src/main/java/com/zenfulcode/commercify/service/authentication/JwtService.java index d8b4e70..c8c7b64 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/JwtService.java +++ b/src/main/java/com/zenfulcode/commercify/service/authentication/JwtService.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.service; +package com.zenfulcode.commercify.service.authentication; -import com.zenfulcode.commercify.commercify.dto.UserDTO; +import com.zenfulcode.commercify.web.dto.common.UserDTO; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderService.java b/src/main/java/com/zenfulcode/commercify/service/core/OrderService.java similarity index 73% rename from src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderService.java rename to src/main/java/com/zenfulcode/commercify/service/core/OrderService.java index e2ca7b8..d56900f 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderService.java +++ b/src/main/java/com/zenfulcode/commercify/service/core/OrderService.java @@ -1,20 +1,21 @@ -package com.zenfulcode.commercify.commercify.service.order; - -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.api.requests.orders.CreateOrderLineRequest; -import com.zenfulcode.commercify.commercify.api.requests.orders.CreateOrderRequest; -import com.zenfulcode.commercify.commercify.dto.*; -import com.zenfulcode.commercify.commercify.dto.mapper.OrderMapper; -import com.zenfulcode.commercify.commercify.dto.mapper.ProductMapper; -import com.zenfulcode.commercify.commercify.dto.mapper.ProductVariantMapper; -import com.zenfulcode.commercify.commercify.entity.*; -import com.zenfulcode.commercify.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.commercify.repository.OrderShippingInfoRepository; -import com.zenfulcode.commercify.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.commercify.repository.ProductVariantRepository; -import com.zenfulcode.commercify.commercify.service.StockManagementService; +package com.zenfulcode.commercify.service.core; + +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.domain.model.*; +import com.zenfulcode.commercify.exception.OrderNotFoundException; +import com.zenfulcode.commercify.exception.ProductNotFoundException; +import com.zenfulcode.commercify.repository.OrderRepository; +import com.zenfulcode.commercify.repository.OrderShippingInfoRepository; +import com.zenfulcode.commercify.repository.ProductRepository; +import com.zenfulcode.commercify.repository.ProductVariantRepository; +import com.zenfulcode.commercify.service.StockManagementService; +import com.zenfulcode.commercify.service.validations.OrderValidationService; +import com.zenfulcode.commercify.web.dto.common.*; +import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; +import com.zenfulcode.commercify.web.dto.mapper.ProductMapper; +import com.zenfulcode.commercify.web.dto.mapper.ProductVariantMapper; +import com.zenfulcode.commercify.web.dto.request.order.CreateOrderLineRequest; +import com.zenfulcode.commercify.web.dto.request.order.CreateOrderRequest; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -35,7 +36,6 @@ public class OrderService { private final OrderMapper orderMapper; private final ProductMapper productMapper; private final OrderValidationService validationService; - private final OrderCalculationService calculationService; private final StockManagementService stockService; private final ProductVariantRepository variantRepository; private final ProductVariantMapper productVariantMapper; @@ -52,16 +52,16 @@ public OrderDTO createOrder(Long userId, CreateOrderRequest request) { validationService.validateCreateOrderRequest(request); // Get and validate all products and variants upfront - Map products = getAndValidateProducts(request.orderLines()); - Map variants = getAndValidateVariants(request.orderLines()); + Map products = getAndValidateProducts(request.orderLines()); + Map variants = getAndValidateVariants(request.orderLines()); // Get shipping information OrderShippingInfo shippingInfo = getShippingInformation(request); orderShippingInfoRepository.save(shippingInfo); // Create order entity - OrderEntity order = buildOrderEntity(userId, request, products, variants, shippingInfo); - OrderEntity savedOrder = orderRepository.save(order); + Order order = buildOrderEntity(userId, request, products, variants, shippingInfo); + Order savedOrder = orderRepository.save(order); return orderMapper.apply(savedOrder); } @@ -95,7 +95,7 @@ private OrderShippingInfo getShippingInformation(CreateOrderRequest request) { @Transactional public void updateOrderStatus(Long orderId, OrderStatus newStatus) { - OrderEntity order = findOrderById(orderId); + Order order = findOrderById(orderId); OrderStatus oldStatus = order.getStatus(); validationService.validateStatusTransition(oldStatus, newStatus); @@ -106,7 +106,7 @@ public void updateOrderStatus(Long orderId, OrderStatus newStatus) { @Transactional public void cancelOrder(Long orderId) { - OrderEntity order = findOrderById(orderId); + Order order = findOrderById(orderId); validationService.validateOrderCancellation(order); // Restore stock levels @@ -128,7 +128,7 @@ public Page getAllOrders(Pageable pageable) { @Transactional(readOnly = true) public OrderDetailsDTO getOrderById(Long orderId) { - OrderEntity order = findOrderById(orderId); + Order order = findOrderById(orderId); return buildOrderDetailsDTO(order); } @@ -137,7 +137,7 @@ public boolean isOrderOwnedByUser(Long orderId, Long userId) { return orderRepository.existsByIdAndUserId(orderId, userId); } - private Map getAndValidateVariants(List orderLines) { + private Map getAndValidateVariants(List orderLines) { Set variantIds = orderLines.stream() .map(CreateOrderLineRequest::variantId) .filter(Objects::nonNull) @@ -147,13 +147,13 @@ private Map getAndValidateVariants(List variants = variantRepository - .findAllById(variantIds).stream().collect(Collectors.toMap(ProductVariantEntity::getId, Function.identity())); + Map variants = variantRepository + .findAllById(variantIds).stream().collect(Collectors.toMap(ProductVariant::getId, Function.identity())); // Validate all requested variants exist and have sufficient stock orderLines.forEach(line -> { if (line.variantId() != null) { - ProductVariantEntity variant = variants.get(line.variantId()); + ProductVariant variant = variants.get(line.variantId()); if (variant == null) { throw new ProductNotFoundException(line.variantId()); } @@ -172,20 +172,26 @@ private Map getAndValidateVariants(List products, - Map variants, - OrderShippingInfo shippingInfo) { + public double calculateTotalAmount(Set orderLines) { + return orderLines.stream() + .mapToDouble(line -> line.getUnitPrice() * line.getQuantity()) + .sum(); + } + + private Order buildOrderEntity(Long userId, + CreateOrderRequest request, + Map products, + Map variants, + OrderShippingInfo shippingInfo) { // Create order lines first - Set orderLines = request.orderLines().stream() + Set orderLines = request.orderLines().stream() .map(line -> createOrderLine(line.quantity(), products.get(line.productId()), line.variantId() != null ? variants.get(line.variantId()) : null)) .collect(Collectors.toSet()); - double totalAmount = calculationService.calculateTotalAmount(orderLines); + double totalAmount = calculateTotalAmount(orderLines); - OrderEntity order = OrderEntity.builder() + Order order = Order.builder() .userId(userId) .orderLines(orderLines) .status(OrderStatus.PENDING) @@ -200,14 +206,14 @@ private OrderEntity buildOrderEntity(Long userId, return order; } - private OrderLineEntity createOrderLine( + private OrderLine createOrderLine( int quantity, - ProductEntity product, - ProductVariantEntity variant) { + Product product, + ProductVariant variant) { double unitPrice = variant != null && variant.getUnitPrice() != null ? variant.getUnitPrice() : product.getUnitPrice(); - return OrderLineEntity.builder() + return OrderLine.builder() .productId(product.getId()) .productVariant(variant) .quantity(quantity) @@ -216,13 +222,13 @@ private OrderLineEntity createOrderLine( .build(); } - private Map getAndValidateProducts(List orderLines) { + private Map getAndValidateProducts(List orderLines) { Set productIds = orderLines.stream().map(CreateOrderLineRequest::productId).collect(Collectors.toSet()); - Map products = productRepository.findAllById(productIds).stream().collect(Collectors.toMap(ProductEntity::getId, Function.identity())); + Map products = productRepository.findAllById(productIds).stream().collect(Collectors.toMap(com.zenfulcode.commercify.domain.model.Product::getId, Function.identity())); orderLines.forEach(line -> { - ProductEntity product = products.get(line.productId()); + Product product = products.get(line.productId()); if (product == null) { throw new ProductNotFoundException(line.productId()); } @@ -234,14 +240,14 @@ private Map getAndValidateProducts(List new OrderNotFoundException(orderId)); } - private OrderDetailsDTO buildOrderDetailsDTO(OrderEntity order) { + private OrderDetailsDTO buildOrderDetailsDTO(Order order) { List orderLines = order.getOrderLines().stream() .map(line -> { - ProductEntity product = productRepository.findById(line.getProductId()) + Product product = productRepository.findById(line.getProductId()) .orElseThrow(() -> new ProductNotFoundException(line.getProductId())); return OrderLineDTO.builder() diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/PaymentService.java b/src/main/java/com/zenfulcode/commercify/service/core/PaymentService.java similarity index 73% rename from src/main/java/com/zenfulcode/commercify/commercify/service/PaymentService.java rename to src/main/java/com/zenfulcode/commercify/service/core/PaymentService.java index 89b11b1..9a62b50 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/PaymentService.java +++ b/src/main/java/com/zenfulcode/commercify/service/core/PaymentService.java @@ -1,12 +1,11 @@ -package com.zenfulcode.commercify.commercify.service; - -import com.zenfulcode.commercify.commercify.PaymentStatus; -import com.zenfulcode.commercify.commercify.dto.OrderDetailsDTO; -import com.zenfulcode.commercify.commercify.entity.PaymentEntity; -import com.zenfulcode.commercify.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.commercify.service.email.EmailService; -import com.zenfulcode.commercify.commercify.service.order.OrderService; +package com.zenfulcode.commercify.service.core; + +import com.zenfulcode.commercify.domain.enums.PaymentStatus; +import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; +import com.zenfulcode.commercify.domain.model.Payment; +import com.zenfulcode.commercify.repository.OrderRepository; +import com.zenfulcode.commercify.repository.PaymentRepository; +import com.zenfulcode.commercify.service.email.EmailService; import jakarta.mail.MessagingException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,7 +23,7 @@ public class PaymentService { @Transactional public void handlePaymentStatusUpdate(Long orderId, PaymentStatus newStatus) { - PaymentEntity payment = paymentRepository.findByOrderId(orderId) + Payment payment = paymentRepository.findByOrderId(orderId) .orElseThrow(() -> new RuntimeException("Payment not found for order: " + orderId)); PaymentStatus oldStatus = payment.getStatus(); @@ -52,7 +51,7 @@ public void handlePaymentStatusUpdate(Long orderId, PaymentStatus newStatus) { public PaymentStatus getPaymentStatus(Long orderId) { return paymentRepository.findByOrderId(orderId) - .map(PaymentEntity::getStatus) + .map(Payment::getStatus) .orElse(PaymentStatus.NOT_FOUND); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductService.java b/src/main/java/com/zenfulcode/commercify/service/core/ProductService.java similarity index 70% rename from src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductService.java rename to src/main/java/com/zenfulcode/commercify/service/core/ProductService.java index f5a53e6..4781063 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductService.java +++ b/src/main/java/com/zenfulcode/commercify/service/core/ProductService.java @@ -1,14 +1,17 @@ -package com.zenfulcode.commercify.commercify.service.product; - -import com.zenfulcode.commercify.commercify.api.requests.products.ProductRequest; -import com.zenfulcode.commercify.commercify.dto.ProductDTO; -import com.zenfulcode.commercify.commercify.dto.ProductUpdateResult; -import com.zenfulcode.commercify.commercify.dto.mapper.ProductMapper; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; -import com.zenfulcode.commercify.commercify.entity.ProductVariantEntity; -import com.zenfulcode.commercify.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.commercify.factory.ProductFactory; -import com.zenfulcode.commercify.commercify.repository.ProductRepository; +package com.zenfulcode.commercify.service.core; + +import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; +import com.zenfulcode.commercify.domain.model.Product; +import com.zenfulcode.commercify.web.dto.common.ProductDTO; +import com.zenfulcode.commercify.web.dto.common.ProductUpdateResult; +import com.zenfulcode.commercify.web.dto.mapper.ProductMapper; +import com.zenfulcode.commercify.domain.model.ProductVariant; +import com.zenfulcode.commercify.exception.ProductNotFoundException; +import com.zenfulcode.commercify.component.factory.ProductFactory; +import com.zenfulcode.commercify.repository.ProductRepository; +import com.zenfulcode.commercify.service.ProductDeletionService; +import com.zenfulcode.commercify.service.validations.ProductValidationService; +import com.zenfulcode.commercify.service.ProductVariantService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -33,24 +36,24 @@ public class ProductService { @Transactional public ProductDTO saveProduct(ProductRequest request) { validationService.validateProductRequest(request); - ProductEntity product = productFactory.createFromRequest(request); + Product product = productFactory.createFromRequest(request); if (request.variants() != null && !request.variants().isEmpty()) { - Set variants = variantService.createVariantsFromRequest(request.variants(), product); + Set variants = variantService.createVariantsFromRequest(request.variants(), product); product.setVariants(variants); } - ProductEntity savedProduct = productRepository.save(product); + Product savedProduct = productRepository.save(product); return productMapper.apply(savedProduct); } @Transactional public ProductUpdateResult updateProduct(Long id, ProductRequest request) { - ProductEntity product = productRepository.findById(id) + Product product = productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException(id)); updateProductDetails(product, request); - ProductEntity savedProduct = productRepository.save(product); + Product savedProduct = productRepository.save(product); return ProductUpdateResult.withWarnings( productMapper.apply(savedProduct), @@ -60,7 +63,7 @@ public ProductUpdateResult updateProduct(Long id, ProductRequest request) { @Transactional public void deleteProduct(Long id) { - ProductEntity product = productRepository.findById(id) + Product product = productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException(id)); deletionService.validateAndDelete(product); @@ -94,14 +97,14 @@ public Page getActiveProducts(PageRequest pageRequest) { } private void toggleProductStatus(Long id, boolean active) { - ProductEntity product = productRepository.findById(id) + Product product = productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException(id)); product.setActive(active); productRepository.save(product); } - private void updateProductDetails(ProductEntity product, ProductRequest request) { + private void updateProductDetails(Product product, ProductRequest request) { if (request.name() != null) product.setName(request.name()); if (request.description() != null) product.setDescription(request.description()); if (request.stock() != null) product.setStock(request.stock()); diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/UserManagementService.java b/src/main/java/com/zenfulcode/commercify/service/core/UserManagementService.java similarity index 74% rename from src/main/java/com/zenfulcode/commercify/commercify/service/UserManagementService.java rename to src/main/java/com/zenfulcode/commercify/service/core/UserManagementService.java index d7a13c7..ab00a90 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/UserManagementService.java +++ b/src/main/java/com/zenfulcode/commercify/service/core/UserManagementService.java @@ -1,13 +1,13 @@ -package com.zenfulcode.commercify.commercify.service; +package com.zenfulcode.commercify.service.core; -import com.zenfulcode.commercify.commercify.dto.AddressDTO; -import com.zenfulcode.commercify.commercify.dto.UserDTO; -import com.zenfulcode.commercify.commercify.dto.mapper.AddressMapper; -import com.zenfulcode.commercify.commercify.dto.mapper.UserMapper; -import com.zenfulcode.commercify.commercify.entity.AddressEntity; -import com.zenfulcode.commercify.commercify.entity.UserEntity; -import com.zenfulcode.commercify.commercify.repository.UserRepository; +import com.zenfulcode.commercify.web.dto.common.AddressDTO; +import com.zenfulcode.commercify.web.dto.common.UserDTO; +import com.zenfulcode.commercify.web.dto.mapper.AddressMapper; +import com.zenfulcode.commercify.web.dto.mapper.UserMapper; +import com.zenfulcode.commercify.domain.model.Address; +import com.zenfulcode.commercify.domain.model.User; +import com.zenfulcode.commercify.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -25,7 +25,7 @@ public class UserManagementService { @Transactional(readOnly = true) public UserDTO getUserById(Long id) { - UserEntity user = userRepository.findById(id) + User user = userRepository.findById(id) .orElseThrow(() -> new RuntimeException("User not found")); return mapper.apply(user); } @@ -37,14 +37,14 @@ public Page getAllUsers(Pageable pageable) { @Transactional public UserDTO updateUser(Long id, UserDTO userDTO) { - UserEntity user = userRepository.findById(id) + User user = userRepository.findById(id) .orElseThrow(() -> new RuntimeException("User not found")); user.setFirstName(userDTO.getFirstName()); user.setLastName(userDTO.getLastName()); user.setEmail(userDTO.getEmail()); - UserEntity updatedUser = userRepository.save(user); + User updatedUser = userRepository.save(user); return mapper.apply(updatedUser); } @@ -58,10 +58,10 @@ public void deleteUser(Long id) { @Transactional public AddressDTO setDefaultAddress(Long userId, AddressDTO request) { - UserEntity user = userRepository.findById(userId) + User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); - AddressEntity address = AddressEntity.builder() + Address address = Address.builder() .street(request.getStreet()) .city(request.getCity()) .state(request.getState()) @@ -77,7 +77,7 @@ public AddressDTO setDefaultAddress(Long userId, AddressDTO request) { @Transactional public UserDTO removeDefaultAddress(Long userId) { - UserEntity user = userRepository.findById(userId) + User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); user.setDefaultAddress(null); return mapper.apply(userRepository.save(user)); @@ -85,7 +85,7 @@ public UserDTO removeDefaultAddress(Long userId) { @Transactional public UserDTO addRoleToUser(Long userId, String role) { - UserEntity user = userRepository.findById(userId) + User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); if (user.getRoles() == null) { @@ -102,11 +102,11 @@ public UserDTO addRoleToUser(Long userId, String role) { @Transactional public UserDTO removeRoleFromUser(Long userId, String role) { - UserEntity user = userRepository.findById(userId) + User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); user.getRoles().remove(role.toUpperCase()); - UserEntity updatedUser = userRepository.save(user); + User updatedUser = userRepository.save(user); return mapper.apply(updatedUser); } diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/email/EmailConfirmationService.java b/src/main/java/com/zenfulcode/commercify/service/email/EmailConfirmationService.java similarity index 71% rename from src/main/java/com/zenfulcode/commercify/commercify/service/email/EmailConfirmationService.java rename to src/main/java/com/zenfulcode/commercify/service/email/EmailConfirmationService.java index 5ad689e..c41e3bf 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/email/EmailConfirmationService.java +++ b/src/main/java/com/zenfulcode/commercify/service/email/EmailConfirmationService.java @@ -1,9 +1,9 @@ -package com.zenfulcode.commercify.commercify.service.email; +package com.zenfulcode.commercify.service.email; -import com.zenfulcode.commercify.commercify.entity.ConfirmationTokenEntity; -import com.zenfulcode.commercify.commercify.entity.UserEntity; -import com.zenfulcode.commercify.commercify.repository.ConfirmationTokenRepository; -import com.zenfulcode.commercify.commercify.repository.UserRepository; +import com.zenfulcode.commercify.domain.model.ConfirmationToken; +import com.zenfulcode.commercify.domain.model.User; +import com.zenfulcode.commercify.repository.ConfirmationTokenRepository; +import com.zenfulcode.commercify.repository.UserRepository; import jakarta.mail.MessagingException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -19,18 +19,18 @@ public class EmailConfirmationService { private final EmailService emailService; @Transactional - public void createConfirmationToken(UserEntity user) { + public void createConfirmationToken(User user) { // Delete any existing unconfirmed tokens tokenRepository.findByUserIdAndConfirmedFalse(user.getId()) .ifPresent(tokenRepository::delete); // Create new token - ConfirmationTokenEntity token = ConfirmationTokenEntity.builder() + ConfirmationToken token = ConfirmationToken.builder() .user(user) .confirmed(false) .build(); - ConfirmationTokenEntity savedToken = tokenRepository.save(token); + ConfirmationToken savedToken = tokenRepository.save(token); try { emailService.sendConfirmationEmail(user.getEmail(), savedToken.getToken()); @@ -41,7 +41,7 @@ public void createConfirmationToken(UserEntity user) { @Transactional public boolean confirmEmail(String token) { - ConfirmationTokenEntity confirmationToken = tokenRepository.findByToken(token) + ConfirmationToken confirmationToken = tokenRepository.findByToken(token) .orElseThrow(() -> new RuntimeException("Invalid confirmation token")); if (confirmationToken.isConfirmed()) { @@ -53,7 +53,7 @@ public boolean confirmEmail(String token) { } confirmationToken.setConfirmed(true); - UserEntity user = confirmationToken.getUser(); + User user = confirmationToken.getUser(); user.setEmailConfirmed(true); tokenRepository.save(confirmationToken); @@ -64,7 +64,7 @@ public boolean confirmEmail(String token) { @Transactional public void resendConfirmationEmail(Long userId) { - UserEntity user = userRepository.findById(userId) + User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); if (user.isEnabled()) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/email/EmailService.java b/src/main/java/com/zenfulcode/commercify/service/email/EmailService.java similarity index 93% rename from src/main/java/com/zenfulcode/commercify/commercify/service/email/EmailService.java rename to src/main/java/com/zenfulcode/commercify/service/email/EmailService.java index 532175c..a46cb21 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/email/EmailService.java +++ b/src/main/java/com/zenfulcode/commercify/service/email/EmailService.java @@ -1,10 +1,10 @@ -package com.zenfulcode.commercify.commercify.service.email; +package com.zenfulcode.commercify.service.email; -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; -import com.zenfulcode.commercify.commercify.dto.OrderDetailsDTO; -import com.zenfulcode.commercify.commercify.dto.UserDTO; -import com.zenfulcode.commercify.commercify.service.UserManagementService; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; +import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; +import com.zenfulcode.commercify.web.dto.common.UserDTO; +import com.zenfulcode.commercify.service.core.UserManagementService; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayController.java b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayController.java similarity index 90% rename from src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayController.java rename to src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayController.java index 2e7b0d3..ea4ba02 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayController.java +++ b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayController.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.integration.mobilepay; +package com.zenfulcode.commercify.service.integration.mobilepay; -import com.zenfulcode.commercify.commercify.api.requests.PaymentRequest; -import com.zenfulcode.commercify.commercify.api.responses.PaymentResponse; +import com.zenfulcode.commercify.web.dto.request.payment.PaymentRequest; +import com.zenfulcode.commercify.web.dto.response.payment.PaymentResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayService.java b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayService.java similarity index 85% rename from src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayService.java rename to src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayService.java index db64ef6..edd791b 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayService.java +++ b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayService.java @@ -1,16 +1,16 @@ -package com.zenfulcode.commercify.commercify.integration.mobilepay; - -import com.zenfulcode.commercify.commercify.PaymentProvider; -import com.zenfulcode.commercify.commercify.PaymentStatus; -import com.zenfulcode.commercify.commercify.api.requests.PaymentRequest; -import com.zenfulcode.commercify.commercify.api.responses.PaymentResponse; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.PaymentEntity; -import com.zenfulcode.commercify.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.commercify.exception.PaymentProcessingException; -import com.zenfulcode.commercify.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.commercify.service.PaymentService; +package com.zenfulcode.commercify.service.integration.mobilepay; + +import com.zenfulcode.commercify.domain.enums.PaymentProvider; +import com.zenfulcode.commercify.domain.enums.PaymentStatus; +import com.zenfulcode.commercify.web.dto.request.payment.PaymentRequest; +import com.zenfulcode.commercify.web.dto.response.payment.PaymentResponse; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.Payment; +import com.zenfulcode.commercify.exception.OrderNotFoundException; +import com.zenfulcode.commercify.exception.PaymentProcessingException; +import com.zenfulcode.commercify.repository.OrderRepository; +import com.zenfulcode.commercify.repository.PaymentRepository; +import com.zenfulcode.commercify.service.core.PaymentService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -51,7 +51,7 @@ public class MobilePayService { @Transactional public PaymentResponse initiatePayment(PaymentRequest request) { try { - OrderEntity order = orderRepository.findById(request.orderId()) + Order order = orderRepository.findById(request.orderId()) .orElseThrow(() -> new OrderNotFoundException(request.orderId())); // Create MobilePay payment request @@ -61,7 +61,7 @@ public PaymentResponse initiatePayment(PaymentRequest request) { MobilePayResponse mobilePayResponse = createMobilePayPayment(paymentRequest); // Create and save payment entity - PaymentEntity payment = PaymentEntity.builder() + Payment payment = Payment.builder() .orderId(order.getId()) .totalAmount(order.getTotalAmount()) .paymentProvider(PaymentProvider.MOBILEPAY) @@ -70,7 +70,7 @@ public PaymentResponse initiatePayment(PaymentRequest request) { .mobilePayReference(mobilePayResponse.reference()) .build(); - PaymentEntity savedPayment = paymentRepository.save(payment); + Payment savedPayment = paymentRepository.save(payment); return new PaymentResponse( savedPayment.getId(), @@ -85,7 +85,7 @@ public PaymentResponse initiatePayment(PaymentRequest request) { @Transactional public void handlePaymentCallback(String paymentReference, String status) { - PaymentEntity payment = paymentRepository.findByMobilePayReference(paymentReference) + Payment payment = paymentRepository.findByMobilePayReference(paymentReference) .orElseThrow(() -> new PaymentProcessingException("Payment not found", null)); PaymentStatus newStatus = mapMobilePayStatus(status); @@ -136,7 +136,7 @@ private MobilePayResponse createMobilePayPayment(Map request) { } } - private Map createMobilePayRequest(OrderEntity order, PaymentRequest request) { + private Map createMobilePayRequest(Order order, PaymentRequest request) { validationPaymentRequest(request); Map paymentRequest = new HashMap<>(); diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayTokenService.java b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayTokenService.java similarity index 96% rename from src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayTokenService.java rename to src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayTokenService.java index df43af3..6f7568b 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayTokenService.java +++ b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayTokenService.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.integration.mobilepay; +package com.zenfulcode.commercify.service.integration.mobilepay; import com.fasterxml.jackson.annotation.JsonProperty; -import com.zenfulcode.commercify.commercify.exception.PaymentProcessingException; +import com.zenfulcode.commercify.exception.PaymentProcessingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeConfig.java b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeConfig.java similarity index 90% rename from src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeConfig.java rename to src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeConfig.java index 1bd6363..6dd8eeb 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeConfig.java +++ b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeConfig.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.integration.stripe; +package com.zenfulcode.commercify.service.integration.stripe; import com.stripe.Stripe; import jakarta.annotation.PostConstruct; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeController.java b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeController.java similarity index 84% rename from src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeController.java rename to src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeController.java index 6e8f59c..d1df910 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeController.java +++ b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeController.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.integration.stripe; +package com.zenfulcode.commercify.service.integration.stripe; -import com.zenfulcode.commercify.commercify.api.requests.PaymentRequest; -import com.zenfulcode.commercify.commercify.api.responses.PaymentResponse; +import com.zenfulcode.commercify.web.dto.request.payment.PaymentRequest; +import com.zenfulcode.commercify.web.dto.response.payment.PaymentResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeService.java b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeService.java similarity index 70% rename from src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeService.java rename to src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeService.java index cfbf7c1..dad0aab 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeService.java +++ b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeService.java @@ -1,22 +1,22 @@ -package com.zenfulcode.commercify.commercify.integration.stripe; +package com.zenfulcode.commercify.service.integration.stripe; import com.stripe.exception.StripeException; import com.stripe.model.checkout.Session; import com.stripe.param.checkout.SessionCreateParams; -import com.zenfulcode.commercify.commercify.PaymentProvider; -import com.zenfulcode.commercify.commercify.PaymentStatus; -import com.zenfulcode.commercify.commercify.api.requests.PaymentRequest; -import com.zenfulcode.commercify.commercify.api.responses.PaymentResponse; -import com.zenfulcode.commercify.commercify.dto.ProductDTO; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.OrderLineEntity; -import com.zenfulcode.commercify.commercify.entity.PaymentEntity; -import com.zenfulcode.commercify.commercify.entity.VariantOptionEntity; -import com.zenfulcode.commercify.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.commercify.exception.PaymentProcessingException; -import com.zenfulcode.commercify.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.commercify.service.product.ProductService; +import com.zenfulcode.commercify.domain.enums.PaymentProvider; +import com.zenfulcode.commercify.domain.enums.PaymentStatus; +import com.zenfulcode.commercify.web.dto.request.payment.PaymentRequest; +import com.zenfulcode.commercify.web.dto.response.payment.PaymentResponse; +import com.zenfulcode.commercify.web.dto.common.ProductDTO; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.OrderLine; +import com.zenfulcode.commercify.domain.model.Payment; +import com.zenfulcode.commercify.domain.model.VariantOption; +import com.zenfulcode.commercify.exception.OrderNotFoundException; +import com.zenfulcode.commercify.exception.PaymentProcessingException; +import com.zenfulcode.commercify.repository.OrderRepository; +import com.zenfulcode.commercify.repository.PaymentRepository; +import com.zenfulcode.commercify.service.core.ProductService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -36,12 +36,12 @@ public class StripeService { @Transactional public PaymentResponse initiatePayment(PaymentRequest request) { try { - OrderEntity order = orderRepository.findById(request.orderId()) + Order order = orderRepository.findById(request.orderId()) .orElseThrow(() -> new OrderNotFoundException(request.orderId())); Session session = createCheckoutSession(order, request); - PaymentEntity payment = PaymentEntity.builder() + Payment payment = Payment.builder() .orderId(order.getId()) .totalAmount(order.getTotalAmount()) .paymentProvider(PaymentProvider.STRIPE) @@ -49,7 +49,7 @@ public PaymentResponse initiatePayment(PaymentRequest request) { .paymentMethod(request.paymentMethod()) .build(); - PaymentEntity savedPayment = paymentRepository.save(payment); + Payment savedPayment = paymentRepository.save(payment); return new PaymentResponse( savedPayment.getId(), @@ -62,10 +62,10 @@ public PaymentResponse initiatePayment(PaymentRequest request) { } } - private Session createCheckoutSession(OrderEntity order, PaymentRequest request) throws StripeException { + private Session createCheckoutSession(Order order, PaymentRequest request) throws StripeException { List lineItems = new ArrayList<>(); - for (OrderLineEntity line : order.getOrderLines()) { + for (OrderLine line : order.getOrderLines()) { ProductDTO product = productService.getProductById(line.getProductId()); SessionCreateParams.LineItem.PriceData.ProductData.Builder productDataBuilder = SessionCreateParams.LineItem.PriceData.ProductData.builder() .setName(product.getName()) @@ -74,7 +74,7 @@ private Session createCheckoutSession(OrderEntity order, PaymentRequest request) if (line.getProductVariant() != null) { StringBuilder variantInfo = new StringBuilder(); - for (VariantOptionEntity option : line.getProductVariant().getOptions()) { + for (VariantOption option : line.getProductVariant().getOptions()) { variantInfo.append(option.getName()) .append(": ") .append(option.getValue()) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeWebhookHandler.java b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeWebhookHandler.java similarity index 70% rename from src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeWebhookHandler.java rename to src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeWebhookHandler.java index dea8bf4..6fe6397 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/integration/stripe/StripeWebhookHandler.java +++ b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeWebhookHandler.java @@ -1,17 +1,17 @@ -package com.zenfulcode.commercify.commercify.integration.stripe; +package com.zenfulcode.commercify.service.integration.stripe; import com.stripe.exception.SignatureVerificationException; import com.stripe.model.Event; import com.stripe.model.checkout.Session; import com.stripe.net.Webhook; -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.PaymentStatus; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.PaymentEntity; -import com.zenfulcode.commercify.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.commercify.exception.PaymentProcessingException; -import com.zenfulcode.commercify.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.commercify.repository.PaymentRepository; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.domain.enums.PaymentStatus; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.Payment; +import com.zenfulcode.commercify.exception.OrderNotFoundException; +import com.zenfulcode.commercify.exception.PaymentProcessingException; +import com.zenfulcode.commercify.repository.OrderRepository; +import com.zenfulcode.commercify.repository.PaymentRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -47,7 +47,7 @@ private void handleSuccessfulPayment(Session session) { return; } - OrderEntity order = orderRepository.findById(Long.parseLong(orderId)) + Order order = orderRepository.findById(Long.parseLong(orderId)) .orElseThrow(() -> new OrderNotFoundException(Long.parseLong(orderId))); // Update order status @@ -55,7 +55,7 @@ private void handleSuccessfulPayment(Session session) { orderRepository.save(order); // Update payment status - PaymentEntity payment = paymentRepository.findByOrderId(order.getId()) + Payment payment = paymentRepository.findByOrderId(order.getId()) .orElseThrow(() -> new RuntimeException("Payment not found for order: " + orderId)); payment.setStatus(PaymentStatus.PAID); paymentRepository.save(payment); diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderValidationService.java b/src/main/java/com/zenfulcode/commercify/service/validations/OrderValidationService.java similarity index 83% rename from src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderValidationService.java rename to src/main/java/com/zenfulcode/commercify/service/validations/OrderValidationService.java index 5665a2c..0229280 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/order/OrderValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/service/validations/OrderValidationService.java @@ -1,12 +1,12 @@ -package com.zenfulcode.commercify.commercify.service.order; +package com.zenfulcode.commercify.service.validations; -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.api.requests.orders.CreateOrderRequest; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; -import com.zenfulcode.commercify.commercify.exception.InsufficientStockException; -import com.zenfulcode.commercify.commercify.exception.OrderValidationException; -import com.zenfulcode.commercify.commercify.flow.OrderStateFlow; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.web.dto.request.order.CreateOrderRequest; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.Product; +import com.zenfulcode.commercify.exception.InsufficientStockException; +import com.zenfulcode.commercify.exception.OrderValidationException; +import com.zenfulcode.commercify.component.flow.OrderStateFlow; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; @@ -83,7 +83,7 @@ public void validateStatusTransition(OrderStatus currentStatus, OrderStatus newS } } - public void validateOrderCancellation(OrderEntity order) { + public void validateOrderCancellation(Order order) { if (orderStateFlow.canTransition(order.getStatus(), OrderStatus.CANCELLED)) { throw new IllegalStateException( String.format("Cannot cancel order in status: %s", order.getStatus()) @@ -91,7 +91,7 @@ public void validateOrderCancellation(OrderEntity order) { } } - public void validateStockAvailability(ProductEntity product, int requestedQuantity) { + public void validateStockAvailability(Product product, int requestedQuantity) { if (product.getStock() < requestedQuantity) { throw new InsufficientStockException( String.format("Insufficient stock for product %d. Available: %d, Requested: %d", diff --git a/src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductValidationService.java b/src/main/java/com/zenfulcode/commercify/service/validations/ProductValidationService.java similarity index 68% rename from src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductValidationService.java rename to src/main/java/com/zenfulcode/commercify/service/validations/ProductValidationService.java index 3d8ef8e..06b9628 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/service/product/ProductValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/service/validations/ProductValidationService.java @@ -1,15 +1,15 @@ -package com.zenfulcode.commercify.commercify.service.product; +package com.zenfulcode.commercify.service.validations; -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.api.requests.products.ProductRequest; -import com.zenfulcode.commercify.commercify.api.requests.products.ProductVariantRequest; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; -import com.zenfulcode.commercify.commercify.dto.mapper.OrderMapper; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.ProductVariantEntity; -import com.zenfulcode.commercify.commercify.exception.ProductDeletionException; -import com.zenfulcode.commercify.commercify.exception.ProductValidationException; -import com.zenfulcode.commercify.commercify.repository.OrderLineRepository; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; +import com.zenfulcode.commercify.web.dto.request.product.ProductVariantRequest; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; +import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.ProductVariant; +import com.zenfulcode.commercify.exception.ProductDeletionException; +import com.zenfulcode.commercify.exception.ProductValidationException; +import com.zenfulcode.commercify.repository.OrderLineRepository; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; @@ -56,8 +56,8 @@ public void validateVariantRequest(ProductVariantRequest request) { } } - public void validateVariantDeletion(ProductVariantEntity variant) { - Set activeOrders = orderLineRepository.findActiveOrdersForVariant( + public void validateVariantDeletion(ProductVariant variant) { + Set activeOrders = orderLineRepository.findActiveOrdersForVariant( variant.getId(), List.of(OrderStatus.PENDING, OrderStatus.CONFIRMED, OrderStatus.SHIPPED) ); diff --git a/src/main/java/com/zenfulcode/commercify/commercify/controller/AuthenticationController.java b/src/main/java/com/zenfulcode/commercify/web/controller/AuthenticationController.java similarity index 82% rename from src/main/java/com/zenfulcode/commercify/commercify/controller/AuthenticationController.java rename to src/main/java/com/zenfulcode/commercify/web/controller/AuthenticationController.java index d79c13a..17d1651 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/controller/AuthenticationController.java +++ b/src/main/java/com/zenfulcode/commercify/web/controller/AuthenticationController.java @@ -1,12 +1,12 @@ -package com.zenfulcode.commercify.commercify.controller; +package com.zenfulcode.commercify.web.controller; -import com.zenfulcode.commercify.commercify.api.requests.LoginUserRequest; -import com.zenfulcode.commercify.commercify.api.requests.RegisterUserRequest; -import com.zenfulcode.commercify.commercify.api.responses.AuthResponse; -import com.zenfulcode.commercify.commercify.dto.UserDTO; -import com.zenfulcode.commercify.commercify.service.AuthenticationService; -import com.zenfulcode.commercify.commercify.service.JwtService; +import com.zenfulcode.commercify.web.dto.request.auth.LoginUserRequest; +import com.zenfulcode.commercify.web.dto.request.auth.RegisterUserRequest; +import com.zenfulcode.commercify.web.dto.response.auth.AuthResponse; +import com.zenfulcode.commercify.web.dto.common.UserDTO; +import com.zenfulcode.commercify.service.authentication.AuthenticationService; +import com.zenfulcode.commercify.service.authentication.JwtService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/controller/OrderController.java b/src/main/java/com/zenfulcode/commercify/web/controller/OrderController.java similarity index 91% rename from src/main/java/com/zenfulcode/commercify/commercify/controller/OrderController.java rename to src/main/java/com/zenfulcode/commercify/web/controller/OrderController.java index 3a90ea3..df3c0b7 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/controller/OrderController.java +++ b/src/main/java/com/zenfulcode/commercify/web/controller/OrderController.java @@ -1,16 +1,16 @@ -package com.zenfulcode.commercify.commercify.controller; - -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.api.requests.orders.CreateOrderRequest; -import com.zenfulcode.commercify.commercify.api.requests.orders.OrderStatusUpdateRequest; -import com.zenfulcode.commercify.commercify.api.responses.ErrorResponse; -import com.zenfulcode.commercify.commercify.api.responses.orders.CreateOrderResponse; -import com.zenfulcode.commercify.commercify.api.responses.orders.GetOrderResponse; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; -import com.zenfulcode.commercify.commercify.dto.OrderDetailsDTO; -import com.zenfulcode.commercify.commercify.exception.*; -import com.zenfulcode.commercify.commercify.service.order.OrderService; -import com.zenfulcode.commercify.commercify.viewmodel.OrderViewModel; +package com.zenfulcode.commercify.web.controller; + +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.exception.*; +import com.zenfulcode.commercify.service.core.OrderService; +import com.zenfulcode.commercify.web.viewmodel.OrderViewModel; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; +import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; +import com.zenfulcode.commercify.web.dto.request.order.CreateOrderRequest; +import com.zenfulcode.commercify.web.dto.request.order.OrderStatusUpdateRequest; +import com.zenfulcode.commercify.web.dto.response.ErrorResponse; +import com.zenfulcode.commercify.web.dto.response.order.CreateOrderResponse; +import com.zenfulcode.commercify.web.dto.response.order.GetOrderResponse; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/controller/PaymentController.java b/src/main/java/com/zenfulcode/commercify/web/controller/PaymentController.java similarity index 87% rename from src/main/java/com/zenfulcode/commercify/commercify/controller/PaymentController.java rename to src/main/java/com/zenfulcode/commercify/web/controller/PaymentController.java index 3fb7f51..500c1e8 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/controller/PaymentController.java +++ b/src/main/java/com/zenfulcode/commercify/web/controller/PaymentController.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.controller; +package com.zenfulcode.commercify.web.controller; -import com.zenfulcode.commercify.commercify.PaymentStatus; -import com.zenfulcode.commercify.commercify.service.PaymentService; +import com.zenfulcode.commercify.domain.enums.PaymentStatus; +import com.zenfulcode.commercify.service.core.PaymentService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/controller/ProductController.java b/src/main/java/com/zenfulcode/commercify/web/controller/ProductController.java similarity index 90% rename from src/main/java/com/zenfulcode/commercify/commercify/controller/ProductController.java rename to src/main/java/com/zenfulcode/commercify/web/controller/ProductController.java index 6180ac7..9fb7156 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/controller/ProductController.java +++ b/src/main/java/com/zenfulcode/commercify/web/controller/ProductController.java @@ -1,22 +1,22 @@ -package com.zenfulcode.commercify.commercify.controller; +package com.zenfulcode.commercify.web.controller; -import com.zenfulcode.commercify.commercify.api.requests.products.ProductRequest; -import com.zenfulcode.commercify.commercify.api.requests.products.ProductVariantRequest; -import com.zenfulcode.commercify.commercify.api.responses.ErrorResponse; -import com.zenfulcode.commercify.commercify.api.responses.ValidationErrorResponse; -import com.zenfulcode.commercify.commercify.api.responses.products.ProductDeletionErrorResponse; -import com.zenfulcode.commercify.commercify.api.responses.products.ProductUpdateResponse; -import com.zenfulcode.commercify.commercify.dto.ProductDTO; -import com.zenfulcode.commercify.commercify.dto.ProductUpdateResult; -import com.zenfulcode.commercify.commercify.dto.ProductVariantEntityDto; -import com.zenfulcode.commercify.commercify.exception.InvalidSortFieldException; -import com.zenfulcode.commercify.commercify.exception.ProductDeletionException; -import com.zenfulcode.commercify.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.commercify.exception.ProductValidationException; -import com.zenfulcode.commercify.commercify.service.product.ProductService; -import com.zenfulcode.commercify.commercify.service.product.ProductVariantService; -import com.zenfulcode.commercify.commercify.viewmodel.ProductVariantViewModel; -import com.zenfulcode.commercify.commercify.viewmodel.ProductViewModel; +import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; +import com.zenfulcode.commercify.web.dto.request.product.ProductVariantRequest; +import com.zenfulcode.commercify.web.dto.response.ErrorResponse; +import com.zenfulcode.commercify.web.dto.response.ValidationErrorResponse; +import com.zenfulcode.commercify.web.dto.response.product.ProductDeletionErrorResponse; +import com.zenfulcode.commercify.web.dto.response.product.ProductUpdateResponse; +import com.zenfulcode.commercify.web.dto.common.ProductDTO; +import com.zenfulcode.commercify.web.dto.common.ProductUpdateResult; +import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; +import com.zenfulcode.commercify.exception.InvalidSortFieldException; +import com.zenfulcode.commercify.exception.ProductDeletionException; +import com.zenfulcode.commercify.exception.ProductNotFoundException; +import com.zenfulcode.commercify.exception.ProductValidationException; +import com.zenfulcode.commercify.service.core.ProductService; +import com.zenfulcode.commercify.service.ProductVariantService; +import com.zenfulcode.commercify.web.viewmodel.ProductVariantViewModel; +import com.zenfulcode.commercify.web.viewmodel.ProductViewModel; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/controller/UserManagementController.java b/src/main/java/com/zenfulcode/commercify/web/controller/UserManagementController.java similarity index 93% rename from src/main/java/com/zenfulcode/commercify/commercify/controller/UserManagementController.java rename to src/main/java/com/zenfulcode/commercify/web/controller/UserManagementController.java index 039f725..745b92e 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/controller/UserManagementController.java +++ b/src/main/java/com/zenfulcode/commercify/web/controller/UserManagementController.java @@ -1,9 +1,9 @@ -package com.zenfulcode.commercify.commercify.controller; +package com.zenfulcode.commercify.web.controller; -import com.zenfulcode.commercify.commercify.dto.AddressDTO; -import com.zenfulcode.commercify.commercify.dto.UserDTO; -import com.zenfulcode.commercify.commercify.service.UserManagementService; +import com.zenfulcode.commercify.web.dto.common.AddressDTO; +import com.zenfulcode.commercify.web.dto.common.UserDTO; +import com.zenfulcode.commercify.service.core.UserManagementService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/AddressDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/AddressDTO.java similarity index 85% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/AddressDTO.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/AddressDTO.java index 692c3d0..ba103ff 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/AddressDTO.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/AddressDTO.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/CustomerDetailsDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/CustomerDetailsDTO.java similarity index 83% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/CustomerDetailsDTO.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/CustomerDetailsDTO.java index 7636383..177730f 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/CustomerDetailsDTO.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/CustomerDetailsDTO.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDTO.java similarity index 78% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDTO.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDTO.java index d45ab8c..12aa4f4 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDTO.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDTO.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; -import com.zenfulcode.commercify.commercify.OrderStatus; +import com.zenfulcode.commercify.domain.enums.OrderStatus; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDetailsDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDetailsDTO.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDetailsDTO.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDetailsDTO.java index 36330b7..7f1ecb2 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderDetailsDTO.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDetailsDTO.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderLineDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderLineDTO.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/OrderLineDTO.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/OrderLineDTO.java index a3e33d7..5ef5a3b 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/OrderLineDTO.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderLineDTO.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/ProductDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDTO.java similarity index 89% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/ProductDTO.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDTO.java index b1f67f9..ef7387f 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/ProductDTO.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDTO.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/ProductDeletionValidationResult.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDeletionValidationResult.java similarity index 87% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/ProductDeletionValidationResult.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDeletionValidationResult.java index 7cc535b..af5a6b5 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/ProductDeletionValidationResult.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDeletionValidationResult.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/ProductUpdateResult.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductUpdateResult.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/ProductUpdateResult.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/ProductUpdateResult.java index 5afdb4e..83a6ca0 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/ProductUpdateResult.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductUpdateResult.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/ProductVariantEntityDto.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductVariantEntityDto.java similarity index 68% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/ProductVariantEntityDto.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/ProductVariantEntityDto.java index f384f78..46c1e60 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/ProductVariantEntityDto.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductVariantEntityDto.java @@ -1,5 +1,6 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; +import com.zenfulcode.commercify.domain.model.ProductVariant; import lombok.Builder; import lombok.Data; @@ -7,7 +8,7 @@ import java.util.Set; /** - * DTO for {@link com.zenfulcode.commercify.commercify.entity.ProductVariantEntity} + * DTO for {@link ProductVariant} */ @Builder @Data diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/UserDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/UserDTO.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/UserDTO.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/UserDTO.java index ea9c9d9..c16034f 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/UserDTO.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/UserDTO.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/VariantOptionEntityDto.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/VariantOptionEntityDto.java similarity index 61% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/VariantOptionEntityDto.java rename to src/main/java/com/zenfulcode/commercify/web/dto/common/VariantOptionEntityDto.java index e1035a7..e56dc1b 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/VariantOptionEntityDto.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/common/VariantOptionEntityDto.java @@ -1,11 +1,12 @@ -package com.zenfulcode.commercify.commercify.dto; +package com.zenfulcode.commercify.web.dto.common; +import com.zenfulcode.commercify.domain.model.VariantOption; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; /** - * DTO for {@link com.zenfulcode.commercify.commercify.entity.VariantOptionEntity} + * DTO for {@link VariantOption} */ @Builder @Data diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/AddressMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/AddressMapper.java similarity index 58% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/AddressMapper.java rename to src/main/java/com/zenfulcode/commercify/web/dto/mapper/AddressMapper.java index 0c8f37d..79ee70d 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/AddressMapper.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/AddressMapper.java @@ -1,15 +1,15 @@ -package com.zenfulcode.commercify.commercify.dto.mapper; +package com.zenfulcode.commercify.web.dto.mapper; -import com.zenfulcode.commercify.commercify.dto.AddressDTO; -import com.zenfulcode.commercify.commercify.entity.AddressEntity; +import com.zenfulcode.commercify.web.dto.common.AddressDTO; +import com.zenfulcode.commercify.domain.model.Address; import org.springframework.stereotype.Service; import java.util.function.Function; @Service -public class AddressMapper implements Function { +public class AddressMapper implements Function { @Override - public AddressDTO apply(AddressEntity address) { + public AddressDTO apply(Address address) { return AddressDTO.builder() .id(address.getId()) .street(address.getStreet()) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderLineMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderLineMapper.java similarity index 56% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderLineMapper.java rename to src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderLineMapper.java index cf692a5..8f57747 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderLineMapper.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderLineMapper.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.dto.mapper; +package com.zenfulcode.commercify.web.dto.mapper; -import com.zenfulcode.commercify.commercify.dto.OrderLineDTO; -import com.zenfulcode.commercify.commercify.entity.OrderLineEntity; +import com.zenfulcode.commercify.web.dto.common.OrderLineDTO; +import com.zenfulcode.commercify.domain.model.OrderLine; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; @@ -9,9 +9,9 @@ @Service @AllArgsConstructor -public class OrderLineMapper implements Function { +public class OrderLineMapper implements Function { @Override - public OrderLineDTO apply(OrderLineEntity orderLine) { + public OrderLineDTO apply(OrderLine orderLine) { return OrderLineDTO.builder() .id(orderLine.getId()) .quantity(orderLine.getQuantity()) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderMapper.java similarity index 71% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderMapper.java rename to src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderMapper.java index d6d3f34..7f31b1e 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderMapper.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderMapper.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.dto.mapper; +package com.zenfulcode.commercify.web.dto.mapper; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; +import com.zenfulcode.commercify.domain.model.Order; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -9,10 +9,10 @@ @Service @RequiredArgsConstructor -public class OrderMapper implements Function { +public class OrderMapper implements Function { @Override - public OrderDTO apply(OrderEntity order) { + public OrderDTO apply(Order order) { return OrderDTO.builder() .id(order.getId()) .userId(order.getUserId()) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/ProductMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductMapper.java similarity index 69% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/ProductMapper.java rename to src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductMapper.java index c87ede9..36f2a96 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/ProductMapper.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductMapper.java @@ -1,8 +1,8 @@ -package com.zenfulcode.commercify.commercify.dto.mapper; +package com.zenfulcode.commercify.web.dto.mapper; -import com.zenfulcode.commercify.commercify.dto.ProductDTO; -import com.zenfulcode.commercify.commercify.dto.ProductVariantEntityDto; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; +import com.zenfulcode.commercify.domain.model.Product; +import com.zenfulcode.commercify.web.dto.common.ProductDTO; +import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,11 +11,11 @@ @Service @RequiredArgsConstructor -public class ProductMapper implements Function { +public class ProductMapper implements Function { private final ProductVariantMapper variantMapper; @Override - public ProductDTO apply(ProductEntity product) { + public ProductDTO apply(Product product) { final List variants = product.getVariants().stream().map(variantMapper).toList(); return ProductDTO.builder() diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/ProductVariantMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductVariantMapper.java similarity index 70% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/ProductVariantMapper.java rename to src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductVariantMapper.java index 1595d84..368884f 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/ProductVariantMapper.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductVariantMapper.java @@ -1,8 +1,8 @@ -package com.zenfulcode.commercify.commercify.dto.mapper; +package com.zenfulcode.commercify.web.dto.mapper; -import com.zenfulcode.commercify.commercify.dto.ProductVariantEntityDto; -import com.zenfulcode.commercify.commercify.dto.VariantOptionEntityDto; -import com.zenfulcode.commercify.commercify.entity.ProductVariantEntity; +import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; +import com.zenfulcode.commercify.web.dto.common.VariantOptionEntityDto; +import com.zenfulcode.commercify.domain.model.ProductVariant; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,11 +12,11 @@ @Service @RequiredArgsConstructor -public class ProductVariantMapper implements Function { +public class ProductVariantMapper implements Function { private final VariantOptionMapper variantOptionMapper; @Override - public ProductVariantEntityDto apply(ProductVariantEntity productVariant) { + public ProductVariantEntityDto apply(ProductVariant productVariant) { Set options = productVariant.getOptions().stream() .map(variantOptionMapper).collect(Collectors.toSet()); diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/UserMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/UserMapper.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/UserMapper.java rename to src/main/java/com/zenfulcode/commercify/web/dto/mapper/UserMapper.java index 47a6489..e5320bf 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/UserMapper.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/UserMapper.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.dto.mapper; +package com.zenfulcode.commercify.web.dto.mapper; -import com.zenfulcode.commercify.commercify.dto.UserDTO; -import com.zenfulcode.commercify.commercify.entity.UserEntity; +import com.zenfulcode.commercify.web.dto.common.UserDTO; +import com.zenfulcode.commercify.domain.model.User; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Service; @@ -11,11 +11,11 @@ @Service @RequiredArgsConstructor -public class UserMapper implements Function { +public class UserMapper implements Function { private final AddressMapper addressDTOMapper; @Override - public UserDTO apply(UserEntity user) { + public UserDTO apply(User user) { UserDTO.UserDTOBuilder userBuilder = UserDTO.builder() .id(user.getId()) .email(user.getEmail()) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/VariantOptionMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/VariantOptionMapper.java similarity index 59% rename from src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/VariantOptionMapper.java rename to src/main/java/com/zenfulcode/commercify/web/dto/mapper/VariantOptionMapper.java index 7af638c..8f86229 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/dto/mapper/VariantOptionMapper.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/VariantOptionMapper.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.dto.mapper; +package com.zenfulcode.commercify.web.dto.mapper; -import com.zenfulcode.commercify.commercify.dto.VariantOptionEntityDto; -import com.zenfulcode.commercify.commercify.entity.VariantOptionEntity; +import com.zenfulcode.commercify.web.dto.common.VariantOptionEntityDto; +import com.zenfulcode.commercify.domain.model.VariantOption; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -9,10 +9,10 @@ @Service @RequiredArgsConstructor -public class VariantOptionMapper implements Function { +public class VariantOptionMapper implements Function { @Override - public VariantOptionEntityDto apply(VariantOptionEntity product) { + public VariantOptionEntityDto apply(VariantOption product) { return VariantOptionEntityDto.builder() .id(product.getId()) .name(product.getName()) diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/LoginUserRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/LoginUserRequest.java similarity index 53% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/LoginUserRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/auth/LoginUserRequest.java index 7e7d6fd..45f8a83 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/LoginUserRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/LoginUserRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.requests; +package com.zenfulcode.commercify.web.dto.request.auth; public record LoginUserRequest(String email, String password) { } diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/RegisterUserRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/RegisterUserRequest.java similarity index 80% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/RegisterUserRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/auth/RegisterUserRequest.java index 04de06e..9cc77b0 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/RegisterUserRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/RegisterUserRequest.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.api.requests; +package com.zenfulcode.commercify.web.dto.request.auth; -import com.zenfulcode.commercify.commercify.dto.AddressDTO; +import com.zenfulcode.commercify.web.dto.common.AddressDTO; import java.util.UUID; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/CreateOrderLineRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderLineRequest.java similarity index 63% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/CreateOrderLineRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderLineRequest.java index f2a4c6b..3873b57 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/CreateOrderLineRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderLineRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.requests.orders; +package com.zenfulcode.commercify.web.dto.request.order; public record CreateOrderLineRequest( Long productId, diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/CreateOrderRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderRequest.java similarity index 57% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/CreateOrderRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderRequest.java index 6e1f493..0b111e2 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/orders/CreateOrderRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderRequest.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.api.requests.orders; +package com.zenfulcode.commercify.web.dto.request.order; -import com.zenfulcode.commercify.commercify.dto.AddressDTO; -import com.zenfulcode.commercify.commercify.dto.CustomerDetailsDTO; +import com.zenfulcode.commercify.web.dto.common.AddressDTO; +import com.zenfulcode.commercify.web.dto.common.CustomerDetailsDTO; import java.util.List; diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/order/OrderStatusUpdateRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/OrderStatusUpdateRequest.java new file mode 100644 index 0000000..3119d07 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/OrderStatusUpdateRequest.java @@ -0,0 +1,4 @@ +package com.zenfulcode.commercify.web.dto.request.order; + +public record OrderStatusUpdateRequest(String status) { +} diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/PaymentRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/payment/PaymentRequest.java similarity index 80% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/PaymentRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/payment/PaymentRequest.java index 3459997..570f0cd 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/PaymentRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/payment/PaymentRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.requests; +package com.zenfulcode.commercify.web.dto.request.payment; public record PaymentRequest(Long orderId, String currency, diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/CreateVariantOptionRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/CreateVariantOptionRequest.java similarity index 56% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/CreateVariantOptionRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/product/CreateVariantOptionRequest.java index 08282ff..09916ba 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/CreateVariantOptionRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/CreateVariantOptionRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.requests.products; +package com.zenfulcode.commercify.web.dto.request.product; public record CreateVariantOptionRequest( String name, diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/PriceRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/PriceRequest.java similarity index 54% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/PriceRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/product/PriceRequest.java index 096857d..1e95816 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/PriceRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/PriceRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.requests.products; +package com.zenfulcode.commercify.web.dto.request.product; public record PriceRequest( String currency, diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/ProductRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductRequest.java similarity index 78% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/ProductRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductRequest.java index fcd4575..aafaed9 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/ProductRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.requests.products; +package com.zenfulcode.commercify.web.dto.request.product; import java.util.List; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/ProductVariantRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductVariantRequest.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/ProductVariantRequest.java rename to src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductVariantRequest.java index 940973d..5a5f1c7 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/requests/products/ProductVariantRequest.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductVariantRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.requests.products; +package com.zenfulcode.commercify.web.dto.request.product; import java.util.List; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/ErrorResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/ErrorResponse.java similarity index 70% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/ErrorResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/ErrorResponse.java index 4a508f9..47a963b 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/ErrorResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/ErrorResponse.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.responses; +package com.zenfulcode.commercify.web.dto.response; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/ValidationErrorResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/ValidationErrorResponse.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/ValidationErrorResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/ValidationErrorResponse.java index ce2bccd..becfe67 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/ValidationErrorResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/ValidationErrorResponse.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.responses; +package com.zenfulcode.commercify.web.dto.response; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/AuthResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/AuthResponse.java similarity index 77% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/AuthResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/auth/AuthResponse.java index af37219..8208a5f 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/AuthResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/AuthResponse.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.api.responses; +package com.zenfulcode.commercify.web.dto.response.auth; -import com.zenfulcode.commercify.commercify.dto.UserDTO; +import com.zenfulcode.commercify.web.dto.common.UserDTO; public record AuthResponse(UserDTO user, String token, long expiresIn, String message) { public static AuthResponse UserAuthenticated(UserDTO user, String token, long expiresIn) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/JwtErrorResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/JwtErrorResponse.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/JwtErrorResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/auth/JwtErrorResponse.java index e6a487e..8ca857e 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/JwtErrorResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/JwtErrorResponse.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.responses.products; +package com.zenfulcode.commercify.web.dto.response.auth; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/RegisterUserResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/RegisterUserResponse.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/RegisterUserResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/auth/RegisterUserResponse.java index 9f07d8f..31716d6 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/RegisterUserResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/RegisterUserResponse.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.api.responses; +package com.zenfulcode.commercify.web.dto.response.auth; -import com.zenfulcode.commercify.commercify.dto.UserDTO; +import com.zenfulcode.commercify.web.dto.common.UserDTO; public record RegisterUserResponse(UserDTO user, String message) { public static RegisterUserResponse RegistrationFailed(String message) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/orders/CreateOrderResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/order/CreateOrderResponse.java similarity index 72% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/orders/CreateOrderResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/order/CreateOrderResponse.java index 8b95f7c..4c19e3f 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/orders/CreateOrderResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/order/CreateOrderResponse.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.api.responses.orders; +package com.zenfulcode.commercify.web.dto.response.order; -import com.zenfulcode.commercify.commercify.viewmodel.OrderViewModel; +import com.zenfulcode.commercify.web.viewmodel.OrderViewModel; public record CreateOrderResponse( OrderViewModel order, diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/orders/GetOrderResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/order/GetOrderResponse.java similarity index 73% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/orders/GetOrderResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/order/GetOrderResponse.java index 9d63fba..d017418 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/orders/GetOrderResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/order/GetOrderResponse.java @@ -1,10 +1,10 @@ -package com.zenfulcode.commercify.commercify.api.responses.orders; +package com.zenfulcode.commercify.web.dto.response.order; -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; -import com.zenfulcode.commercify.commercify.dto.OrderDetailsDTO; -import com.zenfulcode.commercify.commercify.viewmodel.OrderLineViewModel; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; +import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; +import com.zenfulcode.commercify.web.viewmodel.OrderLineViewModel; import java.time.Instant; import java.util.List; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/CancelPaymentResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/CancelPaymentResponse.java similarity index 91% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/CancelPaymentResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/payment/CancelPaymentResponse.java index 1234805..4dbfab5 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/CancelPaymentResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/CancelPaymentResponse.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.commercify.api.responses; +package com.zenfulcode.commercify.web.dto.response.payment; public record CancelPaymentResponse(boolean success, String message) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/PaymentResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/PaymentResponse.java similarity index 64% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/PaymentResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/payment/PaymentResponse.java index 660505e..6797b10 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/PaymentResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/PaymentResponse.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.api.responses; +package com.zenfulcode.commercify.web.dto.response.payment; -import com.zenfulcode.commercify.commercify.PaymentStatus; +import com.zenfulcode.commercify.domain.enums.PaymentStatus; public record PaymentResponse(Long paymentId, PaymentStatus status, String redirectUrl) { public static PaymentResponse FailedPayment() { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/ProductDeletionErrorResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductDeletionErrorResponse.java similarity index 66% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/ProductDeletionErrorResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductDeletionErrorResponse.java index 2358325..63ba579 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/ProductDeletionErrorResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductDeletionErrorResponse.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.api.responses.products; +package com.zenfulcode.commercify.web.dto.response.product; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/ProductUpdateResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductUpdateResponse.java similarity index 52% rename from src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/ProductUpdateResponse.java rename to src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductUpdateResponse.java index b2f10be..beeb12f 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/api/responses/products/ProductUpdateResponse.java +++ b/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductUpdateResponse.java @@ -1,6 +1,6 @@ -package com.zenfulcode.commercify.commercify.api.responses.products; +package com.zenfulcode.commercify.web.dto.response.product; -import com.zenfulcode.commercify.commercify.viewmodel.ProductViewModel; +import com.zenfulcode.commercify.web.viewmodel.ProductViewModel; import java.util.List; diff --git a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/OrderLineViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderLineViewModel.java similarity index 75% rename from src/main/java/com/zenfulcode/commercify/commercify/viewmodel/OrderLineViewModel.java rename to src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderLineViewModel.java index 165fcf6..473851a 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/OrderLineViewModel.java +++ b/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderLineViewModel.java @@ -1,14 +1,14 @@ -package com.zenfulcode.commercify.commercify.viewmodel; +package com.zenfulcode.commercify.web.viewmodel; -import com.zenfulcode.commercify.commercify.dto.OrderLineDTO; -import com.zenfulcode.commercify.commercify.dto.ProductDTO; +import com.zenfulcode.commercify.web.dto.common.OrderLineDTO; +import com.zenfulcode.commercify.web.dto.common.ProductDTO; public record OrderLineViewModel( String name, String description, - Integer quantity, + int quantity, String imageUrl, - Double unitPrice, + double unitPrice, ProductVariantViewModel variant ) { public static OrderLineViewModel fromDTO(OrderLineDTO orderLineDTO) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/OrderViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderViewModel.java similarity index 79% rename from src/main/java/com/zenfulcode/commercify/commercify/viewmodel/OrderViewModel.java rename to src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderViewModel.java index 7308826..d083965 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/OrderViewModel.java +++ b/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderViewModel.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.viewmodel; +package com.zenfulcode.commercify.web.viewmodel; -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; import java.time.Instant; diff --git a/src/main/java/com/zenfulcode/commercify/web/viewmodel/PriceViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/PriceViewModel.java new file mode 100644 index 0000000..b1423f2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/web/viewmodel/PriceViewModel.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.web.viewmodel; + +public record PriceViewModel(String currency, + double amount) { + +} diff --git a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/ProductVariantViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductVariantViewModel.java similarity index 82% rename from src/main/java/com/zenfulcode/commercify/commercify/viewmodel/ProductVariantViewModel.java rename to src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductVariantViewModel.java index b9c9563..b1b1b61 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/ProductVariantViewModel.java +++ b/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductVariantViewModel.java @@ -1,14 +1,14 @@ -package com.zenfulcode.commercify.commercify.viewmodel; +package com.zenfulcode.commercify.web.viewmodel; import com.fasterxml.jackson.annotation.JsonInclude; -import com.zenfulcode.commercify.commercify.dto.ProductVariantEntityDto; +import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; import java.util.List; import java.util.stream.Collectors; @JsonInclude(JsonInclude.Include.NON_NULL) public record ProductVariantViewModel( - Long id, + long id, String sku, List options ) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/ProductViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductViewModel.java similarity index 85% rename from src/main/java/com/zenfulcode/commercify/commercify/viewmodel/ProductViewModel.java rename to src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductViewModel.java index e046e3d..cae8ef6 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/ProductViewModel.java +++ b/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductViewModel.java @@ -1,18 +1,18 @@ -package com.zenfulcode.commercify.commercify.viewmodel; +package com.zenfulcode.commercify.web.viewmodel; import com.fasterxml.jackson.annotation.JsonInclude; -import com.zenfulcode.commercify.commercify.dto.ProductDTO; +import com.zenfulcode.commercify.web.dto.common.ProductDTO; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) public record ProductViewModel( - Long id, + long id, String name, String description, - Integer stock, + int stock, String imageUrl, - Boolean active, + boolean active, PriceViewModel price, List variants ) { diff --git a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/VariantOptionViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/VariantOptionViewModel.java similarity index 76% rename from src/main/java/com/zenfulcode/commercify/commercify/viewmodel/VariantOptionViewModel.java rename to src/main/java/com/zenfulcode/commercify/web/viewmodel/VariantOptionViewModel.java index fbac540..53cc7a6 100644 --- a/src/main/java/com/zenfulcode/commercify/commercify/viewmodel/VariantOptionViewModel.java +++ b/src/main/java/com/zenfulcode/commercify/web/viewmodel/VariantOptionViewModel.java @@ -1,7 +1,7 @@ -package com.zenfulcode.commercify.commercify.viewmodel; +package com.zenfulcode.commercify.web.viewmodel; import com.fasterxml.jackson.annotation.JsonInclude; -import com.zenfulcode.commercify.commercify.dto.VariantOptionEntityDto; +import com.zenfulcode.commercify.web.dto.common.VariantOptionEntityDto; @JsonInclude(JsonInclude.Include.NON_NULL) public record VariantOptionViewModel( diff --git a/src/test/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderMapperTest.java b/src/test/java/com/zenfulcode/commercify/dto/mapper/OrderMapperTest.java similarity index 60% rename from src/test/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderMapperTest.java rename to src/test/java/com/zenfulcode/commercify/dto/mapper/OrderMapperTest.java index 6a88ad7..70a443e 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/dto/mapper/OrderMapperTest.java +++ b/src/test/java/com/zenfulcode/commercify/dto/mapper/OrderMapperTest.java @@ -1,9 +1,10 @@ -package com.zenfulcode.commercify.commercify.dto.mapper; +package com.zenfulcode.commercify.dto.mapper; -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.OrderLineEntity; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.OrderLine; +import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,14 +24,14 @@ class OrderMapperTest { @InjectMocks private OrderMapper orderMapper; - private OrderEntity orderEntity; + private Order order; @BeforeEach void setUp() { Instant now = Instant.now(); - Set orderLines = new LinkedHashSet<>(); - OrderLineEntity orderLine = OrderLineEntity.builder() + Set orderLines = new LinkedHashSet<>(); + OrderLine orderLine = OrderLine.builder() .id(1L) .productId(1L) .quantity(2) @@ -39,7 +40,7 @@ void setUp() { .build(); orderLines.add(orderLine); - orderEntity = OrderEntity.builder() + order = Order.builder() .id(1L) .userId(1L) .orderLines(orderLines) @@ -54,23 +55,23 @@ void setUp() { @Test @DisplayName("Should map OrderEntity to OrderDTO correctly") void apply_Success() { - OrderDTO result = orderMapper.apply(orderEntity); + OrderDTO result = orderMapper.apply(order); assertNotNull(result); - assertEquals(orderEntity.getId(), result.getId()); - assertEquals(orderEntity.getUserId(), result.getUserId()); - assertEquals(orderEntity.getStatus(), result.getOrderStatus()); - assertEquals(orderEntity.getCurrency(), result.getCurrency()); - assertEquals(orderEntity.getTotalAmount(), result.getTotalAmount()); - assertEquals(orderEntity.getCreatedAt(), result.getCreatedAt()); - assertEquals(orderEntity.getUpdatedAt(), result.getUpdatedAt()); - assertEquals(orderEntity.getOrderLines().size(), result.getOrderLinesAmount()); + assertEquals(order.getId(), result.getId()); + assertEquals(order.getUserId(), result.getUserId()); + assertEquals(order.getStatus(), result.getOrderStatus()); + assertEquals(order.getCurrency(), result.getCurrency()); + assertEquals(order.getTotalAmount(), result.getTotalAmount()); + assertEquals(order.getCreatedAt(), result.getCreatedAt()); + assertEquals(order.getUpdatedAt(), result.getUpdatedAt()); + assertEquals(order.getOrderLines().size(), result.getOrderLinesAmount()); } @Test @DisplayName("Should handle OrderEntity with null values") void apply_HandlesNullValues() { - OrderEntity emptyOrder = OrderEntity.builder() + Order emptyOrder = Order.builder() .id(1L) .build(); @@ -87,9 +88,9 @@ void apply_HandlesNullValues() { @Test @DisplayName("Should handle null totalAmount correctly") void apply_HandlesNullTotalAmount() { - orderEntity.setTotalAmount(null); + order.setTotalAmount(null); - OrderDTO result = orderMapper.apply(orderEntity); + OrderDTO result = orderMapper.apply(order); assertNotNull(result); assertEquals(0.0, result.getTotalAmount()); @@ -98,9 +99,9 @@ void apply_HandlesNullTotalAmount() { @Test @DisplayName("Should handle null orderLines correctly") void apply_HandlesNullOrderLines() { - orderEntity.setOrderLines(null); + order.setOrderLines(null); - OrderDTO result = orderMapper.apply(orderEntity); + OrderDTO result = orderMapper.apply(order); assertNotNull(result); assertEquals(0, result.getOrderLinesAmount()); diff --git a/src/test/java/com/zenfulcode/commercify/commercify/entity/OrderEntityTest.java b/src/test/java/com/zenfulcode/commercify/entity/OrderEntityTest.java similarity index 83% rename from src/test/java/com/zenfulcode/commercify/commercify/entity/OrderEntityTest.java rename to src/test/java/com/zenfulcode/commercify/entity/OrderEntityTest.java index 591cb49..2053cb1 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/entity/OrderEntityTest.java +++ b/src/test/java/com/zenfulcode/commercify/entity/OrderEntityTest.java @@ -1,7 +1,9 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.entity; -import com.zenfulcode.commercify.commercify.OrderStatus; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.OrderLine; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,19 +18,19 @@ class OrderEntityTest { - private OrderEntity order; + private Order order; @BeforeEach void setUp() { - Set orderLines = new HashSet<>(); - OrderLineEntity orderLine = new OrderLineEntity(); + Set orderLines = new HashSet<>(); + OrderLine orderLine = new OrderLine(); orderLine.setProductId(1L); orderLine.setQuantity(2); orderLine.setUnitPrice(99.99); orderLine.setCurrency("USD"); orderLines.add(orderLine); - order = OrderEntity.builder() + order = Order.builder() .id(1L) .userId(1L) .orderLines(orderLines) @@ -56,7 +58,7 @@ void testOrderBuilder() { @DisplayName("Should manage order lines correctly") void testOrderLines() { assertEquals(1, order.getOrderLines().size()); - OrderLineEntity firstLine = order.getOrderLines().stream().findFirst().orElse(null); + OrderLine firstLine = order.getOrderLines().stream().findFirst().orElse(null); assertEquals(2, firstLine.getQuantity()); assertEquals(99.99, firstLine.getUnitPrice()); assertEquals(order, firstLine.getOrder()); diff --git a/src/test/java/com/zenfulcode/commercify/commercify/entity/ProductEntityTest.java b/src/test/java/com/zenfulcode/commercify/entity/ProductEntityTest.java similarity index 86% rename from src/test/java/com/zenfulcode/commercify/commercify/entity/ProductEntityTest.java rename to src/test/java/com/zenfulcode/commercify/entity/ProductEntityTest.java index b4f1a21..dcde14a 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/entity/ProductEntityTest.java +++ b/src/test/java/com/zenfulcode/commercify/entity/ProductEntityTest.java @@ -1,6 +1,7 @@ -package com.zenfulcode.commercify.commercify.entity; +package com.zenfulcode.commercify.entity; +import com.zenfulcode.commercify.domain.model.Product; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -9,11 +10,11 @@ class ProductEntityTest { - private ProductEntity product; + private Product product; @BeforeEach void setUp() { - product = ProductEntity.builder() + product = com.zenfulcode.commercify.domain.model.Product.builder() .id(1L) .name("Test Product") .description("Test Description") @@ -52,7 +53,7 @@ void testProductUpdate() { @Test @DisplayName("Should handle null values") void testNullValues() { - ProductEntity emptyProduct = new ProductEntity(); + Product emptyProduct = new Product(); assertNull(emptyProduct.getId()); assertNull(emptyProduct.getName()); assertNull(emptyProduct.getStock()); diff --git a/src/test/java/com/zenfulcode/commercify/commercify/factory/ProductFactoryTest.java b/src/test/java/com/zenfulcode/commercify/factory/ProductFactoryTest.java similarity index 75% rename from src/test/java/com/zenfulcode/commercify/commercify/factory/ProductFactoryTest.java rename to src/test/java/com/zenfulcode/commercify/factory/ProductFactoryTest.java index 152355a..849a016 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/factory/ProductFactoryTest.java +++ b/src/test/java/com/zenfulcode/commercify/factory/ProductFactoryTest.java @@ -1,8 +1,9 @@ -package com.zenfulcode.commercify.commercify.factory; +package com.zenfulcode.commercify.factory; -import com.zenfulcode.commercify.commercify.api.requests.products.PriceRequest; -import com.zenfulcode.commercify.commercify.api.requests.products.ProductRequest; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; +import com.zenfulcode.commercify.component.factory.ProductFactory; +import com.zenfulcode.commercify.web.dto.request.product.PriceRequest; +import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; +import com.zenfulcode.commercify.domain.model.Product; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -22,8 +23,6 @@ class ProductFactoryTest { private ProductFactory productFactory; private ProductRequest createRequest; - private ProductRequest updateRequest; - private ProductEntity existingProduct; @BeforeEach void setUp() { @@ -42,7 +41,7 @@ void setUp() { "USD", 99.99 ); - updateRequest = new ProductRequest( + ProductRequest updateRequest = new ProductRequest( "Updated Product", "Updated Description", 20, @@ -51,17 +50,6 @@ void setUp() { priceRequest, new ArrayList<>() ); - - existingProduct = ProductEntity.builder() - .id(1L) - .name("Existing Product") - .description("Existing Description") - .stock(5) - .active(true) - .imageUrl("existing-image.jpg") - .currency("USD") - .unitPrice(79.99) - .build(); } @Nested @@ -71,7 +59,7 @@ class CreateProductTests { @Test @DisplayName("Should create product from request") void testCreateFromRequest() { - ProductEntity result = productFactory.createFromRequest(createRequest); + Product result = productFactory.createFromRequest(createRequest); assertNotNull(result); assertEquals("Test Product", result.getName()); @@ -96,7 +84,7 @@ void testCreateFromRequestWithNullStock() { new ArrayList<>() ); - ProductEntity result = productFactory.createFromRequest(requestWithNullStock); + Product result = productFactory.createFromRequest(requestWithNullStock); assertNotNull(result); assertEquals(0, result.getStock()); @@ -116,7 +104,7 @@ void testCreateFromRequestPriceInfo() { new ArrayList<>() ); - ProductEntity result = productFactory.createFromRequest(requestWithDifferentPrice); + Product result = productFactory.createFromRequest(requestWithDifferentPrice); assertEquals("EUR", result.getCurrency()); assertEquals(149.99, result.getUnitPrice()); diff --git a/src/test/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlowTest.java b/src/test/java/com/zenfulcode/commercify/flow/OrderStateFlowTest.java similarity index 95% rename from src/test/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlowTest.java rename to src/test/java/com/zenfulcode/commercify/flow/OrderStateFlowTest.java index df22a3e..da51e5a 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/flow/OrderStateFlowTest.java +++ b/src/test/java/com/zenfulcode/commercify/flow/OrderStateFlowTest.java @@ -1,7 +1,9 @@ -package com.zenfulcode.commercify.commercify.flow; +package com.zenfulcode.commercify.flow; -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.PaymentStatus; +import com.zenfulcode.commercify.component.flow.OrderStateFlow; +import com.zenfulcode.commercify.component.flow.PaymentStateFlow; +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.domain.enums.PaymentStatus; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayServiceTest.java b/src/test/java/com/zenfulcode/commercify/integration/mobilepay/MobilePayServiceTest.java similarity index 82% rename from src/test/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayServiceTest.java rename to src/test/java/com/zenfulcode/commercify/integration/mobilepay/MobilePayServiceTest.java index f90bd52..33f45a0 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/integration/mobilepay/MobilePayServiceTest.java +++ b/src/test/java/com/zenfulcode/commercify/integration/mobilepay/MobilePayServiceTest.java @@ -1,10 +1,12 @@ -package com.zenfulcode.commercify.commercify.integration.mobilepay; - -import com.zenfulcode.commercify.commercify.PaymentStatus; -import com.zenfulcode.commercify.commercify.entity.PaymentEntity; -import com.zenfulcode.commercify.commercify.exception.PaymentProcessingException; -import com.zenfulcode.commercify.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.commercify.service.PaymentService; +package com.zenfulcode.commercify.integration.mobilepay; + +import com.zenfulcode.commercify.domain.enums.PaymentStatus; +import com.zenfulcode.commercify.domain.model.Payment; +import com.zenfulcode.commercify.exception.PaymentProcessingException; +import com.zenfulcode.commercify.repository.PaymentRepository; +import com.zenfulcode.commercify.service.core.PaymentService; +import com.zenfulcode.commercify.service.integration.mobilepay.MobilePayService; +import com.zenfulcode.commercify.service.integration.mobilepay.MobilePayTokenService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -39,12 +41,12 @@ class MobilePayServiceTest { @InjectMocks private MobilePayService mobilePayService; - private PaymentEntity payment; + private Payment payment; private static final String PAYMENT_REFERENCE = "test-reference"; @BeforeEach void setUp() { - payment = PaymentEntity.builder() + payment = Payment.builder() .id(1L) .orderId(1L) .mobilePayReference(PAYMENT_REFERENCE) diff --git a/src/test/java/com/zenfulcode/commercify/commercify/service/AuthenticationServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/AuthenticationServiceTest.java similarity index 77% rename from src/test/java/com/zenfulcode/commercify/commercify/service/AuthenticationServiceTest.java rename to src/test/java/com/zenfulcode/commercify/service/AuthenticationServiceTest.java index 06feca8..8f4791e 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/service/AuthenticationServiceTest.java +++ b/src/test/java/com/zenfulcode/commercify/service/AuthenticationServiceTest.java @@ -1,15 +1,18 @@ -package com.zenfulcode.commercify.commercify.service; - - -import com.zenfulcode.commercify.commercify.api.requests.LoginUserRequest; -import com.zenfulcode.commercify.commercify.api.requests.RegisterUserRequest; -import com.zenfulcode.commercify.commercify.dto.AddressDTO; -import com.zenfulcode.commercify.commercify.dto.UserDTO; -import com.zenfulcode.commercify.commercify.dto.mapper.UserMapper; -import com.zenfulcode.commercify.commercify.entity.UserEntity; -import com.zenfulcode.commercify.commercify.repository.AddressRepository; -import com.zenfulcode.commercify.commercify.repository.UserRepository; -import com.zenfulcode.commercify.commercify.service.email.EmailConfirmationService; +package com.zenfulcode.commercify.service; + + +import com.zenfulcode.commercify.web.dto.request.auth.LoginUserRequest; +import com.zenfulcode.commercify.web.dto.request.auth.RegisterUserRequest; +import com.zenfulcode.commercify.web.dto.common.AddressDTO; +import com.zenfulcode.commercify.web.dto.common.UserDTO; +import com.zenfulcode.commercify.web.dto.mapper.UserMapper; +import com.zenfulcode.commercify.domain.model.User; +import com.zenfulcode.commercify.repository.AddressRepository; +import com.zenfulcode.commercify.repository.UserRepository; +import com.zenfulcode.commercify.service.authentication.AuthenticationService; +import com.zenfulcode.commercify.service.authentication.JwtService; +import com.zenfulcode.commercify.service.core.UserManagementService; +import com.zenfulcode.commercify.service.email.EmailConfirmationService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -58,7 +61,7 @@ class AuthenticationServiceTest { private AuthenticationService authenticationService; private RegisterUserRequest registerRequest; - private UserEntity userEntity; + private User user; private UserDTO userDTO; private LoginUserRequest loginRequest; @@ -81,7 +84,7 @@ void setUp() { shippingAddress ); - userEntity = UserEntity.builder() + user = User.builder() .id(1L) .email("test@example.com") .password("encoded_password") @@ -108,8 +111,8 @@ void setUp() { void registerUser_Success() { when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); when(passwordEncoder.encode(anyString())).thenReturn("encoded_password"); - when(userRepository.save(any(UserEntity.class))).thenReturn(userEntity); - when(userMapper.apply(any(UserEntity.class))).thenReturn(userDTO); + when(userRepository.save(any(User.class))).thenReturn(user); + when(userMapper.apply(any(User.class))).thenReturn(userDTO); UserDTO result = authenticationService.registerUser(registerRequest); @@ -119,8 +122,8 @@ void registerUser_Success() { assertEquals("Doe", result.getLastName()); verify(userRepository).findByEmail("test@example.com"); - verify(userRepository).save(any(UserEntity.class)); - verify(userMapper).apply(any(UserEntity.class)); + verify(userRepository).save(any(User.class)); + verify(userMapper).apply(any(User.class)); } @Test @@ -133,8 +136,8 @@ void registerUser_NoPasswordProvided_ShouldSetDefaultPassword() { when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword"); - when(userRepository.save(any(UserEntity.class))).thenReturn(userEntity); - when(userMapper.apply(any(UserEntity.class))).thenReturn(userDTO); + when(userRepository.save(any(User.class))).thenReturn(user); + when(userMapper.apply(any(User.class))).thenReturn(userDTO); // Act UserDTO result = authenticationService.registerUser(request); @@ -143,25 +146,25 @@ void registerUser_NoPasswordProvided_ShouldSetDefaultPassword() { assertNotNull(result); assertEquals("test@example.com", result.getEmail()); verify(passwordEncoder, times(1)).encode(anyString()); - verify(userRepository, times(1)).save(any(UserEntity.class)); + verify(userRepository, times(1)).save(any(User.class)); } @Test @DisplayName("Should throw exception when registering with existing email") void registerUser_ExistingEmail() { - when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(userEntity)); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); assertThrows(RuntimeException.class, () -> authenticationService.registerUser(registerRequest)); verify(userRepository).findByEmail("test@example.com"); - verify(userRepository, never()).save(any(UserEntity.class)); + verify(userRepository, never()).save(any(User.class)); } @Test @DisplayName("Should successfully authenticate user") void authenticate_Success() { - when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(userEntity)); - when(userMapper.apply(any(UserEntity.class))).thenReturn(userDTO); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); + when(userMapper.apply(any(User.class))).thenReturn(userDTO); when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true); UserDTO result = authenticationService.authenticate(loginRequest); @@ -178,8 +181,8 @@ void authenticate_Success() { void getAuthenticatedUser_Success() { String jwt = "Bearer valid_jwt_token"; when(jwtService.extractUsername(anyString())).thenReturn("test@example.com"); - when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(userEntity)); - when(userMapper.apply(any(UserEntity.class))).thenReturn(userDTO); + when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); + when(userMapper.apply(any(User.class))).thenReturn(userDTO); UserDTO result = authenticationService.getAuthenticatedUser(jwt); diff --git a/src/test/java/com/zenfulcode/commercify/commercify/service/PaymentServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/PaymentServiceTest.java similarity index 84% rename from src/test/java/com/zenfulcode/commercify/commercify/service/PaymentServiceTest.java rename to src/test/java/com/zenfulcode/commercify/service/PaymentServiceTest.java index b74fd23..c8b8af9 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/service/PaymentServiceTest.java +++ b/src/test/java/com/zenfulcode/commercify/service/PaymentServiceTest.java @@ -1,13 +1,14 @@ -package com.zenfulcode.commercify.commercify.service; - -import com.zenfulcode.commercify.commercify.PaymentStatus; -import com.zenfulcode.commercify.commercify.dto.OrderDetailsDTO; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.PaymentEntity; -import com.zenfulcode.commercify.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.commercify.service.email.EmailService; -import com.zenfulcode.commercify.commercify.service.order.OrderService; +package com.zenfulcode.commercify.service; + +import com.zenfulcode.commercify.domain.enums.PaymentStatus; +import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.Payment; +import com.zenfulcode.commercify.repository.OrderRepository; +import com.zenfulcode.commercify.repository.PaymentRepository; +import com.zenfulcode.commercify.service.core.PaymentService; +import com.zenfulcode.commercify.service.email.EmailService; +import com.zenfulcode.commercify.service.core.OrderService; import jakarta.mail.MessagingException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -42,20 +43,20 @@ class PaymentServiceTest { @InjectMocks private PaymentService paymentService; - private PaymentEntity payment; - private OrderEntity order; + private Payment payment; + private Order order; private OrderDetailsDTO orderDetails; @BeforeEach void setUp() { - payment = PaymentEntity.builder() + payment = Payment.builder() .id(1L) .orderId(1L) .status(PaymentStatus.PENDING) .totalAmount(199.99) .build(); - order = OrderEntity.builder() + order = Order.builder() .id(1L) .userId(1L) .totalAmount(199.99) @@ -68,7 +69,7 @@ void setUp() { @DisplayName("Should update payment status successfully") void handlePaymentStatusUpdate_Success() { when(paymentRepository.findByOrderId(1L)).thenReturn(Optional.of(payment)); - when(paymentRepository.save(any(PaymentEntity.class))).thenReturn(payment); + when(paymentRepository.save(any(Payment.class))).thenReturn(payment); when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); paymentService.handlePaymentStatusUpdate(1L, PaymentStatus.PAID); diff --git a/src/test/java/com/zenfulcode/commercify/commercify/service/UserAddressServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/UserAddressServiceTest.java similarity index 74% rename from src/test/java/com/zenfulcode/commercify/commercify/service/UserAddressServiceTest.java rename to src/test/java/com/zenfulcode/commercify/service/UserAddressServiceTest.java index ce06c65..1230910 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/service/UserAddressServiceTest.java +++ b/src/test/java/com/zenfulcode/commercify/service/UserAddressServiceTest.java @@ -1,13 +1,14 @@ -package com.zenfulcode.commercify.commercify.service; - -import com.zenfulcode.commercify.commercify.dto.AddressDTO; -import com.zenfulcode.commercify.commercify.dto.UserDTO; -import com.zenfulcode.commercify.commercify.dto.mapper.AddressMapper; -import com.zenfulcode.commercify.commercify.dto.mapper.UserMapper; -import com.zenfulcode.commercify.commercify.entity.AddressEntity; -import com.zenfulcode.commercify.commercify.entity.UserEntity; -import com.zenfulcode.commercify.commercify.repository.AddressRepository; -import com.zenfulcode.commercify.commercify.repository.UserRepository; +package com.zenfulcode.commercify.service; + +import com.zenfulcode.commercify.web.dto.common.AddressDTO; +import com.zenfulcode.commercify.web.dto.common.UserDTO; +import com.zenfulcode.commercify.web.dto.mapper.AddressMapper; +import com.zenfulcode.commercify.web.dto.mapper.UserMapper; +import com.zenfulcode.commercify.domain.model.Address; +import com.zenfulcode.commercify.domain.model.User; +import com.zenfulcode.commercify.repository.AddressRepository; +import com.zenfulcode.commercify.repository.UserRepository; +import com.zenfulcode.commercify.service.core.UserManagementService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -43,21 +44,21 @@ class UserAddressServiceTest { @InjectMocks private UserManagementService userManagementService; - private UserEntity user; - private AddressEntity shippingAddress; + private User user; + private Address shippingAddress; private AddressDTO address; private UserDTO userDTO; @BeforeEach void setUp() { - user = UserEntity.builder() + user = User.builder() .id(1L) .email("test@example.com") .firstName("John") .lastName("Doe") .build(); - shippingAddress = AddressEntity.builder() + shippingAddress = Address.builder() .id(1L) .street("123 Ship St") .city("Ship City") @@ -90,14 +91,14 @@ class ShippingAddressTests { @DisplayName("Should set shipping address successfully") void setShippingAddress_Success() { when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(userRepository.save(any(UserEntity.class))).thenReturn(user); + when(userRepository.save(any(User.class))).thenReturn(user); userManagementService.setDefaultAddress(1L, address); - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(UserEntity.class); + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); verify(userRepository).save(userCaptor.capture()); - AddressEntity savedAddress = userCaptor.getValue().getDefaultAddress(); + Address savedAddress = userCaptor.getValue().getDefaultAddress(); assertEquals(address.getStreet(), savedAddress.getStreet()); assertEquals(address.getCity(), savedAddress.getCity()); assertEquals(address.getState(), savedAddress.getState()); @@ -110,8 +111,8 @@ void setShippingAddress_Success() { void removeShippingAddress_Success() { user.setDefaultAddress(shippingAddress); when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(userRepository.save(any(UserEntity.class))).thenReturn(user); - when(userMapper.apply(any(UserEntity.class))).thenReturn(userDTO); + when(userRepository.save(any(User.class))).thenReturn(user); + when(userMapper.apply(any(User.class))).thenReturn(userDTO); userManagementService.removeDefaultAddress(1L); diff --git a/src/test/java/com/zenfulcode/commercify/commercify/service/order/OrderServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/order/OrderServiceTest.java similarity index 74% rename from src/test/java/com/zenfulcode/commercify/commercify/service/order/OrderServiceTest.java rename to src/test/java/com/zenfulcode/commercify/service/order/OrderServiceTest.java index 4067a25..9c09e06 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/service/order/OrderServiceTest.java +++ b/src/test/java/com/zenfulcode/commercify/service/order/OrderServiceTest.java @@ -1,22 +1,23 @@ -package com.zenfulcode.commercify.commercify.service.order; - -import com.zenfulcode.commercify.commercify.OrderStatus; -import com.zenfulcode.commercify.commercify.api.requests.orders.CreateOrderLineRequest; -import com.zenfulcode.commercify.commercify.api.requests.orders.CreateOrderRequest; -import com.zenfulcode.commercify.commercify.dto.AddressDTO; -import com.zenfulcode.commercify.commercify.dto.CustomerDetailsDTO; -import com.zenfulcode.commercify.commercify.dto.OrderDTO; -import com.zenfulcode.commercify.commercify.dto.ProductDTO; -import com.zenfulcode.commercify.commercify.dto.mapper.OrderMapper; -import com.zenfulcode.commercify.commercify.entity.OrderEntity; -import com.zenfulcode.commercify.commercify.entity.OrderLineEntity; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; -import com.zenfulcode.commercify.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.commercify.repository.OrderShippingInfoRepository; -import com.zenfulcode.commercify.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.commercify.service.StockManagementService; +package com.zenfulcode.commercify.service.order; + +import com.zenfulcode.commercify.domain.enums.OrderStatus; +import com.zenfulcode.commercify.domain.model.Order; +import com.zenfulcode.commercify.domain.model.OrderLine; +import com.zenfulcode.commercify.domain.model.Product; +import com.zenfulcode.commercify.exception.OrderNotFoundException; +import com.zenfulcode.commercify.exception.ProductNotFoundException; +import com.zenfulcode.commercify.repository.OrderRepository; +import com.zenfulcode.commercify.repository.OrderShippingInfoRepository; +import com.zenfulcode.commercify.repository.ProductRepository; +import com.zenfulcode.commercify.service.StockManagementService; +import com.zenfulcode.commercify.service.core.OrderService; +import com.zenfulcode.commercify.service.validations.OrderValidationService; +import com.zenfulcode.commercify.web.dto.common.AddressDTO; +import com.zenfulcode.commercify.web.dto.common.CustomerDetailsDTO; +import com.zenfulcode.commercify.web.dto.common.OrderDTO; +import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; +import com.zenfulcode.commercify.web.dto.request.order.CreateOrderLineRequest; +import com.zenfulcode.commercify.web.dto.request.order.CreateOrderRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -51,8 +52,6 @@ class OrderServiceTest { @Mock private OrderValidationService validationService; @Mock - private OrderCalculationService calculationService; - @Mock private StockManagementService stockService; @Mock private OrderShippingInfoRepository orderShippingInfoRepository; @@ -60,18 +59,14 @@ class OrderServiceTest { @InjectMocks private OrderService orderService; - private OrderEntity orderEntity; + private Order order; private OrderDTO orderDTO; private CreateOrderRequest createOrderRequest; - private ProductEntity productEntity; - private ProductDTO productDTO; - private AddressDTO addressDTO; - - private CustomerDetailsDTO customerDetailsDTO; + private Product productEntity; @BeforeEach void setUp() { - productEntity = ProductEntity.builder() + productEntity = com.zenfulcode.commercify.domain.model.Product.builder() .id(1L) .name("Test Product") .active(true) @@ -80,7 +75,7 @@ void setUp() { .currency("USD") .build(); - OrderLineEntity orderLine = OrderLineEntity.builder() + OrderLine orderLine = OrderLine.builder() .id(1L) .productId(1L) .quantity(2) @@ -88,14 +83,14 @@ void setUp() { .currency("USD") .build(); - customerDetailsDTO = CustomerDetailsDTO.builder() + CustomerDetailsDTO customerDetailsDTO = CustomerDetailsDTO.builder() .firstName("Test") .lastName("User") .email("test@email.com") .phone("1234567890") .build(); - orderEntity = OrderEntity.builder() + order = Order.builder() .id(1L) .userId(1L) .status(OrderStatus.PENDING) @@ -113,7 +108,7 @@ void setUp() { .totalAmount(199.98) .build(); - addressDTO = AddressDTO.builder() + AddressDTO addressDTO = AddressDTO.builder() .street("Test Street") .city("Test City") .state("Test State") @@ -133,8 +128,7 @@ class CreateOrderTests { @DisplayName("Should create order successfully") void createOrder_Success() { when(productRepository.findAllById(any())).thenReturn(List.of(productEntity)); - when(calculationService.calculateTotalAmount(any())).thenReturn(199.98); - when(orderRepository.save(any())).thenReturn(orderEntity); + when(orderRepository.save(any())).thenReturn(order); when(orderMapper.apply(any())).thenReturn(orderDTO); OrderDTO result = orderService.createOrder(1L, createOrderRequest); @@ -162,12 +156,12 @@ class UpdateOrderStatusTests { @Test @DisplayName("Should update order status successfully") void updateOrderStatus_Success() { - when(orderRepository.findById(1L)).thenReturn(Optional.of(orderEntity)); - when(orderRepository.save(any())).thenReturn(orderEntity); + when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); + when(orderRepository.save(any())).thenReturn(order); assertDoesNotThrow(() -> orderService.updateOrderStatus(1L, OrderStatus.CONFIRMED)); - assertEquals(OrderStatus.CONFIRMED, orderEntity.getStatus()); + assertEquals(OrderStatus.CONFIRMED, order.getStatus()); } @Test @@ -188,7 +182,7 @@ class GetOrdersTests { @DisplayName("Should get orders by user ID") void getOrdersByUserId_Success() { PageRequest pageRequest = PageRequest.of(0, 10); - Page orderPage = new PageImpl<>(List.of(orderEntity)); + Page orderPage = new PageImpl<>(List.of(order)); when(orderRepository.findByUserId(1L, pageRequest)).thenReturn(orderPage); when(orderMapper.apply(any())).thenReturn(orderDTO); @@ -204,7 +198,7 @@ void getOrdersByUserId_Success() { @DisplayName("Should get all orders") void getAllOrders_Success() { PageRequest pageRequest = PageRequest.of(0, 10); - Page orderPage = new PageImpl<>(List.of(orderEntity)); + Page orderPage = new PageImpl<>(List.of(order)); when(orderRepository.findAll(pageRequest)).thenReturn(orderPage); when(orderMapper.apply(any())).thenReturn(orderDTO); @@ -223,13 +217,13 @@ class CancelOrderTests { @Test @DisplayName("Should cancel order successfully") void cancelOrder_Success() { - when(orderRepository.findById(1L)).thenReturn(Optional.of(orderEntity)); - doNothing().when(validationService).validateOrderCancellation(orderEntity); - when(orderRepository.save(any())).thenReturn(orderEntity); + when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); + doNothing().when(validationService).validateOrderCancellation(order); + when(orderRepository.save(any())).thenReturn(order); assertDoesNotThrow(() -> orderService.cancelOrder(1L)); - assertEquals(OrderStatus.CANCELLED, orderEntity.getStatus()); - verify(stockService).restoreStockLevels(orderEntity.getOrderLines()); + assertEquals(OrderStatus.CANCELLED, order.getStatus()); + verify(stockService).restoreStockLevels(order.getOrderLines()); } @Test diff --git a/src/test/java/com/zenfulcode/commercify/commercify/service/product/ProductServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/product/ProductServiceTest.java similarity index 87% rename from src/test/java/com/zenfulcode/commercify/commercify/service/product/ProductServiceTest.java rename to src/test/java/com/zenfulcode/commercify/service/product/ProductServiceTest.java index 8a584f5..70ff9d2 100644 --- a/src/test/java/com/zenfulcode/commercify/commercify/service/product/ProductServiceTest.java +++ b/src/test/java/com/zenfulcode/commercify/service/product/ProductServiceTest.java @@ -1,13 +1,17 @@ -package com.zenfulcode.commercify.commercify.service.product; - -import com.zenfulcode.commercify.commercify.api.requests.products.PriceRequest; -import com.zenfulcode.commercify.commercify.api.requests.products.ProductRequest; -import com.zenfulcode.commercify.commercify.dto.ProductDTO; -import com.zenfulcode.commercify.commercify.dto.mapper.ProductMapper; -import com.zenfulcode.commercify.commercify.entity.ProductEntity; -import com.zenfulcode.commercify.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.commercify.factory.ProductFactory; -import com.zenfulcode.commercify.commercify.repository.ProductRepository; +package com.zenfulcode.commercify.service.product; + +import com.zenfulcode.commercify.web.dto.request.product.PriceRequest; +import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; +import com.zenfulcode.commercify.domain.model.Product; +import com.zenfulcode.commercify.web.dto.common.ProductDTO; +import com.zenfulcode.commercify.web.dto.mapper.ProductMapper; +import com.zenfulcode.commercify.exception.ProductNotFoundException; +import com.zenfulcode.commercify.component.factory.ProductFactory; +import com.zenfulcode.commercify.repository.ProductRepository; +import com.zenfulcode.commercify.service.ProductDeletionService; +import com.zenfulcode.commercify.service.core.ProductService; +import com.zenfulcode.commercify.service.validations.ProductValidationService; +import com.zenfulcode.commercify.service.ProductVariantService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -45,13 +49,13 @@ class ProductServiceTest { @InjectMocks private ProductService productService; - private ProductEntity productEntity; + private Product productEntity; private ProductDTO productDTO; private ProductRequest productRequest; @BeforeEach void setUp() { - productEntity = ProductEntity.builder() + productEntity = com.zenfulcode.commercify.domain.model.Product.builder() .id(1L) .name("Test Product") .description("Test Description") @@ -134,8 +138,8 @@ void getProductById_NotFound() { @DisplayName("Should get all active products") void getActiveProducts_Success() { PageRequest pageRequest = PageRequest.of(0, 10); - List products = List.of(productEntity); - Page productPage = new PageImpl<>(products); + List products = List.of(productEntity); + Page productPage = new PageImpl<>(products); when(productRepository.queryAllByActiveTrue(pageRequest)).thenReturn(productPage); when(productMapper.apply(any())).thenReturn(productDTO); From a6844a72f93b26a74104b885cace1866a74bff6b Mon Sep 17 00:00:00 2001 From: GustavH Date: Thu, 2 Jan 2025 12:14:24 +0100 Subject: [PATCH 02/57] entire products module rewrite, not finished --- .../api/product/ProductController.java | 171 ++++++++++ .../api/product/dto/ProductDtoMapper.java | 127 ++++++++ .../dto/request/AddVariantsRequest.java | 8 + .../dto/request/AdjustInventoryRequest.java | 8 + .../dto/request/CreateProductRequest.java | 12 + .../dto/request/CreateVariantRequest.java | 11 + .../api/product/dto/request/PriceRequest.java | 7 + .../dto/request/ProductVariantRequest.java | 14 + .../dto/request/UpdateProductRequest.java | 10 + .../request/UpdateVariantPricesRequest.java | 8 + .../dto/request/VariantOptionRequest.java | 6 + .../request/VariantPriceUpdateRequest.java | 7 + .../dto/response/CreateProductResponse.java | 6 + .../api/product/dto/response/PageInfo.java | 25 ++ .../dto/response/PagedProductResponse.java | 9 + .../dto/response/ProductDetailResponse.java | 14 + .../dto/response/ProductSummaryResponse.java | 24 ++ .../ProductVariantSummaryResponse.java | 17 + .../dto/response/UpdateProductResponse.java | 6 + .../product/mapper/ProductResponseMapper.java | 88 ++++++ .../commercify/component/AdminDataLoader.java | 56 ---- .../component/factory/ProductFactory.java | 23 -- .../component/flow/OrderStateFlow.java | 52 --- .../component/flow/PaymentStateFlow.java | 48 --- .../config/JwtAuthenticationFilter.java | 2 - .../commercify/domain/enums/OrderStatus.java | 12 - .../domain/enums/PaymentProvider.java | 5 - .../domain/enums/PaymentStatus.java | 12 - .../commercify/domain/model/Address.java | 44 --- .../domain/model/ConfirmationToken.java | 48 --- .../commercify/domain/model/Order.java | 77 ----- .../commercify/domain/model/OrderLine.java | 68 ---- .../domain/model/OrderShippingInfo.java | 50 --- .../commercify/domain/model/Payment.java | 66 ---- .../commercify/domain/model/Product.java | 65 ---- .../domain/model/ProductVariant.java | 55 ---- .../commercify/domain/model/User.java | 99 ------ .../domain/model/VariantOption.java | 39 --- .../exception/GlobalExceptionHandler.java | 55 ---- .../exception/InsufficientStockException.java | 7 - .../exception/InvalidSortFieldException.java | 11 - .../exception/OrderNotFoundException.java | 7 - .../exception/OrderValidationException.java | 11 - .../exception/PaymentProcessingException.java | 7 - .../exception/PriceNotFoundException.java | 7 - .../exception/ProductDeletionException.java | 18 -- .../exception/ProductNotFoundException.java | 7 - .../exception/ProductValidationException.java | 16 - .../exception/StripeOperationException.java | 8 - .../command/AddProductVariantsCommand.java | 12 + .../command/AdjustInventoryCommand.java | 12 + .../command/CreateProductCommand.java | 14 + .../command/DeactivateProductCommand.java | 8 + .../command/DeleteProductCommand.java | 8 + .../command/UpdateProductCommand.java | 9 + .../command/UpdateVariantPricesCommand.java | 12 + .../application/query/ProductQuery.java | 26 ++ .../application/query/ProductQueryType.java | 8 + .../service/ProductApplicationService.java | 170 ++++++++++ .../domain/event/LargeStockIncreaseEvent.java | 18 ++ .../product/domain/event/LowStockEvent.java | 17 + .../domain/event/ProductCreatedEvent.java | 19 ++ .../domain/event/StockCorrectionEvent.java | 18 ++ .../exception/InsufficientStockException.java | 20 ++ .../exception/ProductDeletionException.java | 11 + .../ProductModificationException.java | 9 + .../exception/ProductNotFoundException.java | 10 + .../exception/ProductValidationException.java | 11 + .../exception/VariantNotFoundException.java | 9 + .../product/domain/model/Product.java | 152 +++++++++ .../product/domain/model/ProductVariant.java | 95 ++++++ .../product/domain/model/VariantOption.java | 49 +++ .../policies/ProductInventoryPolicy.java | 11 + .../domain/policies/ProductPricingPolicy.java | 13 + .../domain/repository/ProductRepository.java | 27 ++ .../repository/ProductVariantRepository.java | 18 ++ .../DefaultProductInventoryPolicy.java | 54 ++++ .../service/DefaultProductPricingPolicy.java | 58 ++++ .../domain/service/ProductDomainService.java | 193 +++++++++++ .../domain/service/ProductFactory.java | 104 ++++++ .../product/domain/service/SkuGenerator.java | 29 ++ .../domain/valueobject/CategoryId.java | 30 ++ .../valueobject/InventoryAdjustment.java | 8 + .../valueobject/InventoryAdjustmentType.java | 7 + .../ProductDeletionValidation.java | 8 + .../product/domain/valueobject/ProductId.java | 30 ++ .../valueobject/ProductSpecification.java | 17 + .../domain/valueobject/ProductUpdateSpec.java | 31 ++ .../domain/valueobject/VariantOption.java | 6 + .../valueobject/VariantPriceUpdate.java | 8 + .../valueobject/VariantSpecification.java | 13 + .../persistence/JpaProductRepository.java | 61 ++++ .../JpaProductVariantRepository.java | 43 +++ .../SpringDataJpaProductRepository.java | 18 ++ .../SpringDataJpaVariantRepository.java | 13 + .../repository/AddressRepository.java | 7 - .../ConfirmationTokenRepository.java | 13 - .../repository/OrderLineRepository.java | 32 -- .../repository/OrderRepository.java | 14 - .../OrderShippingInfoRepository.java | 7 - .../repository/PaymentRepository.java | 14 - .../repository/ProductRepository.java | 12 - .../repository/ProductVariantRepository.java | 10 - .../commercify/repository/UserRepository.java | 12 - .../service/ProductDeletionService.java | 55 ---- .../service/ProductVariantService.java | 132 -------- .../service/StockManagementService.java | 56 ---- .../authentication/AuthenticationService.java | 96 ------ .../service/authentication/JwtService.java | 89 ------ .../commercify/service/core/OrderService.java | 296 ----------------- .../service/core/PaymentService.java | 57 ---- .../service/core/ProductService.java | 118 ------- .../service/core/UserManagementService.java | 113 ------- .../email/EmailConfirmationService.java | 76 ----- .../service/email/EmailService.java | 140 -------- .../mobilepay/MobilePayController.java | 54 ---- .../mobilepay/MobilePayService.java | 212 ------------- .../mobilepay/MobilePayTokenService.java | 147 --------- .../integration/stripe/StripeConfig.java | 27 -- .../integration/stripe/StripeController.java | 33 -- .../integration/stripe/StripeService.java | 109 ------- .../stripe/StripeWebhookHandler.java | 63 ---- .../validations/OrderValidationService.java | 102 ------ .../validations/ProductValidationService.java | 77 ----- .../shared/domain/event/DomainEvent.java | 21 ++ .../domain/event/DomainEventHandler.java | 7 + .../domain/event/DomainEventPublisher.java | 8 + .../shared/domain/event/DomainEventStore.java | 8 + .../domain/exception/DomainException.java | 12 + .../DomainInvariantViolationException.java | 14 + .../exception/DomainValidationException.java | 17 + .../exception/EntityNotFoundException.java | 16 + .../EventDeserializationException.java | 7 + .../EventSerializationException.java | 7 + .../shared/domain/model/AggregateRoot.java | 25 ++ .../commercify/shared/domain/model/Money.java | 45 +++ .../shared/domain/model/StoredEvent.java | 42 +++ .../service/DefaultDomainEventPublisher.java | 44 +++ .../domain/service/EventSerializer.java | 58 ++++ .../domain/service/EventTypeResolver.java | 25 ++ .../persistence/EnhancedEventStore.java | 60 ++++ .../persistence/EventStoreRepository.java | 55 ++++ .../persistence/JpaDomainEventStore.java | 60 ++++ .../persistence/ProductIdConverter.java | 19 ++ .../shared/interfaces/ApiResponse.java | 148 +++++++++ .../exception/GlobalExceptionHandler.java | 53 ++++ .../controller/AuthenticationController.java | 58 ---- .../web/controller/OrderController.java | 208 ------------ .../web/controller/PaymentController.java | 38 --- .../web/controller/ProductController.java | 299 ------------------ .../controller/UserManagementController.java | 83 ----- .../commercify/web/dto/common/AddressDTO.java | 17 - .../web/dto/common/CustomerDetailsDTO.java | 15 - .../commercify/web/dto/common/OrderDTO.java | 22 -- .../web/dto/common/OrderDetailsDTO.java | 18 -- .../web/dto/common/OrderLineDTO.java | 20 -- .../commercify/web/dto/common/ProductDTO.java | 22 -- .../ProductDeletionValidationResult.java | 18 -- .../web/dto/common/ProductUpdateResult.java | 19 -- .../dto/common/ProductVariantEntityDto.java | 22 -- .../commercify/web/dto/common/UserDTO.java | 21 -- .../dto/common/VariantOptionEntityDto.java | 18 -- .../web/dto/mapper/AddressMapper.java | 22 -- .../web/dto/mapper/OrderLineMapper.java | 22 -- .../web/dto/mapper/OrderMapper.java | 27 -- .../web/dto/mapper/ProductMapper.java | 33 -- .../web/dto/mapper/ProductVariantMapper.java | 32 -- .../commercify/web/dto/mapper/UserMapper.java | 33 -- .../web/dto/mapper/VariantOptionMapper.java | 22 -- .../dto/request/auth/LoginUserRequest.java | 4 - .../dto/request/auth/RegisterUserRequest.java | 24 -- .../request/order/CreateOrderLineRequest.java | 8 - .../dto/request/order/CreateOrderRequest.java | 15 - .../order/OrderStatusUpdateRequest.java | 4 - .../dto/request/payment/PaymentRequest.java | 8 - .../product/CreateVariantOptionRequest.java | 7 - .../web/dto/request/product/PriceRequest.java | 7 - .../dto/request/product/ProductRequest.java | 14 - .../product/ProductVariantRequest.java | 12 - .../web/dto/response/ErrorResponse.java | 10 - .../dto/response/ValidationErrorResponse.java | 12 - .../web/dto/response/auth/AuthResponse.java | 14 - .../dto/response/auth/JwtErrorResponse.java | 22 -- .../response/auth/RegisterUserResponse.java | 14 - .../response/order/CreateOrderResponse.java | 16 - .../dto/response/order/GetOrderResponse.java | 37 --- .../payment/CancelPaymentResponse.java | 20 -- .../dto/response/payment/PaymentResponse.java | 11 - .../product/ProductDeletionErrorResponse.java | 15 - .../product/ProductUpdateResponse.java | 12 - .../web/viewmodel/OrderLineViewModel.java | 27 -- .../web/viewmodel/OrderViewModel.java | 28 -- .../web/viewmodel/PriceViewModel.java | 6 - .../viewmodel/ProductVariantViewModel.java | 24 -- .../web/viewmodel/ProductViewModel.java | 36 --- .../web/viewmodel/VariantOptionViewModel.java | 17 - .../dto/mapper/OrderMapperTest.java | 109 ------- .../commercify/entity/OrderEntityTest.java | 89 ------ .../commercify/entity/ProductEntityTest.java | 62 ---- .../factory/ProductFactoryTest.java | 113 ------- .../commercify/flow/OrderStateFlowTest.java | 122 ------- .../mobilepay/MobilePayServiceTest.java | 99 ------ .../service/AuthenticationServiceTest.java | 194 ------------ .../service/PaymentServiceTest.java | 146 --------- .../service/UserAddressServiceTest.java | 132 -------- .../service/order/OrderServiceTest.java | 260 --------------- .../service/product/ProductServiceTest.java | 245 -------------- 207 files changed, 2846 insertions(+), 6232 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/ProductController.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/AddVariantsRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/AdjustInventoryRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateVariantRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/PriceRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/ProductVariantRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateProductRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateVariantPricesRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantOptionRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantPriceUpdateRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/response/CreateProductResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/response/PageInfo.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/response/PagedProductResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductSummaryResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductVariantSummaryResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/response/UpdateProductResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java delete mode 100644 src/main/java/com/zenfulcode/commercify/component/AdminDataLoader.java delete mode 100644 src/main/java/com/zenfulcode/commercify/component/factory/ProductFactory.java delete mode 100644 src/main/java/com/zenfulcode/commercify/component/flow/OrderStateFlow.java delete mode 100644 src/main/java/com/zenfulcode/commercify/component/flow/PaymentStateFlow.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/enums/OrderStatus.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/enums/PaymentProvider.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/enums/PaymentStatus.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/Address.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/ConfirmationToken.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/Order.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/OrderLine.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/OrderShippingInfo.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/Payment.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/Product.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/ProductVariant.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/User.java delete mode 100644 src/main/java/com/zenfulcode/commercify/domain/model/VariantOption.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/InsufficientStockException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/InvalidSortFieldException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/OrderNotFoundException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/OrderValidationException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/PaymentProcessingException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/PriceNotFoundException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/ProductDeletionException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/ProductNotFoundException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/ProductValidationException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/exception/StripeOperationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/command/AddProductVariantsCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/command/AdjustInventoryCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/command/CreateProductCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/command/DeactivateProductCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/command/DeleteProductCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/command/UpdateProductCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/command/UpdateVariantPricesCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/query/ProductQuery.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/query/ProductQueryType.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/exception/InsufficientStockException.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductDeletionException.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductModificationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductNotFoundException.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductValidationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/exception/VariantNotFoundException.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductInventoryPolicy.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductPricingPolicy.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductVariantRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductPricingPolicy.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/service/SkuGenerator.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/InventoryAdjustment.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/InventoryAdjustmentType.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductDeletionValidation.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductSpecification.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductUpdateSpec.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantOption.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantPriceUpdate.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantSpecification.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductVariantRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaVariantRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/AddressRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/ConfirmationTokenRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/OrderLineRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/OrderRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/OrderShippingInfoRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/PaymentRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/ProductRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/ProductVariantRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/repository/UserRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/ProductDeletionService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/ProductVariantService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/StockManagementService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/authentication/AuthenticationService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/authentication/JwtService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/core/OrderService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/core/PaymentService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/core/ProductService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/core/UserManagementService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/email/EmailConfirmationService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/email/EmailService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayController.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayTokenService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeConfig.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeController.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeWebhookHandler.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/validations/OrderValidationService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/service/validations/ProductValidationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventPublisher.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventStore.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainException.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainInvariantViolationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainValidationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/EntityNotFoundException.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/EventDeserializationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/EventSerializationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/model/Money.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/service/EventSerializer.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/service/EventTypeResolver.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EnhancedEventStore.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EventStoreRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/ProductIdConverter.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/interfaces/ApiResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/controller/AuthenticationController.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/controller/OrderController.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/controller/PaymentController.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/controller/ProductController.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/controller/UserManagementController.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/AddressDTO.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/CustomerDetailsDTO.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDTO.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDetailsDTO.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/OrderLineDTO.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDTO.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDeletionValidationResult.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/ProductUpdateResult.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/ProductVariantEntityDto.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/UserDTO.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/common/VariantOptionEntityDto.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/mapper/AddressMapper.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderLineMapper.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderMapper.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductMapper.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductVariantMapper.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/mapper/UserMapper.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/mapper/VariantOptionMapper.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/auth/LoginUserRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/auth/RegisterUserRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderLineRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/order/OrderStatusUpdateRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/payment/PaymentRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/product/CreateVariantOptionRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/product/PriceRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductVariantRequest.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/ErrorResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/ValidationErrorResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/auth/AuthResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/auth/JwtErrorResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/auth/RegisterUserResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/order/CreateOrderResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/order/GetOrderResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/payment/CancelPaymentResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/payment/PaymentResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductDeletionErrorResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductUpdateResponse.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderLineViewModel.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderViewModel.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/viewmodel/PriceViewModel.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductVariantViewModel.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductViewModel.java delete mode 100644 src/main/java/com/zenfulcode/commercify/web/viewmodel/VariantOptionViewModel.java delete mode 100644 src/test/java/com/zenfulcode/commercify/dto/mapper/OrderMapperTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/entity/OrderEntityTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/entity/ProductEntityTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/factory/ProductFactoryTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/flow/OrderStateFlowTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/integration/mobilepay/MobilePayServiceTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/service/AuthenticationServiceTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/service/PaymentServiceTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/service/UserAddressServiceTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/service/order/OrderServiceTest.java delete mode 100644 src/test/java/com/zenfulcode/commercify/service/product/ProductServiceTest.java diff --git a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java new file mode 100644 index 0000000..8fe4eb0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java @@ -0,0 +1,171 @@ +package com.zenfulcode.commercify.api.product; + +import com.zenfulcode.commercify.api.product.dto.ProductDtoMapper; +import com.zenfulcode.commercify.api.product.dto.request.*; +import com.zenfulcode.commercify.api.product.dto.response.CreateProductResponse; +import com.zenfulcode.commercify.api.product.dto.response.PagedProductResponse; +import com.zenfulcode.commercify.api.product.dto.response.ProductDetailResponse; +import com.zenfulcode.commercify.api.product.dto.response.UpdateProductResponse; +import com.zenfulcode.commercify.api.product.mapper.ProductResponseMapper; +import com.zenfulcode.commercify.product.application.command.*; +import com.zenfulcode.commercify.product.application.query.ProductQuery; +import com.zenfulcode.commercify.product.application.service.ProductApplicationService; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/products") +@RequiredArgsConstructor +public class ProductController { + private final ProductApplicationService productApplicationService; + private final ProductDtoMapper dtoMapper; + private final ProductResponseMapper responseMapper; + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> createProduct( + @Validated @RequestBody CreateProductRequest request) { + + // Map request to command + CreateProductCommand command = dtoMapper.toCommand(request); + + // Execute use case + ProductId productId = productApplicationService.createProduct(command); + + // Return response + CreateProductResponse response = new CreateProductResponse( + productId, + "Product created successfully" + ); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @GetMapping("/{productId}") + public ResponseEntity> getProduct( + @PathVariable String productId) { + + Product product = productApplicationService.getProduct(ProductId.of(productId)); + ProductDetailResponse response = dtoMapper.toDetailResponse(product); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @GetMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getAllProducts( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "id") String sortBy, + @RequestParam(defaultValue = "DESC") String sortDirection) { + + Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); + PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); + + Page products = productApplicationService.findProducts( + ProductQuery.all(), + pageRequest + ); + + PagedProductResponse response = responseMapper.toPagedResponse(products); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @GetMapping("/active") + public ResponseEntity> getActiveProducts( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "id") String sortBy, + @RequestParam(defaultValue = "DESC") String sortDirection) { + + Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); + PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); + + Page products = productApplicationService.findProducts( + ProductQuery.active(), + pageRequest + ); + + PagedProductResponse response = responseMapper.toPagedResponse(products); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PutMapping("/{productId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> updateProduct( + @PathVariable String productId, + @RequestBody UpdateProductRequest request) { + + UpdateProductCommand command = dtoMapper.toCommand(ProductId.of(productId), request); + productApplicationService.updateProduct(command); + + UpdateProductResponse response = new UpdateProductResponse( + "Product updated successfully" + ); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PostMapping("/{productId}/inventory") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> adjustInventory( + @PathVariable String productId, + @RequestBody AdjustInventoryRequest request) { + + AdjustInventoryCommand command = dtoMapper.toCommand(ProductId.of(productId), request); + productApplicationService.adjustInventory(command); + + return ResponseEntity.ok(ApiResponse.success("Inventory adjusted successfully")); + } + + @PostMapping("/{productId}/variants") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> addVariants( + @PathVariable String productId, + @RequestBody AddVariantsRequest request) { + + AddProductVariantsCommand command = dtoMapper.toCommand(ProductId.of(productId), request); + productApplicationService.addProductVariants(command); + + return ResponseEntity.ok(ApiResponse.success("Variants added successfully")); + } + + @PutMapping("/{productId}/variants/prices") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> updateVariantPrices( + @PathVariable String productId, + @RequestBody UpdateVariantPricesRequest request) { + + UpdateVariantPricesCommand command = dtoMapper.toCommand(ProductId.of(productId), request); + productApplicationService.updateVariantPrices(command); + + return ResponseEntity.ok(ApiResponse.success("Variant prices updated successfully")); + } + + @PostMapping("/{productId}/deactivate") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> deactivateProduct(@PathVariable String productId) { + DeactivateProductCommand command = new DeactivateProductCommand(ProductId.of(productId)); + productApplicationService.deactivateProduct(command); + + return ResponseEntity.ok(ApiResponse.success("Product deactivated successfully")); + } + + @DeleteMapping("/{productId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> deleteProduct(@PathVariable String productId) { + DeleteProductCommand command = new DeleteProductCommand(ProductId.of(productId)); + productApplicationService.deleteProduct(command); + + return ResponseEntity.ok(ApiResponse.success("Product deleted successfully")); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java new file mode 100644 index 0000000..7312ce5 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java @@ -0,0 +1,127 @@ +package com.zenfulcode.commercify.api.product.dto; + +import com.zenfulcode.commercify.api.product.dto.request.*; +import com.zenfulcode.commercify.api.product.dto.response.ProductDetailResponse; +import com.zenfulcode.commercify.api.product.dto.response.ProductSummaryResponse; +import com.zenfulcode.commercify.api.product.dto.response.ProductVariantSummaryResponse; +import com.zenfulcode.commercify.product.application.command.*; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.valueobject.*; +import com.zenfulcode.commercify.shared.domain.model.Money; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class ProductDtoMapper { + + public CreateProductCommand toCommand(CreateProductRequest request) { + Money price = new Money(request.price().amount(), request.price().currency()); + + return new CreateProductCommand( + request.name(), + request.description(), + request.initialStock(), + price, + mapVariantSpecs(request.variants()) + ); + } + + public AddProductVariantsCommand toCommand(ProductId productId, AddVariantsRequest request) { + List specs = request.variants().stream() + .map(this::toVariantSpec) + .collect(Collectors.toList()); + + return new AddProductVariantsCommand(productId, specs); + } + + public AdjustInventoryCommand toCommand(ProductId productId, AdjustInventoryRequest request) { + return new AdjustInventoryCommand( + productId, + InventoryAdjustmentType.valueOf(request.type()), + request.quantity(), + request.reason() + ); + } + + public UpdateProductCommand toCommand(ProductId productId, UpdateProductRequest request) { + ProductUpdateSpec updateSpec = new ProductUpdateSpec( + request.name(), + request.description(), + request.stock(), + request.price() != null ? + new Money(request.price().amount(), request.price().currency()) : null, + request.active() + ); + return new UpdateProductCommand(productId, updateSpec); + } + + public UpdateVariantPricesCommand toCommand(ProductId productId, UpdateVariantPricesRequest request) { + List updates = request.updates().stream() + .map(update -> new VariantPriceUpdate( + update.sku(), + new Money(update.price().amount(), update.price().currency()) + )) + .collect(Collectors.toList()); + + return new UpdateVariantPricesCommand(productId, updates); + } + + private List mapVariantSpecs(List variants) { + if (variants == null) return List.of(); + + return variants.stream() + .map(this::toVariantSpec) + .collect(Collectors.toList()); + } + + private VariantSpecification toVariantSpec(CreateVariantRequest request) { + return new VariantSpecification( + request.stock(), + request.price() != null ? + new Money(request.price().amount(), request.price().currency()) : null, + request.imageUrl(), + request.options().stream() + .map(opt -> new VariantOption(opt.name(), opt.value())) + .collect(Collectors.toList()) + ); + } + + private List mapVariants(Set variants) { + return variants.stream() + .map(variant -> new ProductVariantSummaryResponse( + variant.getId().toString(), + variant.getSku(), + variant.getOptions().stream() + .map(opt -> new ProductVariantSummaryResponse.VariantOptionResponse( + opt.getName(), + opt.getValue() + )) + .collect(Collectors.toList()), + toPriceResponse(variant.getPrice()), + variant.getStock() + )) + .collect(Collectors.toList()); + } + + private ProductSummaryResponse.ProductPriceResponse toPriceResponse(Money price) { + return new ProductSummaryResponse.ProductPriceResponse( + price.getAmount().doubleValue(), price.getCurrency() + ); + } + + public ProductDetailResponse toDetailResponse(Product product) { + return new ProductDetailResponse( + product.getId().getValue(), + product.getName(), + product.getDescription(), + product.getStock(), + toPriceResponse(product.getPrice()), + product.isActive(), + mapVariants(product.getVariants()) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AddVariantsRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AddVariantsRequest.java new file mode 100644 index 0000000..287bd74 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AddVariantsRequest.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +import java.util.List; + +public record AddVariantsRequest( + List variants +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AdjustInventoryRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AdjustInventoryRequest.java new file mode 100644 index 0000000..5c604c8 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AdjustInventoryRequest.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +public record AdjustInventoryRequest( + String type, + int quantity, + String reason +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java new file mode 100644 index 0000000..0e0c492 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +import java.util.List; + +public record CreateProductRequest( + String name, + String description, + int initialStock, + PriceRequest price, + List variants +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateVariantRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateVariantRequest.java new file mode 100644 index 0000000..6b27e83 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateVariantRequest.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +import java.util.List; + +public record CreateVariantRequest( + Integer stock, + PriceRequest price, + String imageUrl, + List options +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/PriceRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/PriceRequest.java new file mode 100644 index 0000000..67028bf --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/PriceRequest.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +public record PriceRequest( + double amount, + String currency +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/ProductVariantRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/ProductVariantRequest.java new file mode 100644 index 0000000..c98e9aa --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/ProductVariantRequest.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +import com.zenfulcode.commercify.product.domain.valueobject.VariantOption; +import com.zenfulcode.commercify.shared.domain.model.Money; + +import java.util.List; + +public record ProductVariantRequest( + int stock, + Money price, + String imageUrl, + List options +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateProductRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateProductRequest.java new file mode 100644 index 0000000..11a78a0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateProductRequest.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +public record UpdateProductRequest( + String name, + String description, + Integer stock, + PriceRequest price, + Boolean active +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateVariantPricesRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateVariantPricesRequest.java new file mode 100644 index 0000000..bc072f2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateVariantPricesRequest.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +import java.util.List; + +public record UpdateVariantPricesRequest( + List updates +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantOptionRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantOptionRequest.java new file mode 100644 index 0000000..dd1f375 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantOptionRequest.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +public record VariantOptionRequest( + String name, + String value +) {} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantPriceUpdateRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantPriceUpdateRequest.java new file mode 100644 index 0000000..9e85e20 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantPriceUpdateRequest.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.api.product.dto.request; + +public record VariantPriceUpdateRequest( + String sku, + PriceRequest price +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/CreateProductResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/CreateProductResponse.java new file mode 100644 index 0000000..0000ae2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/CreateProductResponse.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.api.product.dto.response; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; + +public record CreateProductResponse(ProductId productId, String message) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/PageInfo.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/PageInfo.java new file mode 100644 index 0000000..f58ea3b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/PageInfo.java @@ -0,0 +1,25 @@ +package com.zenfulcode.commercify.api.product.dto.response; + +public record PageInfo( + int pageNumber, + int pageSize, + long totalElements, + int totalPages, + boolean isFirst, + boolean isLast, + boolean hasNext, + boolean hasPrevious +) { + public PageInfo(int pageNumber, int pageSize, long totalElements, int totalPages) { + this( + pageNumber, + pageSize, + totalElements, + totalPages, + pageNumber == 0, + pageNumber == totalPages - 1, + pageNumber < totalPages - 1, + pageNumber > 0 + ); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/PagedProductResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/PagedProductResponse.java new file mode 100644 index 0000000..c32c8d8 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/PagedProductResponse.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.api.product.dto.response; + +import java.util.List; + +public record PagedProductResponse( + List items, + PageInfo pageInfo +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java new file mode 100644 index 0000000..fe35a1c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.api.product.dto.response; + +import java.util.List; + +public record ProductDetailResponse( + String id, + String name, + String description, + int stock, + ProductSummaryResponse.ProductPriceResponse price, + boolean active, + List variants +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductSummaryResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductSummaryResponse.java new file mode 100644 index 0000000..69362b5 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductSummaryResponse.java @@ -0,0 +1,24 @@ +package com.zenfulcode.commercify.api.product.dto.response; + +public record ProductSummaryResponse( + String id, + String name, + String description, + String imageUrl, + ProductPriceResponse price, + int stock +) { + public record ProductPriceResponse( + double amount, + String currency + ) { + } + + public record ProductInventoryResponse( + int quantity, + String status, + boolean backorderable, + Integer reorderPoint + ) { + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductVariantSummaryResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductVariantSummaryResponse.java new file mode 100644 index 0000000..65f1370 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductVariantSummaryResponse.java @@ -0,0 +1,17 @@ +package com.zenfulcode.commercify.api.product.dto.response; + +import java.util.List; + +public record ProductVariantSummaryResponse( + String id, + String sku, + List options, + ProductSummaryResponse.ProductPriceResponse price, + int stock +) { + public record VariantOptionResponse( + String name, + String value + ) { + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/UpdateProductResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/UpdateProductResponse.java new file mode 100644 index 0000000..67d6f86 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/UpdateProductResponse.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.api.product.dto.response; + +public record UpdateProductResponse( + String message +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java new file mode 100644 index 0000000..d65aef0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java @@ -0,0 +1,88 @@ +package com.zenfulcode.commercify.api.product.mapper; + +import com.zenfulcode.commercify.api.product.dto.response.PageInfo; +import com.zenfulcode.commercify.api.product.dto.response.PagedProductResponse; +import com.zenfulcode.commercify.api.product.dto.response.ProductSummaryResponse; +import com.zenfulcode.commercify.api.product.dto.response.ProductVariantSummaryResponse; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.model.VariantOption; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +@Component +public class ProductResponseMapper { + public PagedProductResponse toPagedResponse(Page productPage) { + List items = productPage.getContent() + .stream() + .map(this::toSummaryResponse) + .toList(); + + PageInfo pageInfo = new PageInfo( + productPage.getNumber(), + productPage.getSize(), + productPage.getTotalElements(), + productPage.getTotalPages() + ); + + return new PagedProductResponse( + items, + pageInfo + ); + } + + private ProductSummaryResponse toSummaryResponse(Product product) { + return new ProductSummaryResponse( + product.getId().toString(), + product.getName(), + product.getDescription(), + product.getImageUrl(), + toPriceResponse(product), + product.getStock() + ); + } + + private ProductSummaryResponse.ProductPriceResponse toPriceResponse(Product product) { + return new ProductSummaryResponse.ProductPriceResponse( + product.getPrice().getAmount().doubleValue(), + product.getPrice().getCurrency() + ); + } + + private List toVariantResponses(Set variants) { + return variants.stream() + .map(this::toVariantResponse) + .toList(); + } + + private ProductVariantSummaryResponse toVariantResponse(ProductVariant variant) { + return new ProductVariantSummaryResponse( + variant.getId().toString(), + variant.getSku(), + toVariantOptionResponses(variant.getOptions()), + toVariantPriceResponse(variant), + variant.getStock() != null ? variant.getStock() : variant.getProduct().getStock() + ); + } + + private List toVariantOptionResponses( + Set options) { + return options.stream() + .map(option -> new ProductVariantSummaryResponse.VariantOptionResponse( + option.getName(), + option.getValue() + )) + .toList(); + } + + private ProductSummaryResponse.ProductPriceResponse toVariantPriceResponse( + ProductVariant variant) { + return new ProductSummaryResponse.ProductPriceResponse( + variant.getPrice().getAmount().doubleValue(), + variant.getPrice().getCurrency() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/component/AdminDataLoader.java b/src/main/java/com/zenfulcode/commercify/component/AdminDataLoader.java deleted file mode 100644 index 2b3b475..0000000 --- a/src/main/java/com/zenfulcode/commercify/component/AdminDataLoader.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.zenfulcode.commercify.component; - -import com.zenfulcode.commercify.domain.model.Address; -import com.zenfulcode.commercify.domain.model.User; -import com.zenfulcode.commercify.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.CommandLineRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.List; - -@Configuration -@RequiredArgsConstructor -public class AdminDataLoader { - - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - - @Value("${admin.email}") - private String email; - @Value("${admin.password}") - private String password; - - @Bean - public CommandLineRunner loadData() { - return args -> { - if (userRepository.findByEmail(email).isEmpty()) { - Address defaultAddress = Address.builder() - .street("123 Main St") - .city("Springfield") - .state("IL") - .zipCode("62701") - .country("US") - .build(); - - User adminUser = User.builder() - .email(email) - .password(passwordEncoder.encode(password)) - .firstName("Admin") - .lastName("User") - .roles(List.of("ADMIN", "USER")) - .defaultAddress(defaultAddress) - .emailConfirmed(true) - .build(); - - userRepository.save(adminUser); - System.out.println("Admin user created successfully."); - } else { - System.out.println("Admin user already exists. Skipping creation."); - } - }; - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/component/factory/ProductFactory.java b/src/main/java/com/zenfulcode/commercify/component/factory/ProductFactory.java deleted file mode 100644 index c0cf26c..0000000 --- a/src/main/java/com/zenfulcode/commercify/component/factory/ProductFactory.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.zenfulcode.commercify.component.factory; - -import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; -import com.zenfulcode.commercify.domain.model.Product; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -@Component -@Slf4j -public class ProductFactory { - public Product createFromRequest(ProductRequest request) { - return com.zenfulcode.commercify.domain.model.Product.builder() - .name(request.name()) - .description(request.description()) - .stock(request.stock() != null ? request.stock() : 0) - .active(true) - .imageUrl(request.imageUrl()) - .currency(request.price().currency()) - .unitPrice(request.price().amount()) - .build(); - } -} - diff --git a/src/main/java/com/zenfulcode/commercify/component/flow/OrderStateFlow.java b/src/main/java/com/zenfulcode/commercify/component/flow/OrderStateFlow.java deleted file mode 100644 index 649b4d8..0000000 --- a/src/main/java/com/zenfulcode/commercify/component/flow/OrderStateFlow.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.zenfulcode.commercify.component.flow; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import org.springframework.stereotype.Component; - -import java.util.EnumMap; -import java.util.Set; - -@Component -public class OrderStateFlow { - private final EnumMap> validTransitions; - - public OrderStateFlow() { - validTransitions = new EnumMap<>(OrderStatus.class); - - // Initial state -> Confirmed or Cancelled - validTransitions.put(OrderStatus.PENDING, Set.of( - OrderStatus.CONFIRMED, - OrderStatus.CANCELLED - )); - - // Payment received -> Processing or Cancelled - validTransitions.put(OrderStatus.CONFIRMED, Set.of( - OrderStatus.SHIPPED, - OrderStatus.CANCELLED - )); - - // Shipped -> Completed or Returned - validTransitions.put(OrderStatus.SHIPPED, Set.of( - OrderStatus.COMPLETED, - OrderStatus.RETURNED - )); - - // Terminal states - validTransitions.put(OrderStatus.COMPLETED, Set.of()); - validTransitions.put(OrderStatus.CANCELLED, Set.of()); - validTransitions.put(OrderStatus.RETURNED, Set.of()); - validTransitions.put(OrderStatus.REFUNDED, Set.of()); - } - - public boolean canTransition(OrderStatus currentState, OrderStatus newState) { - return validTransitions.get(currentState).contains(newState); - } - - public Set getValidTransitions(OrderStatus currentState) { - return validTransitions.get(currentState); - } - - public boolean isTerminalState(OrderStatus state) { - return validTransitions.get(state).isEmpty(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/component/flow/PaymentStateFlow.java b/src/main/java/com/zenfulcode/commercify/component/flow/PaymentStateFlow.java deleted file mode 100644 index b4f8d27..0000000 --- a/src/main/java/com/zenfulcode/commercify/component/flow/PaymentStateFlow.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.zenfulcode.commercify.component.flow; - -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import org.springframework.stereotype.Component; - -import java.util.EnumMap; -import java.util.Set; - -@Component -public class PaymentStateFlow { - private final EnumMap> validTransitions; - - public PaymentStateFlow() { - validTransitions = new EnumMap<>(PaymentStatus.class); - - // Initial state -> Paid, Failed, or Cancelled - validTransitions.put(PaymentStatus.PENDING, Set.of( - PaymentStatus.PAID, - PaymentStatus.FAILED, - PaymentStatus.CANCELLED, - PaymentStatus.EXPIRED - )); - - // Successful payment -> Refunded - validTransitions.put(PaymentStatus.PAID, Set.of( - PaymentStatus.REFUNDED - )); - - // Terminal states - validTransitions.put(PaymentStatus.FAILED, Set.of()); - validTransitions.put(PaymentStatus.CANCELLED, Set.of()); - validTransitions.put(PaymentStatus.REFUNDED, Set.of()); - validTransitions.put(PaymentStatus.EXPIRED, Set.of()); - validTransitions.put(PaymentStatus.TERMINATED, Set.of()); - } - - public boolean canTransition(PaymentStatus currentState, PaymentStatus newState) { - return validTransitions.get(currentState).contains(newState); - } - - public Set getValidTransitions(PaymentStatus currentState) { - return validTransitions.get(currentState); - } - - public boolean isTerminalState(PaymentStatus state) { - return validTransitions.get(state).isEmpty(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java b/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java index 2fb368a..522c569 100644 --- a/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import com.zenfulcode.commercify.web.dto.response.auth.JwtErrorResponse; -import com.zenfulcode.commercify.service.authentication.JwtService; import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/com/zenfulcode/commercify/domain/enums/OrderStatus.java b/src/main/java/com/zenfulcode/commercify/domain/enums/OrderStatus.java deleted file mode 100644 index e780758..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/enums/OrderStatus.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zenfulcode.commercify.domain.enums; - -public enum OrderStatus { - PENDING, // Order has been created but not yet confirmed - CONFIRMED, // Order has been confirmed by the customer - SHIPPED, // Order has been shipped - COMPLETED, // Order has been delivered - CANCELLED, // Order has been cancelled - FAILED, // Order has failed - REFUNDED, // Order has been refunded - RETURNED // Order has been returned -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentProvider.java b/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentProvider.java deleted file mode 100644 index 9c75ca5..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentProvider.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.zenfulcode.commercify.domain.enums; - -public enum PaymentProvider { - STRIPE, MOBILEPAY -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentStatus.java b/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentStatus.java deleted file mode 100644 index cbf40c5..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/enums/PaymentStatus.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zenfulcode.commercify.domain.enums; - -public enum PaymentStatus { - PENDING, - PAID, - FAILED, - CANCELLED, - REFUNDED, - NOT_FOUND, - TERMINATED, - EXPIRED -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/Address.java b/src/main/java/com/zenfulcode/commercify/domain/model/Address.java deleted file mode 100644 index 0fe3f32..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/Address.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.Instant; - -@Entity -@Table(name = "addresses") -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Address { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String street; - - @Column(nullable = false) - private String city; - - private String state; - - @Column(name = "zip_code", nullable = false) - private String zipCode; - - @Column(nullable = false) - private String country; - - @Column(name = "created_at", nullable = false) - @CreationTimestamp - private Instant createdAt; - - @Column(name = "updated_at") - @UpdateTimestamp - private Instant updatedAt; - -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/ConfirmationToken.java b/src/main/java/com/zenfulcode/commercify/domain/model/ConfirmationToken.java deleted file mode 100644 index 11e0019..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/ConfirmationToken.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.Instant; -import java.util.UUID; - -@Entity -@Table(name = "confirmation_tokens") -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ConfirmationToken { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String token; - - @Column(nullable = false, name = "expiry_date") - private Instant expiryDate; - - @Column(nullable = false) - private boolean confirmed; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @CreationTimestamp - @Column(nullable = false, name = "created_at") - private Instant createdAt; - - @PrePersist - public void prePersist() { - if (token == null) { - token = UUID.randomUUID().toString(); - } - if (expiryDate == null) { - expiryDate = Instant.now().plusSeconds(24 * 60 * 60); // 24 hours - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/domain/model/Order.java deleted file mode 100644 index 2c019b6..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/Order.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import org.hibernate.proxy.HibernateProxy; - -import java.time.Instant; -import java.util.LinkedHashSet; -import java.util.Objects; -import java.util.Set; - -@Entity -@Table(name = "orders", indexes = { - @Index(name = "idx_orders_user_id", columnList = "user_id"), - @Index(name = "idx_orders_status", columnList = "status"), - @Index(name = "idx_orders_user_id_status", columnList = "user_id, status") -}) -@Getter -@Setter -@ToString -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Order { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) - private Long id; - - @Column(name = "user_id") - private Long userId; - - @ToString.Exclude - @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) - @Builder.Default - private Set orderLines = new LinkedHashSet<>(); - - @Enumerated(EnumType.STRING) - @Column(name = "status", nullable = false) - private OrderStatus status; - - private String currency; - @Column(name = "total_amount") - private Double totalAmount; - - @ToString.Exclude - @ManyToOne(cascade = CascadeType.ALL) - @JoinColumn(name = "order_shipping_info_id") - private OrderShippingInfo orderShippingInfo; - - @Column(name = "created_at", nullable = false) - @CreationTimestamp - private Instant createdAt; - - @Column(name = "updated_at") - @UpdateTimestamp - private Instant updatedAt; - - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; - Order that = (Order) o; - return getId() != null && Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/OrderLine.java b/src/main/java/com/zenfulcode/commercify/domain/model/OrderLine.java deleted file mode 100644 index a400df5..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/OrderLine.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.proxy.HibernateProxy; - -import java.util.Objects; - -@Builder -@Getter -@Setter -@Entity -@Table(name = "order_lines") -@NoArgsConstructor -@AllArgsConstructor -public class OrderLine { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(nullable = false) - private Long id; - - @Column(name = "product_id", nullable = false, updatable = false) - private Long productId; - - @Column(nullable = false, updatable = false) - private Integer quantity; - - @Column(name = "unit_price", nullable = false, updatable = false) - private Double unitPrice; - - @Column(nullable = false, updatable = false) - private String currency; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_variant_id") - private ProductVariant productVariant; - - @ManyToOne(optional = false) - @JoinColumn(name = "order_id") - private Order order; - - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; - OrderLine that = (OrderLine) o; - return getId() != null && Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "(" + - "id = " + id + ", " + - "productId = " + productId + ", " + - "quantity = " + quantity + ", " + - "unitPrice = " + unitPrice + ", " + - "currency = " + currency + ", " + - "variantId = " + productVariant + ")"; - } -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/OrderShippingInfo.java b/src/main/java/com/zenfulcode/commercify/domain/model/OrderShippingInfo.java deleted file mode 100644 index 35b0a37..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/OrderShippingInfo.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import jakarta.persistence.*; -import lombok.*; - -@Table(name = "order_shipping_info") -@Entity -@Getter -@Setter -@ToString -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class OrderShippingInfo { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) - private Long id; - - @Column(name = "customer_first_name") - private String customerFirstName; - @Column(name = "customer_last_name") - private String customerLastName; - @Column(name = "customer_email") - private String customerEmail; - @Column(name = "customer_phone") - private String customerPhone; - - @Column(name = "shipping_street", nullable = false) - private String shippingStreet; - @Column(name = "shipping_city", nullable = false) - private String shippingCity; - @Column(name = "shipping_state") - private String shippingState; - @Column(name = "shipping_zip", nullable = false) - private String shippingZip; - @Column(name = "shipping_country", nullable = false) - private String shippingCountry; - - @Column(name = "billing_street") - private String billingStreet; - @Column(name = "billing_city") - private String billingCity; - @Column(name = "billing_state") - private String billingState; - @Column(name = "billing_zip") - private String billingZip; - @Column(name = "billing_country") - private String billingCountry; -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/Payment.java b/src/main/java/com/zenfulcode/commercify/domain/model/Payment.java deleted file mode 100644 index 3aaa9f2..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/Payment.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import com.zenfulcode.commercify.domain.enums.PaymentProvider; -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import org.hibernate.proxy.HibernateProxy; - -import java.time.Instant; -import java.util.Objects; - -@Entity -@Table(name = "payments") -@Getter -@Setter -@ToString -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Payment { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "payment_reference", unique = true) - private String mobilePayReference; - - @Column(name = "order_id", nullable = false) - private Long orderId; - @Column(name = "total_amount") - private Double totalAmount; - @Column(name = "payment_method") - private String paymentMethod; // e.g., WALLET; CARD - @Column(name = "payment_provider") - @Enumerated(EnumType.STRING) - private PaymentProvider paymentProvider; - - @Enumerated(EnumType.STRING) - private PaymentStatus status; - - @Column(name = "created_at", nullable = false) - @CreationTimestamp - private Instant createdAt; - - @Column(name = "updated_at") - @UpdateTimestamp - private Instant updatedAt; - - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; - Payment that = (Payment) o; - return getId() != null && Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/domain/model/Product.java deleted file mode 100644 index eeadfb6..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/Product.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; - -@Entity -@Table(name = "products") -@Builder -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -public class Product { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String name; - private String description; - private Integer stock; - private Boolean active; - @Column(name = "image_url") - private String imageUrl; - - // Prices - private String currency; - @Column(name = "unit_price") - private Double unitPrice; - - @OneToMany(mappedBy = "product", fetch = FetchType.EAGER, cascade = CascadeType.ALL) - @Builder.Default - private Set variants = new HashSet<>(); - - @Column(name = "created_at", nullable = false) - @CreationTimestamp - private Instant createdAt; - - @Column(name = "updated_at") - @UpdateTimestamp - private Instant updatedAt; - - public void addVariant(ProductVariant variant) { - variants.add(variant); - variant.setProduct(this); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "(" + - "id = " + id + ", " + - "name = " + name + ", " + - "description = " + description + ", " + - "stock = " + stock + ", " + - "active = " + active + ", " + - "imageUrl = " + imageUrl + ", " + - "currency = " + currency + ", " + - "unitPrice = " + unitPrice + ")"; - } -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/ProductVariant.java b/src/main/java/com/zenfulcode/commercify/domain/model/ProductVariant.java deleted file mode 100644 index 66e2fae..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/ProductVariant.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; - -@Builder -@Getter -@Setter -@Entity -@Table(name = "product_variants") -@AllArgsConstructor -@NoArgsConstructor -public class ProductVariant { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String sku; - - private Integer stock; - - @Column(name = "image_url") - private String imageUrl; - @Column(name = "unit_price") - private Double unitPrice; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id", nullable = false) - private Product product; - - @OneToMany(mappedBy = "productVariant", cascade = CascadeType.ALL, orphanRemoval = true) - @Builder.Default - private Set options = new HashSet<>(); - - @Column(name = "created_at", nullable = false) - @CreationTimestamp - private Instant createdAt; - - @Column(name = "updated_at") - @UpdateTimestamp - private Instant updatedAt; - - public void addOption(VariantOption option) { - options.add(option); - option.setProductVariant(this); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/domain/model/User.java deleted file mode 100644 index ec3a2f6..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/User.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.time.Instant; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Table(name = "users") -@Entity -@Builder -@Getter -@Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor -public class User implements UserDetails { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "first_name", length = 50, nullable = false) - private String firstName; - @Column(name = "last_name", length = 50, nullable = false) - private String lastName; - - @Column(unique = true, length = 100, nullable = false) - private String email; - private String password; - - @Column(name = "phone_number", length = 20) - private String phoneNumber; - - @ToString.Exclude - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "default_address_id", unique = true) - private Address defaultAddress; - - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "id")) - @Column(name = "role") - private List roles; - - @Column(nullable = false, name = "email_confirmed") - private Boolean emailConfirmed = false; - - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - @ToString.Exclude - private Set confirmationTokens = new HashSet<>(); - - @Column(name = "created_at", nullable = false) - @CreationTimestamp - private Instant createdAt; - - @Column(name = "updated_at") - @UpdateTimestamp - private Instant updatedAt; - - @Override - public Collection getAuthorities() { - return roles.stream() - .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) - .collect(Collectors.toList()); - } - - @Override - public String getUsername() { - return email; - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/src/main/java/com/zenfulcode/commercify/domain/model/VariantOption.java b/src/main/java/com/zenfulcode/commercify/domain/model/VariantOption.java deleted file mode 100644 index 5eb8713..0000000 --- a/src/main/java/com/zenfulcode/commercify/domain/model/VariantOption.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.zenfulcode.commercify.domain.model; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.Instant; - -@Builder -@Getter -@Setter -@Entity -@Table(name = "variant_option_entity") -@NoArgsConstructor -@AllArgsConstructor -public class VariantOption { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String name; // e.g., "Size", "Color" - - @Column(nullable = false) - private String value; // e.g., "Large", "Red" - - @ManyToOne - @JoinColumn(name = "product_variant_id") - private ProductVariant productVariant; - - @Column(name = "created_at", nullable = false) - @CreationTimestamp - private Instant createdAt; - - @Column(name = "updated_at") - @UpdateTimestamp - private Instant updatedAt; -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/exception/GlobalExceptionHandler.java b/src/main/java/com/zenfulcode/commercify/exception/GlobalExceptionHandler.java deleted file mode 100644 index 5211833..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.zenfulcode.commercify.exception; - -import io.jsonwebtoken.ExpiredJwtException; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ProblemDetail; -import org.springframework.security.authentication.AccountStatusException; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -import java.nio.file.AccessDeniedException; -import java.security.SignatureException; - -@RestControllerAdvice -public class GlobalExceptionHandler { - @ExceptionHandler(Exception.class) - public ProblemDetail handleSecurityException(Exception exception) { - ProblemDetail errorDetail = null; - - - if (exception instanceof BadCredentialsException) { - errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(401), exception.getMessage()); - errorDetail.setProperty("description", "The username or password is incorrect"); - - return errorDetail; - } - - if (exception instanceof AccountStatusException) { - errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(403), exception.getMessage()); - errorDetail.setProperty("description", "The account is locked"); - } - - if (exception instanceof AccessDeniedException) { - errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(403), exception.getMessage()); - errorDetail.setProperty("description", "You are not authorized to access this resource"); - } - - if (exception instanceof SignatureException) { - errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(403), exception.getMessage()); - errorDetail.setProperty("description", "The JWT signature is invalid"); - } - - if (exception instanceof ExpiredJwtException) { - errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(403), exception.getMessage()); - errorDetail.setProperty("description", "The JWT token has expired"); - } - - if (errorDetail == null) { - errorDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(500), exception.getMessage()); - errorDetail.setProperty("description", "Unknown internal server error."); - } - - return errorDetail; - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/exception/InsufficientStockException.java b/src/main/java/com/zenfulcode/commercify/exception/InsufficientStockException.java deleted file mode 100644 index 4a89ceb..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/InsufficientStockException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.exception; - -public class InsufficientStockException extends RuntimeException { - public InsufficientStockException(String message) { - super(message); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/exception/InvalidSortFieldException.java b/src/main/java/com/zenfulcode/commercify/exception/InvalidSortFieldException.java deleted file mode 100644 index bea6e33..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/InvalidSortFieldException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.zenfulcode.commercify.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.BAD_REQUEST) -public class InvalidSortFieldException extends RuntimeException { - public InvalidSortFieldException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/exception/OrderNotFoundException.java b/src/main/java/com/zenfulcode/commercify/exception/OrderNotFoundException.java deleted file mode 100644 index c73c8f2..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/OrderNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.exception; - -public class OrderNotFoundException extends RuntimeException { - public OrderNotFoundException(Long orderId) { - super("Order not found with ID: " + orderId); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/exception/OrderValidationException.java b/src/main/java/com/zenfulcode/commercify/exception/OrderValidationException.java deleted file mode 100644 index 443edc0..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/OrderValidationException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.zenfulcode.commercify.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.BAD_REQUEST) -public class OrderValidationException extends RuntimeException { - public OrderValidationException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/exception/PaymentProcessingException.java b/src/main/java/com/zenfulcode/commercify/exception/PaymentProcessingException.java deleted file mode 100644 index 5064195..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/PaymentProcessingException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.exception; - -public class PaymentProcessingException extends RuntimeException { - public PaymentProcessingException(String message, Throwable cause) { - super(message, cause); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/exception/PriceNotFoundException.java b/src/main/java/com/zenfulcode/commercify/exception/PriceNotFoundException.java deleted file mode 100644 index 406b628..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/PriceNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.exception; - -public class PriceNotFoundException extends RuntimeException { - public PriceNotFoundException(Long priceId) { - super("Price not found with ID: " + priceId); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/exception/ProductDeletionException.java b/src/main/java/com/zenfulcode/commercify/exception/ProductDeletionException.java deleted file mode 100644 index 506cdf5..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/ProductDeletionException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.zenfulcode.commercify.exception; - -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import lombok.Getter; - -import java.util.List; - -@Getter -public class ProductDeletionException extends RuntimeException { - private final List issues; - private final List activeOrders; - - public ProductDeletionException(String message, List issues, List activeOrders) { - super(message); - this.issues = issues; - this.activeOrders = activeOrders; - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/exception/ProductNotFoundException.java b/src/main/java/com/zenfulcode/commercify/exception/ProductNotFoundException.java deleted file mode 100644 index dd73ab9..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/ProductNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.exception; - -public class ProductNotFoundException extends RuntimeException { - public ProductNotFoundException(Long productId) { - super("Product not found with ID: " + productId); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/exception/ProductValidationException.java b/src/main/java/com/zenfulcode/commercify/exception/ProductValidationException.java deleted file mode 100644 index 5878372..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/ProductValidationException.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.zenfulcode.commercify.exception; - -import lombok.Getter; - -import java.util.List; - -@Getter -public class ProductValidationException extends RuntimeException { - private final List errors; - - public ProductValidationException(List errors) { - super("Product validation failed: " + String.join(", ", errors)); - this.errors = errors; - } - -} diff --git a/src/main/java/com/zenfulcode/commercify/exception/StripeOperationException.java b/src/main/java/com/zenfulcode/commercify/exception/StripeOperationException.java deleted file mode 100644 index fb72e11..0000000 --- a/src/main/java/com/zenfulcode/commercify/exception/StripeOperationException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.zenfulcode.commercify.exception; - -public class StripeOperationException extends RuntimeException { - - public StripeOperationException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/AddProductVariantsCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/AddProductVariantsCommand.java new file mode 100644 index 0000000..fde319e --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/AddProductVariantsCommand.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.product.application.command; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantSpecification; + +import java.util.List; + +public record AddProductVariantsCommand( + ProductId productId, + List variantSpecs +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/AdjustInventoryCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/AdjustInventoryCommand.java new file mode 100644 index 0000000..a323ff8 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/AdjustInventoryCommand.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.product.application.command; + +import com.zenfulcode.commercify.product.domain.valueobject.InventoryAdjustmentType; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; + +public record AdjustInventoryCommand( + ProductId productId, + InventoryAdjustmentType adjustmentType, + int quantity, + String reason +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/CreateProductCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/CreateProductCommand.java new file mode 100644 index 0000000..b719c77 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/CreateProductCommand.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.product.application.command; + +import com.zenfulcode.commercify.product.domain.valueobject.VariantSpecification; +import com.zenfulcode.commercify.shared.domain.model.Money; + +import java.util.List; + +public record CreateProductCommand( + String name, + String description, + int initialStock, + Money price, + List variantSpecs +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/DeactivateProductCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/DeactivateProductCommand.java new file mode 100644 index 0000000..0eadea6 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/DeactivateProductCommand.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.product.application.command; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; + +public record DeactivateProductCommand( + ProductId productId +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/DeleteProductCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/DeleteProductCommand.java new file mode 100644 index 0000000..738dff8 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/DeleteProductCommand.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.product.application.command; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; + +public record DeleteProductCommand( + ProductId productId +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/UpdateProductCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/UpdateProductCommand.java new file mode 100644 index 0000000..44fc5b2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/UpdateProductCommand.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.product.application.command; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductUpdateSpec; + +public record UpdateProductCommand( + ProductId productId, + ProductUpdateSpec updateSpec +) {} diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/UpdateVariantPricesCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/UpdateVariantPricesCommand.java new file mode 100644 index 0000000..4f558a7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/UpdateVariantPricesCommand.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.product.application.command; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantPriceUpdate; + +import java.util.List; + +public record UpdateVariantPricesCommand( + ProductId productId, + List priceUpdates +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/application/query/ProductQuery.java b/src/main/java/com/zenfulcode/commercify/product/application/query/ProductQuery.java new file mode 100644 index 0000000..d7214b5 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/query/ProductQuery.java @@ -0,0 +1,26 @@ +package com.zenfulcode.commercify.product.application.query; + + +import com.zenfulcode.commercify.product.domain.valueobject.CategoryId; + +public record ProductQuery( + ProductQueryType type, + CategoryId categoryId, + int threshold +) { + public static ProductQuery all() { + return new ProductQuery(ProductQueryType.ALL, null, 0); + } + + public static ProductQuery active() { + return new ProductQuery(ProductQueryType.ACTIVE, null, 0); + } + + public static ProductQuery byCategory(CategoryId categoryId) { + return new ProductQuery(ProductQueryType.BY_CATEGORY, categoryId, 0); + } + + public static ProductQuery lowStock(int threshold) { + return new ProductQuery(ProductQueryType.LOW_STOCK, null, threshold); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/application/query/ProductQueryType.java b/src/main/java/com/zenfulcode/commercify/product/application/query/ProductQueryType.java new file mode 100644 index 0000000..b08b3db --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/query/ProductQueryType.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.product.application.query; + +public enum ProductQueryType { + ALL, + ACTIVE, + BY_CATEGORY, + LOW_STOCK +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java new file mode 100644 index 0000000..8f98af0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java @@ -0,0 +1,170 @@ +package com.zenfulcode.commercify.product.application.service; + +import com.zenfulcode.commercify.product.application.command.*; +import com.zenfulcode.commercify.product.application.query.ProductQuery; +import com.zenfulcode.commercify.product.domain.exception.ProductDeletionException; +import com.zenfulcode.commercify.product.domain.exception.ProductNotFoundException; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.repository.ProductRepository; +import com.zenfulcode.commercify.product.domain.service.ProductDomainService; +import com.zenfulcode.commercify.product.domain.valueobject.InventoryAdjustment; +import com.zenfulcode.commercify.product.domain.valueobject.ProductDeletionValidation; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductSpecification; +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +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; + +@Service +@RequiredArgsConstructor +public class ProductApplicationService { + private final ProductDomainService productDomainService; + private final DomainEventPublisher eventPublisher; + private final ProductRepository productRepository; + + /** + * Creates a new product + */ + @Transactional + public ProductId createProduct(CreateProductCommand command) { + // Convert command to domain specification + ProductSpecification spec = new ProductSpecification( + command.name(), + command.description(), + command.initialStock(), + command.price(), + command.variantSpecs() + ); + + // Use domain service to create product + Product product = productDomainService.createProduct(spec); + + // Persist the product + Product savedProduct = productRepository.save(product); + + // Publish domain events + eventPublisher.publish(product.getDomainEvents()); + + return savedProduct.getId(); + } + + /** + * Updates an existing product + */ + @Transactional + public void updateProduct(UpdateProductCommand command) { + // Retrieve product + Product product = productRepository.findById(command.productId()) + .orElseThrow(() -> new ProductNotFoundException(command.productId())); + + // Update product through domain service + productDomainService.updateProduct(product, command.updateSpec()); + + // Save changes + productRepository.save(product); + + // Publish events + eventPublisher.publish(product.getDomainEvents()); + } + + /** + * Handles product inventory adjustments + */ + @Transactional + public void adjustInventory(AdjustInventoryCommand command) { + Product product = productRepository.findById(command.productId()) + .orElseThrow(() -> new ProductNotFoundException(command.productId())); + + InventoryAdjustment adjustment = new InventoryAdjustment( + command.adjustmentType(), + command.quantity(), + command.reason() + ); + + productDomainService.adjustInventory(product, adjustment); + productRepository.save(product); + eventPublisher.publish(product.getDomainEvents()); + } + + /** + * Updates variant prices + */ + @Transactional + public void updateVariantPrices(UpdateVariantPricesCommand command) { + Product product = productRepository.findById(command.productId()) + .orElseThrow(() -> new ProductNotFoundException(command.productId())); + + productDomainService.updateVariantPrices(product, command.priceUpdates()); + productRepository.save(product); + eventPublisher.publish(product.getDomainEvents()); + } + + /** + * Adds variants to a product + */ + @Transactional + public void addProductVariants(AddProductVariantsCommand command) { + Product product = productRepository.findById(command.productId()) + .orElseThrow(() -> new ProductNotFoundException(command.productId())); + + productDomainService.createProductVariants(product, command.variantSpecs()); + productRepository.save(product); + eventPublisher.publish(product.getDomainEvents()); + } + + /** + * Deactivates a product + */ + @Transactional + public void deactivateProduct(DeactivateProductCommand command) { + Product product = productRepository.findById(command.productId()) + .orElseThrow(() -> new ProductNotFoundException(command.productId())); + + product.deactivate(); + productRepository.save(product); + eventPublisher.publish(product.getDomainEvents()); + } + + /** + * Deletes a product + */ + @Transactional + public void deleteProduct(DeleteProductCommand command) { + Product product = productRepository.findById(command.productId()) + .orElseThrow(() -> new ProductNotFoundException(command.productId())); + + ProductDeletionValidation validation = productDomainService.validateProductDeletion(product); + + if (!validation.canDelete()) { + throw new ProductDeletionException( + "Cannot delete product", + validation.issues() + ); + } + + productRepository.delete(product); + eventPublisher.publish(product.getDomainEvents()); + } + + /** + * Queries for products + */ + @Transactional(readOnly = true) + public Page findProducts(ProductQuery query, Pageable pageable) { + return switch (query.type()) { + case ALL -> productRepository.findAll(pageable); + case ACTIVE -> productRepository.findByActiveTrue(pageable); + case BY_CATEGORY -> productRepository.findByCategory(query.categoryId(), pageable); + case LOW_STOCK -> productRepository.findByStockLessThan(query.threshold(), pageable); + }; + } + + @Transactional(readOnly = true) + public Product getProduct(ProductId productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new ProductNotFoundException(productId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java new file mode 100644 index 0000000..cb4488c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.product.domain.event; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import lombok.Getter; + +@Getter +public class LargeStockIncreaseEvent extends DomainEvent { + private final ProductId productId; + private final int quantity; + private final String reason; + + public LargeStockIncreaseEvent(ProductId productId, int quantity, String reason) { + this.productId = productId; + this.quantity = quantity; + this.reason = reason; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java new file mode 100644 index 0000000..30dcc18 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java @@ -0,0 +1,17 @@ +package com.zenfulcode.commercify.product.domain.event; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import lombok.Getter; + +@Getter +public class LowStockEvent extends DomainEvent { + private final ProductId productId; + private final int stockAmount; + + public LowStockEvent(ProductId productId, int stockAmount) { + this.productId = productId; + this.stockAmount = stockAmount; + } + +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java new file mode 100644 index 0000000..aff0257 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java @@ -0,0 +1,19 @@ +package com.zenfulcode.commercify.product.domain.event; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.Getter; + +@Getter +public class ProductCreatedEvent extends DomainEvent { + private final ProductId productId; + private final String name; + private final Money price; + + public ProductCreatedEvent(ProductId productId, String name, Money price) { + this.productId = productId; + this.name = name; + this.price = price; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java new file mode 100644 index 0000000..1e72462 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.product.domain.event; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import lombok.Getter; + +@Getter +public class StockCorrectionEvent extends DomainEvent { + private final ProductId productId; + private final int quantity; + private final String reason; + + public StockCorrectionEvent(ProductId productId, int quantity, String reason) { + this.productId = productId; + this.quantity = quantity; + this.reason = reason; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/exception/InsufficientStockException.java b/src/main/java/com/zenfulcode/commercify/product/domain/exception/InsufficientStockException.java new file mode 100644 index 0000000..cc29a44 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/exception/InsufficientStockException.java @@ -0,0 +1,20 @@ +package com.zenfulcode.commercify.product.domain.exception; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import lombok.Getter; + +@Getter +public class InsufficientStockException extends DomainException { + private final ProductId productId; + private final int requestedQuantity; + private final int availableStock; + + public InsufficientStockException(ProductId productId, int requestedQuantity, int availableStock) { + super(String.format("Insufficient stock for product %s. Requested: %d, Available: %d", + productId, requestedQuantity, availableStock)); + this.productId = productId; + this.requestedQuantity = requestedQuantity; + this.availableStock = availableStock; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductDeletionException.java b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductDeletionException.java new file mode 100644 index 0000000..872b563 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductDeletionException.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.product.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainValidationException; + +import java.util.List; + +public class ProductDeletionException extends DomainValidationException { + public ProductDeletionException(String message, List violations) { + super(message, violations); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductModificationException.java b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductModificationException.java new file mode 100644 index 0000000..9ebd91b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductModificationException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.product.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; + +public class ProductModificationException extends DomainException { + public ProductModificationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductNotFoundException.java b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductNotFoundException.java new file mode 100644 index 0000000..2cdbb9b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductNotFoundException.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.product.domain.exception; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.exception.EntityNotFoundException; + +public class ProductNotFoundException extends EntityNotFoundException { + public ProductNotFoundException(ProductId productId) { + super("Product", productId); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductValidationException.java b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductValidationException.java new file mode 100644 index 0000000..225663f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductValidationException.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.product.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainValidationException; + +import java.util.List; + +public class ProductValidationException extends DomainValidationException { + public ProductValidationException(List violations) { + super("Product validation failed", violations); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/exception/VariantNotFoundException.java b/src/main/java/com/zenfulcode/commercify/product/domain/exception/VariantNotFoundException.java new file mode 100644 index 0000000..8b29ba1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/exception/VariantNotFoundException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.product.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.EntityNotFoundException; + +public class VariantNotFoundException extends EntityNotFoundException { + public VariantNotFoundException(Object entityId) { + super("Product variant", entityId); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java new file mode 100644 index 0000000..1540e63 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java @@ -0,0 +1,152 @@ +package com.zenfulcode.commercify.product.domain.model; + +import com.zenfulcode.commercify.product.domain.event.ProductCreatedEvent; +import com.zenfulcode.commercify.product.domain.exception.InsufficientStockException; +import com.zenfulcode.commercify.product.domain.exception.ProductModificationException; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; +import com.zenfulcode.commercify.shared.domain.model.Money; +import jakarta.persistence.*; +import lombok.*; +import org.apache.commons.lang3.NotImplementedException; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +@Builder +@Entity +@Table(name = "products") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Product extends AggregateRoot { + @EmbeddedId + private ProductId id; + + @Column(nullable = false) + private String name; + + private String description; + + @Column(nullable = false) + private int stock; + + private String imageUrl; + + @Column(nullable = false) + private boolean active; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "unit_price")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) + private Money price; + + @Builder.Default + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "product") + private Set variants = new HashSet<>(); + + // Factory method for creating new products + public static Product create(String name, String description, int stock, Money money) { + Product product = new Product(); + product.id = ProductId.generate(); + product.name = Objects.requireNonNull(name, "Product name is required"); + product.description = description; + product.stock = stock; + product.price = Objects.requireNonNull(money, "Product price is required"); + product.active = true; + + // Register domain event + product.registerEvent(new ProductCreatedEvent( + product.getId(), + product.getName(), + product.getPrice() + )); + + return product; + } + + // Domain methods + public void updateStock(int quantity) { + if (quantity < 0) { + throw new IllegalArgumentException("Stock cannot be negative"); + } + this.stock = quantity; + } + + public void addStock(int quantity) { + if (quantity <= 0) { + throw new IllegalArgumentException("Quantity must be positive"); + } + this.stock += quantity; + } + + public void removeStock(int quantity) { + if (quantity <= 0) { + throw new IllegalArgumentException("Quantity must be positive"); + } + if (quantity > this.stock) { + throw new InsufficientStockException(this.id, quantity, this.stock); + } + this.stock -= quantity; + } + + public void updatePrice(Money newPrice) { + this.price = Objects.requireNonNull(newPrice, "Price cannot be null"); + } + + public void activate() { + this.active = true; + } + + public void deactivate() { + this.active = false; + } + + public boolean hasEnoughStock(int quantity) { + return this.stock >= quantity; + } + + public void addVariant(ProductVariant variant) { + Objects.requireNonNull(variant, "Variant cannot be null"); + variants.add(variant); + variant.setProduct(this); + } + + public void removeVariant(ProductVariant variant) { + if (variant.hasActiveOrders()) { + throw new ProductModificationException("Cannot remove variant with active orders"); + } + variants.remove(variant); + variant.setProduct(null); + } + + public boolean hasVariant(String sku) { + return variants.stream() + .anyMatch(variant -> variant.getSku().equals(sku)); + } + + public Money getEffectivePrice(ProductVariant variant) { + return variant != null && variant.getPrice() != null ? + variant.getPrice() : this.price; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Product product)) return false; + return Objects.equals(id, product.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public Optional findVariantBySku(String sku) { + throw new NotImplementedException("find variant by sku has not been implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java new file mode 100644 index 0000000..42d38d6 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java @@ -0,0 +1,95 @@ +package com.zenfulcode.commercify.product.domain.model; + +import com.zenfulcode.commercify.shared.domain.model.Money; +import jakarta.persistence.*; +import lombok.*; +import org.apache.commons.lang3.NotImplementedException; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +@Builder +@Entity +@Table(name = "product_variants") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ProductVariant { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String sku; + + private Integer stock; + + private String imageUrl; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "unit_price")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) + private Money price; + + @Setter + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + + @Builder.Default + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private Set options = new HashSet<>(); + + // Factory method + public static ProductVariant create(String sku, Integer stock, Money price) { + ProductVariant variant = new ProductVariant(); + variant.sku = Objects.requireNonNull(sku, "SKU is required"); + variant.stock = stock; + variant.price = price; + return variant; + } + + // Domain methods + public void updateStock(Integer stock) { + if (stock != null && stock < 0) { + throw new IllegalArgumentException("Stock cannot be negative"); + } + this.stock = stock; + } + + public void addOption(String name, String value) { + options.add(VariantOption.create(name, value, this)); + } + + public boolean hasActiveOrders() { + // This would typically check a repository or domain service + return false; + } + + public Money getEffectivePrice() { + return price != null ? price : product.getPrice(); + } + + public int getEffectiveStock() { + return stock != null ? stock : product.getStock(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProductVariant)) return false; + ProductVariant that = (ProductVariant) o; + return Objects.equals(sku, that.sku); + } + + @Override + public int hashCode() { + return Objects.hash(sku); + } + + public void updatePrice(Money newPrice) { + throw new NotImplementedException("update price has not been implemented"); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java new file mode 100644 index 0000000..488786e --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java @@ -0,0 +1,49 @@ +package com.zenfulcode.commercify.product.domain.model; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +@Entity +@Table(name = "variant_options") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class VariantOption { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String value; + + @ManyToOne(fetch = FetchType.LAZY) + private ProductVariant variant; + + public static VariantOption create(String name, String value, ProductVariant variant) { + VariantOption option = new VariantOption(); + option.name = Objects.requireNonNull(name, "Option name is required"); + option.value = Objects.requireNonNull(value, "Option value is required"); + option.variant = Objects.requireNonNull(variant, "Variant is required"); + return option; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof VariantOption that)) return false; + return Objects.equals(name, that.name) && + Objects.equals(value, that.value) && + Objects.equals(variant, that.variant); + } + + @Override + public int hashCode() { + return Objects.hash(name, value); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductInventoryPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductInventoryPolicy.java new file mode 100644 index 0000000..f3b4cdb --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductInventoryPolicy.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.product.domain.policies; + +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.valueobject.InventoryAdjustment; + +public interface ProductInventoryPolicy { + void initializeInventory(Product product); + void handleStockIncrease(Product product, InventoryAdjustment adjustment); + void handleStockDecrease(Product product, InventoryAdjustment adjustment); + void handleStockCorrection(Product product, InventoryAdjustment adjustment); +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductPricingPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductPricingPolicy.java new file mode 100644 index 0000000..e0cba62 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductPricingPolicy.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.product.domain.policies; + +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.valueobject.VariantSpecification; +import com.zenfulcode.commercify.shared.domain.model.Money; + +public interface ProductPricingPolicy { + void applyDefaultPricing(Product product); + Money calculateVariantPrice(Product product, VariantSpecification spec); + Money validateAndAdjustPrice(Product product, ProductVariant variant, Money newPrice); +} + diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java new file mode 100644 index 0000000..23855ca --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java @@ -0,0 +1,27 @@ +package com.zenfulcode.commercify.product.domain.repository; + +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.valueobject.CategoryId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface ProductRepository { + Product save(Product product); + + Optional findById(ProductId id); + + void delete(Product product); + + Page findAll(Pageable pageable); + + Page findByActiveTrue(Pageable pageable); + + Page findByCategory(CategoryId categoryId, Pageable pageable); + + Page findByStockLessThan(int threshold, Pageable pageable); + + boolean existsBySku(String sku); +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductVariantRepository.java b/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductVariantRepository.java new file mode 100644 index 0000000..e2a4f77 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductVariantRepository.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.product.domain.repository; + +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ProductVariantRepository { + ProductVariant save(ProductVariant variant); + Optional findById(Long id); + Optional findBySku(String sku); + void delete(ProductVariant variant); + List findByProductId(ProductId productId); +} + diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java new file mode 100644 index 0000000..fe635ae --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java @@ -0,0 +1,54 @@ +package com.zenfulcode.commercify.product.domain.service; + +import com.zenfulcode.commercify.product.domain.event.LargeStockIncreaseEvent; +import com.zenfulcode.commercify.product.domain.event.LowStockEvent; +import com.zenfulcode.commercify.product.domain.event.StockCorrectionEvent; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.policies.ProductInventoryPolicy; +import com.zenfulcode.commercify.product.domain.valueobject.InventoryAdjustment; +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DefaultProductInventoryPolicy implements ProductInventoryPolicy { + private static final int LOW_STOCK_THRESHOLD = 5; + private static final int REORDER_THRESHOLD = 10; + + private final DomainEventPublisher eventPublisher; + + @Override + public void initializeInventory(Product product) { + if (product.getStock() <= REORDER_THRESHOLD) { + eventPublisher.publish(new LowStockEvent(product.getId(), product.getStock())); + } + } + + @Override + public void handleStockIncrease(Product product, InventoryAdjustment adjustment) { + if (adjustment.quantity() > 100) { + eventPublisher.publish(new LargeStockIncreaseEvent( + product.getId(), + adjustment.quantity(), + adjustment.reason() + )); + } + } + + @Override + public void handleStockDecrease(Product product, InventoryAdjustment adjustment) { + if (product.getStock() <= LOW_STOCK_THRESHOLD) { + eventPublisher.publish(new LowStockEvent(product.getId(), product.getStock())); + } + } + + @Override + public void handleStockCorrection(Product product, InventoryAdjustment adjustment) { + eventPublisher.publish(new StockCorrectionEvent( + product.getId(), + adjustment.quantity(), + adjustment.reason() + )); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductPricingPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductPricingPolicy.java new file mode 100644 index 0000000..ce721af --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductPricingPolicy.java @@ -0,0 +1,58 @@ +package com.zenfulcode.commercify.product.domain.service; + +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.policies.ProductPricingPolicy; +import com.zenfulcode.commercify.product.domain.valueobject.VariantSpecification; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +@Service +@RequiredArgsConstructor +public class DefaultProductPricingPolicy implements ProductPricingPolicy { + private static final BigDecimal MAXIMUM_VARIANT_PRICE_MULTIPLIER = new BigDecimal("3.0"); + private static final BigDecimal MINIMUM_MARGIN_PERCENTAGE = new BigDecimal("0.10"); + + @Override + public void applyDefaultPricing(Product product) { + // Apply minimum margin check + if (product.getPrice().getAmount() + .compareTo(calculateMinimumPrice(product)) < 0) { + throw new InvalidPriceException("Price does not meet minimum margin requirements"); + } + } + + @Override + public Money calculateVariantPrice(Product product, VariantSpecification spec) { + if (spec.price() == null) { + return product.getPrice(); + } + + // Validate variant price is not too high compared to base price + if (spec.price().getAmount().divide(product.getPrice().getAmount(), 2, RoundingMode.HALF_UP) + .compareTo(MAXIMUM_VARIANT_PRICE_MULTIPLIER) > 0) { + throw new InvalidPriceException("Variant price cannot exceed 3x the base product price"); + } + + return spec.price(); + } + + @Override + public Money validateAndAdjustPrice(Product product, ProductVariant variant, Money newPrice) { + // Ensure variant price meets minimum margin + if (newPrice.getAmount().compareTo(calculateMinimumPrice(product)) < 0) { + throw new InvalidPriceException("Variant price does not meet minimum margin requirements"); + } + + return newPrice; + } + + private Money calculateMinimumPrice(Product product) { + // Implementation of minimum price calculation based on costs and margin + return Money.of(BigDecimal.TEN, product.getPrice().getCurrency()); // Simplified example + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java new file mode 100644 index 0000000..4978633 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java @@ -0,0 +1,193 @@ +package com.zenfulcode.commercify.product.domain.service; + +import com.zenfulcode.commercify.product.domain.exception.ProductValidationException; +import com.zenfulcode.commercify.product.domain.exception.VariantNotFoundException; +import com.zenfulcode.commercify.product.domain.model.*; +import com.zenfulcode.commercify.product.domain.policies.ProductInventoryPolicy; +import com.zenfulcode.commercify.product.domain.policies.ProductPricingPolicy; +import com.zenfulcode.commercify.product.domain.valueobject.*; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ProductDomainService { + private final OrderLineRepository orderLineRepository; + private final ProductInventoryPolicy inventoryPolicy; + private final ProductPricingPolicy pricingPolicy; + private final SkuGenerator skuGenerator; + private final ProductFactory productFactory; + + /** + * Creates a new product with validation and enrichment + */ + public Product createProduct(ProductSpecification spec) { + validateProductSpecification(spec); + + Product product = productFactory.createProduct(spec); + + // Apply any default product policies + pricingPolicy.applyDefaultPricing(product); + inventoryPolicy.initializeInventory(product); + + // Create variants if specified + if (spec.hasVariants()) { + createProductVariants(product, spec.variantSpecs()); + } + + return product; + } + + /** + * Handles complex variant creation logic + */ + public void createProductVariants(Product product, List variantSpecs) { + for (VariantSpecification spec : variantSpecs) { + validateVariantSpecification(spec); + + String sku = skuGenerator.generateSku(product, spec); + Money variantPrice = pricingPolicy.calculateVariantPrice(product, spec); + + ProductVariant variant = ProductVariant.create( + sku, + spec.stock(), + variantPrice + ); + + // Add variant options + spec.options().forEach(option -> + variant.addOption(option.name(), option.value()) + ); + + product.addVariant(variant); + } + } + + /** + * Validates if a product can be deleted + */ + public ProductDeletionValidation validateProductDeletion(Product product) { + List issues = new ArrayList<>(); + + // Check for active orders + if (orderLineRepository.hasActiveOrders(product.getId())) { + issues.add("Product has active orders"); + } + + // Check variants + for (ProductVariant variant : product.getVariants()) { + if (orderLineRepository.hasActiveOrdersForVariant(variant.getId())) { + issues.add("Variant " + variant.getSku() + " has active orders"); + } + } + + // Check inventory + if (product.getStock() > 0) { + issues.add("Product has remaining stock of " + product.getStock() + " units"); + } + + return new ProductDeletionValidation(issues.isEmpty(), issues); + } + + /** + * Updates product stock based on complex business rules + */ + public void adjustInventory(Product product, InventoryAdjustment adjustment) { + validateInventoryAdjustment(adjustment); + + switch (adjustment.type()) { + case STOCK_ADDITION -> { + product.addStock(adjustment.quantity()); + inventoryPolicy.handleStockIncrease(product, adjustment); + } + case STOCK_REMOVAL -> { + product.removeStock(adjustment.quantity()); + inventoryPolicy.handleStockDecrease(product, adjustment); + } + case STOCK_CORRECTION -> { + product.updateStock(adjustment.quantity()); + inventoryPolicy.handleStockCorrection(product, adjustment); + } + } + } + + /** + * Handles variant pricing updates with validation + */ + public void updateVariantPrices(Product product, List updates) { + validatePriceUpdates(updates); + + for (VariantPriceUpdate update : updates) { + ProductVariant variant = product.findVariantBySku(update.sku()) + .orElseThrow(() -> new VariantNotFoundException(update.sku())); + + Money newPrice = pricingPolicy.validateAndAdjustPrice( + product, + variant, + update.newPrice() + ); + + variant.updatePrice(newPrice); + } + } + + private void validateProductSpecification(ProductSpecification spec) { + List violations = new ArrayList<>(); + + if (spec.name() == null || spec.name().isBlank()) { + violations.add("Product name is required"); + } + if (spec.price() == null || spec.price().isNegative()) { + violations.add("Valid product price is required"); + } + if (spec.initialStock() < 0) { + violations.add("Initial stock cannot be negative"); + } + + if (!violations.isEmpty()) { + throw new ProductValidationException(violations); + } + } + + private void validateVariantSpecification(VariantSpecification spec) { + List violations = new ArrayList<>(); + + if (spec.options() == null || spec.options().isEmpty()) { + violations.add("Variant must have at least one option"); + } + if (spec.stock() != null && spec.stock() < 0) { + violations.add("Variant stock cannot be negative"); + } + + if (!violations.isEmpty()) { + throw new ProductValidationException(violations); + } + } + + private void validateInventoryAdjustment(InventoryAdjustment adjustment) { + if (adjustment.quantity() < 0) { + throw new IllegalArgumentException("Adjustment quantity cannot be negative"); + } + } + + private void validatePriceUpdates(List updates) { + updates.forEach(update -> { + if (update.newPrice().isNegative()) { + throw new IllegalArgumentException( + "Price cannot be negative for variant: " + update.sku() + ); + } + }); + } + + public void updateProduct(Product product, ProductUpdateSpec productUpdateSpec) { + + } +} + + + diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java new file mode 100644 index 0000000..038b1c1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java @@ -0,0 +1,104 @@ +package com.zenfulcode.commercify.product.domain.service; + +import com.zenfulcode.commercify.product.domain.exception.ProductValidationException; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.valueobject.ProductSpecification; +import com.zenfulcode.commercify.product.domain.valueobject.VariantSpecification; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ProductFactory { + private final SkuGenerator skuGenerator; + private final DefaultProductPricingPolicy pricingPolicy; + private final DefaultProductInventoryPolicy inventoryPolicy; + + public Product createProduct(ProductSpecification spec) { + // Validate specification + validateSpecification(spec); + + // Create base product + Product product = Product.create(spec.name(), spec.description(), spec.initialStock(), spec.price()); + + // Apply default policies + pricingPolicy.applyDefaultPricing(product); + inventoryPolicy.initializeInventory(product); + + // Create variants if specified + if (spec.hasVariants()) { + createVariants(product, spec.variantSpecs()); + } + + return product; + } + + private void createVariants(Product product, List specs) { + for (VariantSpecification spec : specs) { + String sku = skuGenerator.generateSku(product, spec); + Money variantPrice = pricingPolicy.calculateVariantPrice(product, spec); + + ProductVariant variant = ProductVariant.builder() + .sku(sku) + .stock(spec.stock()) + .price(variantPrice) + .imageUrl(spec.imageUrl()) + .build(); + + // Add variant options + spec.options().forEach(option -> + variant.addOption(option.name(), option.value()) + ); + + product.addVariant(variant); + } + } + + private void validateSpecification(ProductSpecification spec) { + List violations = new ArrayList<>(); + + if (spec.name() == null || spec.name().isBlank()) { + violations.add("Product name is required"); + } + if (spec.price() == null || spec.price().isNegative()) { + violations.add("Valid product price is required"); + } + if (spec.initialStock() < 0) { + violations.add("Initial stock cannot be negative"); + } + + if (!violations.isEmpty()) { + throw new ProductValidationException(violations); + } + } + + public ProductVariant createVariantFromTemplate( + Product product, + ProductVariant template, + VariantSpecification spec) { + + String sku = skuGenerator.generateSku(product, spec); + Money variantPrice = template.getPrice(); + if (spec.price() != null) { + variantPrice = pricingPolicy.calculateVariantPrice(product, spec); + } + + ProductVariant variant = ProductVariant.builder() + .sku(sku) + .stock(spec.stock()) + .price(variantPrice) + .imageUrl(spec.imageUrl() != null ? spec.imageUrl() : template.getImageUrl()) + .build(); + + spec.options().forEach(option -> + variant.addOption(option.name(), option.value()) + ); + + return variant; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/SkuGenerator.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/SkuGenerator.java new file mode 100644 index 0000000..99ff59b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/SkuGenerator.java @@ -0,0 +1,29 @@ +package com.zenfulcode.commercify.product.domain.service; + +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.valueobject.VariantSpecification; +import org.springframework.stereotype.Service; + +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class SkuGenerator { + private static final int SKU_LENGTH = 8; + + public String generateSku(Product product, VariantSpecification spec) { + String basePrefix = product.getName() + .substring(0, Math.min(3, product.getName().length())) + .toUpperCase(); + + String variantSuffix = spec.options().stream() + .map(opt -> opt.value().substring(0, 1)) + .collect(Collectors.joining()); + + String uniquePart = UUID.randomUUID() + .toString() + .substring(0, SKU_LENGTH - basePrefix.length() - variantSuffix.length()); + + return basePrefix + uniquePart + variantSuffix; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java new file mode 100644 index 0000000..eae641f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java @@ -0,0 +1,30 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +import jakarta.persistence.Embeddable; +import lombok.Value; + +import java.util.Objects; +import java.util.UUID; + +@Value +@Embeddable +public class CategoryId { + String value; + + private CategoryId(String value) { + this.value = Objects.requireNonNull(value); + } + + public static CategoryId generate() { + return new CategoryId(UUID.randomUUID().toString()); + } + + public static CategoryId of(String value) { + return new CategoryId(value); + } + + // Required by JPA + protected CategoryId() { + this.value = null; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/InventoryAdjustment.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/InventoryAdjustment.java new file mode 100644 index 0000000..a17a906 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/InventoryAdjustment.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + + +public record InventoryAdjustment( + InventoryAdjustmentType type, + int quantity, + String reason +) {} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/InventoryAdjustmentType.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/InventoryAdjustmentType.java new file mode 100644 index 0000000..daaf377 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/InventoryAdjustmentType.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +public enum InventoryAdjustmentType { + STOCK_ADDITION, + STOCK_REMOVAL, + STOCK_CORRECTION +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductDeletionValidation.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductDeletionValidation.java new file mode 100644 index 0000000..7a5b77c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductDeletionValidation.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +import java.util.List; + +public record ProductDeletionValidation( + boolean canDelete, + List issues +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java new file mode 100644 index 0000000..24b7bef --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java @@ -0,0 +1,30 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +import jakarta.persistence.Embeddable; +import lombok.Value; + +import java.util.Objects; +import java.util.UUID; + +@Value +@Embeddable +public class ProductId { + String value; + + private ProductId(String value) { + this.value = Objects.requireNonNull(value); + } + + public static ProductId generate() { + return new ProductId(UUID.randomUUID().toString()); + } + + public static ProductId of(String value) { + return new ProductId(value); + } + + // Required by JPA + protected ProductId() { + this.value = null; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductSpecification.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductSpecification.java new file mode 100644 index 0000000..b92772c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductSpecification.java @@ -0,0 +1,17 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +import com.zenfulcode.commercify.shared.domain.model.Money; + +import java.util.List; + +public record ProductSpecification( + String name, + String description, + int initialStock, + Money price, + List variantSpecs +) { + public boolean hasVariants() { + return variantSpecs != null && !variantSpecs.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductUpdateSpec.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductUpdateSpec.java new file mode 100644 index 0000000..a5bc011 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductUpdateSpec.java @@ -0,0 +1,31 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +import com.zenfulcode.commercify.shared.domain.model.Money; + +public record ProductUpdateSpec( + String name, + String description, + Integer stock, + Money price, + Boolean active +) { + public boolean hasNameUpdate() { + return name != null; + } + + public boolean hasDescriptionUpdate() { + return description != null; + } + + public boolean hasStockUpdate() { + return stock != null; + } + + public boolean hasPriceUpdate() { + return price != null; + } + + public boolean hasActiveUpdate() { + return active != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantOption.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantOption.java new file mode 100644 index 0000000..9cdfc8f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantOption.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +public record VariantOption( + String name, + String value +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantPriceUpdate.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantPriceUpdate.java new file mode 100644 index 0000000..83e13b0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantPriceUpdate.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +import com.zenfulcode.commercify.shared.domain.model.Money; + +public record VariantPriceUpdate( + String sku, + Money newPrice +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantSpecification.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantSpecification.java new file mode 100644 index 0000000..b9d15bf --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantSpecification.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +import com.zenfulcode.commercify.shared.domain.model.Money; + +import java.util.List; + +public record VariantSpecification( + Integer stock, + Money price, + String imageUrl, + List options +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java new file mode 100644 index 0000000..d898ff2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java @@ -0,0 +1,61 @@ +package com.zenfulcode.commercify.product.infrastructure.persistence; + + +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.repository.ProductRepository; +import com.zenfulcode.commercify.product.domain.valueobject.CategoryId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public class JpaProductRepository implements ProductRepository { + private final SpringDataJpaProductRepository repository; + + JpaProductRepository(SpringDataJpaProductRepository repository) { + this.repository = repository; + } + + @Override + public Product save(Product product) { + return repository.save(product); + } + + @Override + public Optional findById(ProductId id) { + return repository.findById(id.getValue()); + } + + @Override + public void delete(Product product) { + + } + + @Override + public Page findAll(Pageable pageable) { + return null; + } + + @Override + public Page findByActiveTrue(Pageable pageable) { + return null; + } + + @Override + public Page findByCategory(CategoryId categoryId, Pageable pageable) { + return repository.findByCategoryId(categoryId.getValue(), pageable); + } + + @Override + public Page findByStockLessThan(int threshold, Pageable pageable) { + return null; + } + + @Override + public boolean existsBySku(String sku) { + return false; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductVariantRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductVariantRepository.java new file mode 100644 index 0000000..b1a813f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductVariantRepository.java @@ -0,0 +1,43 @@ +package com.zenfulcode.commercify.product.infrastructure.persistence; + +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.repository.ProductVariantRepository; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +class JpaProductVariantRepository implements ProductVariantRepository { + private final SpringDataJpaVariantRepository repository; + + JpaProductVariantRepository(SpringDataJpaVariantRepository repository) { + this.repository = repository; + } + + @Override + public ProductVariant save(ProductVariant variant) { + return null; + } + + @Override + public Optional findById(Long id) { + return Optional.empty(); + } + + @Override + public Optional findBySku(String sku) { + return Optional.empty(); + } + + @Override + public void delete(ProductVariant variant) { + + } + + @Override + public List findByProductId(ProductId productId) { + return List.of(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java new file mode 100644 index 0000000..f9d1a46 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.product.infrastructure.persistence; + +import com.zenfulcode.commercify.product.domain.model.Product; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +interface SpringDataJpaProductRepository extends JpaRepository { + Page findByActiveTrue(Pageable pageable); + + Page findByCategoryId(String categoryId, Pageable pageable); + + Page findByStockLessThan(int threshold, Pageable pageable); + + boolean existsBySku(String sku); +} diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaVariantRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaVariantRepository.java new file mode 100644 index 0000000..cd8ec20 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaVariantRepository.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.product.infrastructure.persistence; + +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +interface SpringDataJpaVariantRepository extends JpaRepository { + Optional findBySku(String sku); + + List findByProductId(String productId); +} diff --git a/src/main/java/com/zenfulcode/commercify/repository/AddressRepository.java b/src/main/java/com/zenfulcode/commercify/repository/AddressRepository.java deleted file mode 100644 index 57e4651..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/AddressRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.model.Address; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface AddressRepository extends JpaRepository { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/ConfirmationTokenRepository.java b/src/main/java/com/zenfulcode/commercify/repository/ConfirmationTokenRepository.java deleted file mode 100644 index 698455f..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/ConfirmationTokenRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.model.ConfirmationToken; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface ConfirmationTokenRepository extends JpaRepository { - Optional findByToken(String token); - Optional findByUserIdAndConfirmedFalse(Long userId); -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/OrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/repository/OrderLineRepository.java deleted file mode 100644 index 0f0a7d1..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/OrderLineRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.OrderLine; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.Collection; -import java.util.Set; - -@Repository -public interface OrderLineRepository extends JpaRepository { - @Query("SELECT DISTINCT ol.order FROM OrderLine ol " + - "WHERE ol.productId = :productId " + - "AND ol.order.status IN :statuses") - Set findActiveOrdersForProduct( - @Param("productId") Long productId, - @Param("statuses") Collection statuses - ); - - @Query("SELECT DISTINCT ol.order FROM OrderLine ol " + - "JOIN ol.productVariant v " + - "WHERE v.id = :variantId " + - "AND ol.order.status IN :statuses") - Set findActiveOrdersForVariant( - @Param("variantId") Long variantId, - @Param("statuses") Collection statuses - ); -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/OrderRepository.java b/src/main/java/com/zenfulcode/commercify/repository/OrderRepository.java deleted file mode 100644 index 5225ec4..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/OrderRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.model.Order; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface OrderRepository extends JpaRepository { - Page findByUserId(Long userId, Pageable pageable); - - boolean existsByIdAndUserId(Long id, Long userId); -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/OrderShippingInfoRepository.java b/src/main/java/com/zenfulcode/commercify/repository/OrderShippingInfoRepository.java deleted file mode 100644 index e162ba6..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/OrderShippingInfoRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.model.OrderShippingInfo; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface OrderShippingInfoRepository extends JpaRepository { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/PaymentRepository.java b/src/main/java/com/zenfulcode/commercify/repository/PaymentRepository.java deleted file mode 100644 index 69289d3..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/PaymentRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.model.Payment; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface PaymentRepository extends JpaRepository { - Optional findByOrderId(Long orderId); - - Optional findByMobilePayReference(String mobilePayReference); -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/repository/ProductRepository.java b/src/main/java/com/zenfulcode/commercify/repository/ProductRepository.java deleted file mode 100644 index e38e029..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/ProductRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.model.Product; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface ProductRepository extends JpaRepository { - Page queryAllByActiveTrue(Pageable pageable); -} diff --git a/src/main/java/com/zenfulcode/commercify/repository/ProductVariantRepository.java b/src/main/java/com/zenfulcode/commercify/repository/ProductVariantRepository.java deleted file mode 100644 index 24344fa..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/ProductVariantRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.model.ProductVariant; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductVariantRepository extends JpaRepository { - Page findByProductId(Long productId, Pageable pageable); -} diff --git a/src/main/java/com/zenfulcode/commercify/repository/UserRepository.java b/src/main/java/com/zenfulcode/commercify/repository/UserRepository.java deleted file mode 100644 index bd68644..0000000 --- a/src/main/java/com/zenfulcode/commercify/repository/UserRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zenfulcode.commercify.repository; - -import com.zenfulcode.commercify.domain.model.User; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/zenfulcode/commercify/service/ProductDeletionService.java b/src/main/java/com/zenfulcode/commercify/service/ProductDeletionService.java deleted file mode 100644 index 1f08ee9..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/ProductDeletionService.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.zenfulcode.commercify.service; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.domain.model.Product; -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import com.zenfulcode.commercify.web.dto.common.ProductDeletionValidationResult; -import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; -import com.zenfulcode.commercify.exception.ProductDeletionException; -import com.zenfulcode.commercify.repository.OrderLineRepository; -import com.zenfulcode.commercify.repository.ProductRepository; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - -@Service -@AllArgsConstructor -public class ProductDeletionService { - private final OrderLineRepository orderLineRepository; - private final OrderMapper orderMapper; - private final ProductRepository productRepository; - - public void validateAndDelete(Product product) { - ProductDeletionValidationResult validationResult = validateDeletion(product); - - if (!validationResult.canDelete()) { - throw new ProductDeletionException( - "Cannot delete product", - validationResult.getIssues(), - validationResult.getActiveOrders() - ); - } - - productRepository.delete(product); - } - - private ProductDeletionValidationResult validateDeletion(Product product) { - List activeOrders = orderLineRepository - .findActiveOrdersForProduct( - product.getId(), - List.of(OrderStatus.PENDING, OrderStatus.CONFIRMED, OrderStatus.SHIPPED) - ) - .stream() - .map(orderMapper) - .toList(); - - List issues = new ArrayList<>(); - if (!activeOrders.isEmpty()) { - issues.add(String.format("Product has %d active orders", activeOrders.size())); - } - - return new ProductDeletionValidationResult(issues.isEmpty(), issues, activeOrders); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/ProductVariantService.java b/src/main/java/com/zenfulcode/commercify/service/ProductVariantService.java deleted file mode 100644 index c59bffa..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/ProductVariantService.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.zenfulcode.commercify.service; - -import com.zenfulcode.commercify.web.dto.request.product.CreateVariantOptionRequest; -import com.zenfulcode.commercify.web.dto.request.product.ProductVariantRequest; -import com.zenfulcode.commercify.domain.model.Product; -import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; -import com.zenfulcode.commercify.web.dto.mapper.ProductVariantMapper; -import com.zenfulcode.commercify.domain.model.ProductVariant; -import com.zenfulcode.commercify.domain.model.VariantOption; -import com.zenfulcode.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.repository.ProductVariantRepository; -import com.zenfulcode.commercify.service.validations.ProductValidationService; -import lombok.AllArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Service -@AllArgsConstructor -public class ProductVariantService { - private final ProductRepository productRepository; - private final ProductVariantRepository variantRepository; - private final ProductVariantMapper variantMapper; - private final ProductValidationService validationService; - - @Transactional - public ProductVariantEntityDto addVariant(Long productId, ProductVariantRequest request) { - validationService.validateVariantRequest(request); - Product product = getProduct(productId); - - ProductVariant variant = createVariantFromRequest(request, product); - product.addVariant(variant); - - ProductVariant savedVariant = variantRepository.save(variant); - return variantMapper.apply(savedVariant); - } - - @Transactional - public ProductVariantEntityDto updateVariant(Long productId, Long variantId, ProductVariantRequest request) { - validationService.validateVariantRequest(request); - - ProductVariant variant = getVariant(productId, variantId); - updateVariantDetails(variant, request); - - ProductVariant savedVariant = variantRepository.save(variant); - return variantMapper.apply(savedVariant); - } - - @Transactional - public void deleteVariant(Long productId, Long variantId) { - ProductVariant variant = getVariant(productId, variantId); - validationService.validateVariantDeletion(variant); - variantRepository.delete(variant); - } - - @Transactional(readOnly = true) - public Page getProductVariants(Long productId, PageRequest pageRequest) { - // Verify product exists - getProduct(productId); - return variantRepository.findByProductId(productId, pageRequest) - .map(variantMapper); - } - - public ProductVariantEntityDto getVariantDto(Long productId, Long variantId) { - ProductVariant variant = getVariant(productId, variantId); - Product product = variant.getProduct(); - - ProductVariantEntityDto dto = variantMapper.apply(variant); - - // Apply inheritance only when retrieving - dto.setStock(variant.getStock() != null ? variant.getStock() : product.getStock()); - dto.setImageUrl(variant.getImageUrl() != null ? variant.getImageUrl() : product.getImageUrl()); - dto.setUnitPrice(variant.getUnitPrice() != null ? variant.getUnitPrice() : product.getUnitPrice()); - - return dto; - } - - @Transactional(readOnly = true) - public Set createVariantsFromRequest(List requests, Product product) { - return requests.stream() - .map(request -> createVariantFromRequest(request, product)) - .collect(Collectors.toSet()); - } - - private Product getProduct(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new ProductNotFoundException(productId)); - } - - private ProductVariant getVariant(Long productId, Long variantId) { - Product product = getProduct(productId); - return product.getVariants().stream() - .filter(variant -> variant.getId().equals(variantId)) - .findFirst() - .orElseThrow(() -> new RuntimeException( - String.format("Variant %d not found for product %d", variantId, product.getId()) - )); - } - - private ProductVariant createVariantFromRequest(ProductVariantRequest request, Product product) { - ProductVariant variant = new ProductVariant(); - updateVariantDetails(variant, request); - variant.setProduct(product); - return variant; - } - - private void updateVariantDetails(ProductVariant variant, ProductVariantRequest request) { - variant.setSku(request.sku()); - variant.setStock(request.stock()); - variant.setImageUrl(request.imageUrl()); - variant.setUnitPrice(request.unitPrice()); - updateVariantOptions(variant, request.options()); - } - - private void updateVariantOptions(ProductVariant variant, List options) { - variant.getOptions().clear(); - options.forEach(optionRequest -> { - VariantOption option = VariantOption.builder() - .name(optionRequest.name()) - .value(optionRequest.value()) - .productVariant(variant) - .build(); - variant.addOption(option); - }); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/StockManagementService.java b/src/main/java/com/zenfulcode/commercify/service/StockManagementService.java deleted file mode 100644 index f2faf49..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/StockManagementService.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.zenfulcode.commercify.service; - -import com.zenfulcode.commercify.domain.model.OrderLine; -import com.zenfulcode.commercify.domain.model.Product; -import com.zenfulcode.commercify.domain.model.ProductVariant; -import com.zenfulcode.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.repository.ProductVariantRepository; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Set; - -@Service -@AllArgsConstructor -public class StockManagementService { - private final ProductRepository productRepository; - private final ProductVariantRepository variantRepository; - - @Transactional - public void updateStockLevels(Set orderLines) { - for (OrderLine line : orderLines) { - if (line.getProductVariant() != null) { - // Update variant stock - ProductVariant variant = line.getProductVariant(); - variant.setStock(variant.getStock() - line.getQuantity()); - variantRepository.save(variant); - } else { - // Update product stock - Product product = productRepository.findById(line.getProductId()) - .orElseThrow(() -> new ProductNotFoundException(line.getProductId())); - product.setStock(product.getStock() - line.getQuantity()); - productRepository.save(product); - } - } - } - - @Transactional - public void restoreStockLevels(Set orderLines) { - for (OrderLine line : orderLines) { - if (line.getProductVariant() != null) { - // Restore variant stock - ProductVariant variant = line.getProductVariant(); - variant.setStock(variant.getStock() + line.getQuantity()); - variantRepository.save(variant); - } else { - // Restore product stock - Product product = productRepository.findById(line.getProductId()) - .orElseThrow(() -> new ProductNotFoundException(line.getProductId())); - product.setStock(product.getStock() + line.getQuantity()); - productRepository.save(product); - } - } - } -} diff --git a/src/main/java/com/zenfulcode/commercify/service/authentication/AuthenticationService.java b/src/main/java/com/zenfulcode/commercify/service/authentication/AuthenticationService.java deleted file mode 100644 index 8dcd5cf..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/authentication/AuthenticationService.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.zenfulcode.commercify.service.authentication; - - -import com.zenfulcode.commercify.web.dto.request.auth.LoginUserRequest; -import com.zenfulcode.commercify.web.dto.request.auth.RegisterUserRequest; -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import com.zenfulcode.commercify.web.dto.mapper.UserMapper; -import com.zenfulcode.commercify.domain.model.Address; -import com.zenfulcode.commercify.domain.model.User; -import com.zenfulcode.commercify.repository.UserRepository; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; - -@Service -@AllArgsConstructor -@Slf4j -public class AuthenticationService { - private final UserRepository userRepository; - private final AuthenticationManager authenticationManager; - private final UserMapper mapper; - private final JwtService jwtService; - private final PasswordEncoder passwordEncoder; - - @Transactional - public UserDTO registerUser(RegisterUserRequest registerRequest) { - Optional existing = userRepository.findByEmail(registerRequest.email()); - - if (existing.isPresent()) { - throw new RuntimeException("User with email " + registerRequest.email() + " already exists"); - } - - Address shippingAddress = null; - - if (registerRequest.defaultAddress() != null) { - shippingAddress = Address.builder() - .street(registerRequest.defaultAddress().getStreet()) - .city(registerRequest.defaultAddress().getCity()) - .state(registerRequest.defaultAddress().getState()) - .zipCode(registerRequest.defaultAddress().getZipCode()) - .country(registerRequest.defaultAddress().getCountry()) - .build(); - } - - User user = User.builder() - .firstName(registerRequest.firstName()) - .lastName(registerRequest.lastName()) - .email(registerRequest.email()) - .password(passwordEncoder.encode(registerRequest.password())) - .roles(List.of("USER")) - .defaultAddress(shippingAddress) - .emailConfirmed(false) - .build(); - - User savedUser = userRepository.save(user); - - // TODO: Send user confirmation email - - return mapper.apply(savedUser); - } - - public UserDTO authenticate(LoginUserRequest login) { - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - login.email(), - login.password() - ) - ); - - User user = userRepository.findByEmail(login.email()).orElseThrow(); - - if (!passwordEncoder.matches(login.password(), user.getPassword())) - return null; - - return mapper.apply(user); - } - - @Transactional(readOnly = true) - public UserDTO getAuthenticatedUser(String authHeader) { - if (!authHeader.startsWith("Bearer ")) return null; - - final String jwt = authHeader.substring(7); - final String email = jwtService.extractUsername(jwt); - - return userRepository.findByEmail(email) - .map(mapper) - .orElseThrow(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/service/authentication/JwtService.java b/src/main/java/com/zenfulcode/commercify/service/authentication/JwtService.java deleted file mode 100644 index c8c7b64..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/authentication/JwtService.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.zenfulcode.commercify.service.authentication; - -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; - -import javax.crypto.SecretKey; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -@Service -public class JwtService { - @Value("${security.jwt.secret-key}") - private String secretKey; - - @Value("${security.jwt.expiration-time}") - private long jwtExpiration; - - public String extractUsername(String token) { - return extractClaim(token, Claims::getSubject); - } - - public T extractClaim(String token, Function claimsResolver) { - final Claims claims = extractAllClaims(token); - return claimsResolver.apply(claims); - } - - public String generateToken(UserDTO user) { - return generateToken(new HashMap<>(), user); - } - - public String generateToken(Map extraClaims, UserDTO user) { - extraClaims.put("roles", user.getRoles()); - - return buildToken(extraClaims, user); - } - - public long getExpirationTime() { - return jwtExpiration; - } - - private String buildToken( - Map extraClaims, - UserDTO user - ) { - return Jwts - .builder() - .claims(extraClaims) - .subject(user.getEmail()) - .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + jwtExpiration)) - .signWith(getSignInKey()) - .compact(); - } - - public boolean isTokenValid(String token, UserDetails userDetails) { - final String username = extractUsername(token); - return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); - } - - private boolean isTokenExpired(String token) { - return extractExpiration(token).before(new Date()); - } - - private Date extractExpiration(String token) { - return extractClaim(token, Claims::getExpiration); - } - - private Claims extractAllClaims(String token) { - return Jwts - .parser() - .verifyWith(getSignInKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - } - - private SecretKey getSignInKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); - return Keys.hmacShaKeyFor(keyBytes); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/core/OrderService.java b/src/main/java/com/zenfulcode/commercify/service/core/OrderService.java deleted file mode 100644 index d56900f..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/core/OrderService.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.zenfulcode.commercify.service.core; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.domain.model.*; -import com.zenfulcode.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.repository.OrderShippingInfoRepository; -import com.zenfulcode.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.repository.ProductVariantRepository; -import com.zenfulcode.commercify.service.StockManagementService; -import com.zenfulcode.commercify.service.validations.OrderValidationService; -import com.zenfulcode.commercify.web.dto.common.*; -import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; -import com.zenfulcode.commercify.web.dto.mapper.ProductMapper; -import com.zenfulcode.commercify.web.dto.mapper.ProductVariantMapper; -import com.zenfulcode.commercify.web.dto.request.order.CreateOrderLineRequest; -import com.zenfulcode.commercify.web.dto.request.order.CreateOrderRequest; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Service -@AllArgsConstructor -@Slf4j -public class OrderService { - private final OrderRepository orderRepository; - private final ProductRepository productRepository; - private final OrderMapper orderMapper; - private final ProductMapper productMapper; - private final OrderValidationService validationService; - private final StockManagementService stockService; - private final ProductVariantRepository variantRepository; - private final ProductVariantMapper productVariantMapper; - private final OrderShippingInfoRepository orderShippingInfoRepository; - - @Transactional - public OrderDTO createOrder(CreateOrderRequest request) { - return createOrder(null, request); - } - - @Transactional - public OrderDTO createOrder(Long userId, CreateOrderRequest request) { - // Validate request and check stock - validationService.validateCreateOrderRequest(request); - - // Get and validate all products and variants upfront - Map products = getAndValidateProducts(request.orderLines()); - Map variants = getAndValidateVariants(request.orderLines()); - - // Get shipping information - OrderShippingInfo shippingInfo = getShippingInformation(request); - orderShippingInfoRepository.save(shippingInfo); - - // Create order entity - Order order = buildOrderEntity(userId, request, products, variants, shippingInfo); - Order savedOrder = orderRepository.save(order); - - return orderMapper.apply(savedOrder); - } - - private OrderShippingInfo getShippingInformation(CreateOrderRequest request) { - AddressDTO shippingAddress = request.shippingAddress(); - - OrderShippingInfo.OrderShippingInfoBuilder shippingInfo = OrderShippingInfo.builder() - .shippingStreet(shippingAddress.getStreet()) - .shippingCity(shippingAddress.getCity()) - .shippingState(shippingAddress.getState()) - .shippingZip(shippingAddress.getZipCode()) - .shippingCountry(shippingAddress.getCountry()); - - AddressDTO billingAddress = request.billingAddress(); - if (billingAddress != null) { - shippingInfo.billingStreet(billingAddress.getStreet()) - .billingCity(billingAddress.getCity()) - .billingState(billingAddress.getState()) - .billingZip(billingAddress.getZipCode()) - .billingCountry(billingAddress.getCountry()); - } - - shippingInfo.customerEmail(request.customerDetails().getEmail()) - .customerFirstName(request.customerDetails().getFirstName()) - .customerLastName(request.customerDetails().getLastName()) - .customerPhone(request.customerDetails().getPhone()); - - return shippingInfo.build(); - } - - @Transactional - public void updateOrderStatus(Long orderId, OrderStatus newStatus) { - Order order = findOrderById(orderId); - OrderStatus oldStatus = order.getStatus(); - - validationService.validateStatusTransition(oldStatus, newStatus); - order.setStatus(newStatus); - - orderRepository.save(order); - } - - @Transactional - public void cancelOrder(Long orderId) { - Order order = findOrderById(orderId); - validationService.validateOrderCancellation(order); - - // Restore stock levels - stockService.restoreStockLevels(order.getOrderLines()); - - order.setStatus(OrderStatus.CANCELLED); - orderRepository.save(order); - } - - @Transactional(readOnly = true) - public Page getOrdersByUserId(Long userId, Pageable pageable) { - return orderRepository.findByUserId(userId, pageable).map(orderMapper); - } - - @Transactional(readOnly = true) - public Page getAllOrders(Pageable pageable) { - return orderRepository.findAll(pageable).map(orderMapper); - } - - @Transactional(readOnly = true) - public OrderDetailsDTO getOrderById(Long orderId) { - Order order = findOrderById(orderId); - return buildOrderDetailsDTO(order); - } - - @Transactional(readOnly = true) - public boolean isOrderOwnedByUser(Long orderId, Long userId) { - return orderRepository.existsByIdAndUserId(orderId, userId); - } - - private Map getAndValidateVariants(List orderLines) { - Set variantIds = orderLines.stream() - .map(CreateOrderLineRequest::variantId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - if (variantIds.isEmpty()) { - return Collections.emptyMap(); - } - - Map variants = variantRepository - .findAllById(variantIds).stream().collect(Collectors.toMap(ProductVariant::getId, Function.identity())); - - // Validate all requested variants exist and have sufficient stock - orderLines.forEach(line -> { - if (line.variantId() != null) { - ProductVariant variant = variants.get(line.variantId()); - if (variant == null) { - throw new ProductNotFoundException(line.variantId()); - } - - // TODO - Validate stock levels - - if (!variant.getProduct().getId().equals(line.productId())) { - throw new IllegalArgumentException( - String.format("Variant %d does not belong to product %d", - line.variantId(), line.productId()) - ); - } - } - }); - - return variants; - } - - public double calculateTotalAmount(Set orderLines) { - return orderLines.stream() - .mapToDouble(line -> line.getUnitPrice() * line.getQuantity()) - .sum(); - } - - private Order buildOrderEntity(Long userId, - CreateOrderRequest request, - Map products, - Map variants, - OrderShippingInfo shippingInfo) { - // Create order lines first - Set orderLines = request.orderLines().stream() - .map(line -> createOrderLine(line.quantity(), products.get(line.productId()), - line.variantId() != null ? variants.get(line.variantId()) : null)) - .collect(Collectors.toSet()); - - double totalAmount = calculateTotalAmount(orderLines); - - Order order = Order.builder() - .userId(userId) - .orderLines(orderLines) - .status(OrderStatus.PENDING) - .currency(request.currency()) - .totalAmount(totalAmount) - .orderShippingInfo(shippingInfo) - .build(); - - // Set up bidirectional relationship - orderLines.forEach(line -> line.setOrder(order)); - - return order; - } - - private OrderLine createOrderLine( - int quantity, - Product product, - ProductVariant variant) { - - double unitPrice = variant != null && variant.getUnitPrice() != null ? variant.getUnitPrice() : product.getUnitPrice(); - - return OrderLine.builder() - .productId(product.getId()) - .productVariant(variant) - .quantity(quantity) - .unitPrice(unitPrice) - .currency(product.getCurrency()) - .build(); - } - - private Map getAndValidateProducts(List orderLines) { - Set productIds = orderLines.stream().map(CreateOrderLineRequest::productId).collect(Collectors.toSet()); - - Map products = productRepository.findAllById(productIds).stream().collect(Collectors.toMap(com.zenfulcode.commercify.domain.model.Product::getId, Function.identity())); - - orderLines.forEach(line -> { - Product product = products.get(line.productId()); - if (product == null) { - throw new ProductNotFoundException(line.productId()); - } - if (!product.getActive()) { - throw new IllegalArgumentException("Product is not active: " + line.productId()); - } - }); - - return products; - } - - private Order findOrderById(Long orderId) { - return orderRepository.findById(orderId).orElseThrow(() -> new OrderNotFoundException(orderId)); - } - - private OrderDetailsDTO buildOrderDetailsDTO(Order order) { - List orderLines = order.getOrderLines().stream() - .map(line -> { - Product product = productRepository.findById(line.getProductId()) - .orElseThrow(() -> new ProductNotFoundException(line.getProductId())); - - return OrderLineDTO.builder() - .id(line.getId()) - .productId(line.getProductId()) - .quantity(line.getQuantity()) - .unitPrice(line.getUnitPrice()) - .currency(line.getCurrency()) - .product(productMapper.apply(product)) - .variant(line.getProductVariant() != null ? - productVariantMapper.apply(line.getProductVariant()) : null) - .build(); - }) - .collect(Collectors.toList()); - - OrderDTO orderDTO = orderMapper.apply(order); - - OrderShippingInfo shippingInfo = order.getOrderShippingInfo(); - - System.out.println("shippingInfo = " + shippingInfo); - - CustomerDetailsDTO customerDetails = CustomerDetailsDTO.builder() - .email(shippingInfo.getCustomerEmail()) - .firstName(shippingInfo.getCustomerFirstName()) - .lastName(shippingInfo.getCustomerLastName()) - .build(); - - AddressDTO shippingAddress = AddressDTO.builder() - .city(shippingInfo.getShippingCity()) - .country(shippingInfo.getShippingCountry()) - .state(shippingInfo.getShippingState()) - .street(shippingInfo.getShippingStreet()) - .zipCode(shippingInfo.getShippingZip()) - .build(); - - AddressDTO billingAddress = AddressDTO.builder() - .city(shippingInfo.getBillingCity()) - .country(shippingInfo.getBillingCountry()) - .state(shippingInfo.getBillingState()) - .street(shippingInfo.getBillingStreet()) - .zipCode(shippingInfo.getBillingZip()) - .build(); - - return new OrderDetailsDTO(orderDTO, orderLines, customerDetails, shippingAddress, billingAddress); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/service/core/PaymentService.java b/src/main/java/com/zenfulcode/commercify/service/core/PaymentService.java deleted file mode 100644 index 9a62b50..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/core/PaymentService.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.zenfulcode.commercify.service.core; - -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; -import com.zenfulcode.commercify.domain.model.Payment; -import com.zenfulcode.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.service.email.EmailService; -import jakarta.mail.MessagingException; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@AllArgsConstructor -@Slf4j -public class PaymentService { - private final PaymentRepository paymentRepository; - private final OrderRepository orderRepository; - private final EmailService emailService; - private final OrderService orderService; - - @Transactional - public void handlePaymentStatusUpdate(Long orderId, PaymentStatus newStatus) { - Payment payment = paymentRepository.findByOrderId(orderId) - .orElseThrow(() -> new RuntimeException("Payment not found for order: " + orderId)); - - PaymentStatus oldStatus = payment.getStatus(); - payment.setStatus(newStatus); - paymentRepository.save(payment); - - // If payment is successful, send confirmation email - if (newStatus == PaymentStatus.PAID && oldStatus != PaymentStatus.PAID) { - try { - orderRepository.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found: " + orderId)); - - // Get order details for email - OrderDetailsDTO orderDetails = orderService.getOrderById(orderId); - - // Send confirmation email - emailService.sendOrderConfirmation(orderDetails); - - log.info("Order confirmation email sent for order: {}", orderId); - } catch (MessagingException e) { - log.error("Failed to send order confirmation email for order {}: {}", - orderId, e.getMessage()); - } - } - } - - public PaymentStatus getPaymentStatus(Long orderId) { - return paymentRepository.findByOrderId(orderId) - .map(Payment::getStatus) - .orElse(PaymentStatus.NOT_FOUND); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/core/ProductService.java b/src/main/java/com/zenfulcode/commercify/service/core/ProductService.java deleted file mode 100644 index 4781063..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/core/ProductService.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.zenfulcode.commercify.service.core; - -import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; -import com.zenfulcode.commercify.domain.model.Product; -import com.zenfulcode.commercify.web.dto.common.ProductDTO; -import com.zenfulcode.commercify.web.dto.common.ProductUpdateResult; -import com.zenfulcode.commercify.web.dto.mapper.ProductMapper; -import com.zenfulcode.commercify.domain.model.ProductVariant; -import com.zenfulcode.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.component.factory.ProductFactory; -import com.zenfulcode.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.service.ProductDeletionService; -import com.zenfulcode.commercify.service.validations.ProductValidationService; -import com.zenfulcode.commercify.service.ProductVariantService; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collections; -import java.util.Set; - -@Service -@AllArgsConstructor -@Slf4j -public class ProductService { - private final ProductRepository productRepository; - private final ProductMapper productMapper; - private final ProductFactory productFactory; - private final ProductValidationService validationService; - private final ProductVariantService variantService; - private final ProductDeletionService deletionService; - - @Transactional - public ProductDTO saveProduct(ProductRequest request) { - validationService.validateProductRequest(request); - Product product = productFactory.createFromRequest(request); - - if (request.variants() != null && !request.variants().isEmpty()) { - Set variants = variantService.createVariantsFromRequest(request.variants(), product); - product.setVariants(variants); - } - - Product savedProduct = productRepository.save(product); - return productMapper.apply(savedProduct); - } - - @Transactional - public ProductUpdateResult updateProduct(Long id, ProductRequest request) { - Product product = productRepository.findById(id) - .orElseThrow(() -> new ProductNotFoundException(id)); - - updateProductDetails(product, request); - Product savedProduct = productRepository.save(product); - - return ProductUpdateResult.withWarnings( - productMapper.apply(savedProduct), - Collections.emptyList() - ); - } - - @Transactional - public void deleteProduct(Long id) { - Product product = productRepository.findById(id) - .orElseThrow(() -> new ProductNotFoundException(id)); - - deletionService.validateAndDelete(product); - } - - @Transactional - public void reactivateProduct(Long id) { - toggleProductStatus(id, true); - } - - @Transactional - public void deactivateProduct(Long id) { - toggleProductStatus(id, false); - } - - @Transactional(readOnly = true) - public Page getAllProducts(PageRequest pageRequest) { - return productRepository.findAll(pageRequest).map(productMapper); - } - - @Transactional(readOnly = true) - public ProductDTO getProductById(Long id) { - return productRepository.findById(id) - .map(productMapper) - .orElseThrow(() -> new ProductNotFoundException(id)); - } - - @Transactional(readOnly = true) - public Page getActiveProducts(PageRequest pageRequest) { - return productRepository.queryAllByActiveTrue(pageRequest).map(productMapper); - } - - private void toggleProductStatus(Long id, boolean active) { - Product product = productRepository.findById(id) - .orElseThrow(() -> new ProductNotFoundException(id)); - - product.setActive(active); - productRepository.save(product); - } - - private void updateProductDetails(Product product, ProductRequest request) { - if (request.name() != null) product.setName(request.name()); - if (request.description() != null) product.setDescription(request.description()); - if (request.stock() != null) product.setStock(request.stock()); - if (request.active() != null) product.setActive(request.active()); - if (request.imageUrl() != null) product.setImageUrl(request.imageUrl()); - if (request.price() != null) { - product.setUnitPrice(request.price().amount()); - product.setCurrency(request.price().currency()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/core/UserManagementService.java b/src/main/java/com/zenfulcode/commercify/service/core/UserManagementService.java deleted file mode 100644 index ab00a90..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/core/UserManagementService.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.zenfulcode.commercify.service.core; - - -import com.zenfulcode.commercify.web.dto.common.AddressDTO; -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import com.zenfulcode.commercify.web.dto.mapper.AddressMapper; -import com.zenfulcode.commercify.web.dto.mapper.UserMapper; -import com.zenfulcode.commercify.domain.model.Address; -import com.zenfulcode.commercify.domain.model.User; -import com.zenfulcode.commercify.repository.UserRepository; -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 java.util.ArrayList; - -@Service -@RequiredArgsConstructor -public class UserManagementService { - private final UserRepository userRepository; - private final UserMapper mapper; - private final AddressMapper addressMapper; - - @Transactional(readOnly = true) - public UserDTO getUserById(Long id) { - User user = userRepository.findById(id) - .orElseThrow(() -> new RuntimeException("User not found")); - return mapper.apply(user); - } - - @Transactional(readOnly = true) - public Page getAllUsers(Pageable pageable) { - return userRepository.findAll(pageable).map(mapper); - } - - @Transactional - public UserDTO updateUser(Long id, UserDTO userDTO) { - User user = userRepository.findById(id) - .orElseThrow(() -> new RuntimeException("User not found")); - - user.setFirstName(userDTO.getFirstName()); - user.setLastName(userDTO.getLastName()); - user.setEmail(userDTO.getEmail()); - - User updatedUser = userRepository.save(user); - return mapper.apply(updatedUser); - } - - @Transactional - public void deleteUser(Long id) { - if (!userRepository.existsById(id)) { - throw new RuntimeException("User not found"); - } - userRepository.deleteById(id); - } - - @Transactional - public AddressDTO setDefaultAddress(Long userId, AddressDTO request) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); - - Address address = Address.builder() - .street(request.getStreet()) - .city(request.getCity()) - .state(request.getState()) - .zipCode(request.getZipCode()) - .country(request.getCountry()) - .build(); - - user.setDefaultAddress(address); - userRepository.save(user); - - return addressMapper.apply(address); - } - - @Transactional - public UserDTO removeDefaultAddress(Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); - user.setDefaultAddress(null); - return mapper.apply(userRepository.save(user)); - } - - @Transactional - public UserDTO addRoleToUser(Long userId, String role) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); - - if (user.getRoles() == null) { - user.setRoles(new ArrayList<>()); - } - - if (!user.getRoles().contains(role.toUpperCase())) { - user.getRoles().add(role.toUpperCase()); - userRepository.save(user); - } - - return mapper.apply(user); - } - - @Transactional - public UserDTO removeRoleFromUser(Long userId, String role) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); - - user.getRoles().remove(role.toUpperCase()); - User updatedUser = userRepository.save(user); - - return mapper.apply(updatedUser); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/service/email/EmailConfirmationService.java b/src/main/java/com/zenfulcode/commercify/service/email/EmailConfirmationService.java deleted file mode 100644 index c41e3bf..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/email/EmailConfirmationService.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.zenfulcode.commercify.service.email; - -import com.zenfulcode.commercify.domain.model.ConfirmationToken; -import com.zenfulcode.commercify.domain.model.User; -import com.zenfulcode.commercify.repository.ConfirmationTokenRepository; -import com.zenfulcode.commercify.repository.UserRepository; -import jakarta.mail.MessagingException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; - -@Service -@RequiredArgsConstructor -public class EmailConfirmationService { - private final ConfirmationTokenRepository tokenRepository; - private final UserRepository userRepository; - private final EmailService emailService; - - @Transactional - public void createConfirmationToken(User user) { - // Delete any existing unconfirmed tokens - tokenRepository.findByUserIdAndConfirmedFalse(user.getId()) - .ifPresent(tokenRepository::delete); - - // Create new token - ConfirmationToken token = ConfirmationToken.builder() - .user(user) - .confirmed(false) - .build(); - - ConfirmationToken savedToken = tokenRepository.save(token); - - try { - emailService.sendConfirmationEmail(user.getEmail(), savedToken.getToken()); - } catch (MessagingException e) { - throw new RuntimeException("Failed to send confirmation email", e); - } - } - - @Transactional - public boolean confirmEmail(String token) { - ConfirmationToken confirmationToken = tokenRepository.findByToken(token) - .orElseThrow(() -> new RuntimeException("Invalid confirmation token")); - - if (confirmationToken.isConfirmed()) { - throw new RuntimeException("Email already confirmed"); - } - - if (Instant.now().isAfter(confirmationToken.getExpiryDate())) { - throw new RuntimeException("Confirmation token expired"); - } - - confirmationToken.setConfirmed(true); - User user = confirmationToken.getUser(); - user.setEmailConfirmed(true); - - tokenRepository.save(confirmationToken); - userRepository.save(user); - - return true; - } - - @Transactional - public void resendConfirmationEmail(Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); - - if (user.isEnabled()) { - throw new RuntimeException("Email already confirmed"); - } - - createConfirmationToken(user); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/email/EmailService.java b/src/main/java/com/zenfulcode/commercify/service/email/EmailService.java deleted file mode 100644 index a46cb21..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/email/EmailService.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.zenfulcode.commercify.service.email; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import com.zenfulcode.commercify.service.core.UserManagementService; -import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.MimeMessageHelper; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.thymeleaf.TemplateEngine; -import org.thymeleaf.context.Context; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class EmailService { - private final JavaMailSender mailSender; - private final TemplateEngine templateEngine; - private final UserManagementService userService; - - @Value("${spring.mail.username}") - private String fromEmail; - - @Value("${app.frontend-url}") - private String frontendUrl; - - @Async - public void sendConfirmationEmail(String to, String token) throws MessagingException { - MimeMessage mimeMessage = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); - - Context context = new Context(); - context.setVariable("confirmationUrl", - frontendUrl + "/confirm-email?token=" + token); - - String htmlContent = templateEngine.process("confirmation-email", context); - - helper.setFrom(fromEmail); - helper.setTo(to); - helper.setSubject("Confirm your email address"); - helper.setText(htmlContent, true); - - mailSender.send(mimeMessage); - } - - @Async - public void sendOrderConfirmation(OrderDetailsDTO orderDetails) throws MessagingException { - OrderDTO order = orderDetails.getOrder(); - UserDTO user = userService.getUserById(order.getUserId()); - - Context context = new Context(); - context.setVariable("order", createOrderContext(orderDetails)); - - String template = "order-confirmation-email"; - String subject = String.format("Order Confirmation #%d - %s", - order.getId(), order.getOrderStatus()); - - sendTemplatedEmail(user.getEmail(), subject, template, context); - } - - @Async - public void sendOrderStatusUpdate(OrderDetailsDTO orderDetails) throws MessagingException { - OrderDTO order = orderDetails.getOrder(); - UserDTO user = userService.getUserById(order.getUserId()); - - Context context = new Context(); - context.setVariable("order", createOrderContext(orderDetails)); - - String template = "order-confirmation-email"; - String subject = String.format("Order #%d Status Update - %s", - order.getId(), order.getOrderStatus()); - - sendTemplatedEmail(user.getEmail(), subject, template, context); - } - - private void sendTemplatedEmail(String to, String subject, String template, Context context) - throws MessagingException { - MimeMessage mimeMessage = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); - - String htmlContent = templateEngine.process(template, context); - - helper.setFrom(fromEmail); - helper.setTo(to); - helper.setSubject(subject); - helper.setText(htmlContent, true); - - mailSender.send(mimeMessage); - } - - private Map createOrderContext(OrderDetailsDTO orderDetails) { - OrderDTO order = orderDetails.getOrder(); - Map orderContext = new HashMap<>(); - - orderContext.put("id", order.getId()); - orderContext.put("status", order.getOrderStatus()); - orderContext.put("createdAt", order.getCreatedAt()); - orderContext.put("currency", order.getCurrency()); - orderContext.put("totalAmount", order.getTotalAmount()); - - // Transform order lines into a format suitable for the template - List> items = orderDetails.getOrderLines().stream() - .map(line -> { - Map item = new HashMap<>(); - item.put("name", line.getProduct().getName()); - item.put("quantity", line.getQuantity()); - item.put("unitPrice", line.getUnitPrice()); - item.put("total", line.getQuantity() * line.getUnitPrice()); - - if (line.getVariant() != null) { - String variantDetails = line.getVariant().getOptions().stream() - .map(opt -> opt.getName() + ": " + opt.getValue()) - .collect(Collectors.joining(", ")); - item.put("variant", variantDetails); - } - - return item; - }) - .collect(Collectors.toList()); - - orderContext.put("items", items); - - if (order.getOrderStatus() == OrderStatus.SHIPPED) { - // Add tracking URL if available - orderContext.put("trackingUrl", frontendUrl + "/orders/" + order.getId() + "/tracking"); - } - - return orderContext; - } -} diff --git a/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayController.java b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayController.java deleted file mode 100644 index ea4ba02..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayController.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.zenfulcode.commercify.service.integration.mobilepay; - -import com.zenfulcode.commercify.web.dto.request.payment.PaymentRequest; -import com.zenfulcode.commercify.web.dto.response.payment.PaymentResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/v1/payments/mobilepay") -@RequiredArgsConstructor -@Slf4j -public class MobilePayController { - private final MobilePayService mobilePayService; - - @PostMapping("/create") - public ResponseEntity createPayment(@RequestBody PaymentRequest request) { - try { - PaymentResponse response = mobilePayService.initiatePayment(request); - return ResponseEntity.ok(response); - } catch (Exception e) { - return ResponseEntity.badRequest().body(PaymentResponse.FailedPayment()); - } - } - - @PostMapping("/callback") - public ResponseEntity handleCallback( - @RequestParam String paymentReference, - @RequestParam String status) { - try { - mobilePayService.handlePaymentCallback(paymentReference, status); - return ResponseEntity.ok("Callback processed successfully"); - } catch (Exception e) { - log.error("Error processing MobilePay callback", e); - return ResponseEntity.badRequest().body("Error processing callback"); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/webhook") - public ResponseEntity handleWebhook( - @RequestParam String paymentReference, - @RequestParam String status) { - try { - mobilePayService.handlePaymentCallback(paymentReference, status); - return ResponseEntity.ok("Callback processed successfully"); - } catch (Exception e) { - log.error("Error processing MobilePay callback", e); - return ResponseEntity.badRequest().body("Error processing callback"); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayService.java b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayService.java deleted file mode 100644 index edd791b..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayService.java +++ /dev/null @@ -1,212 +0,0 @@ -package com.zenfulcode.commercify.service.integration.mobilepay; - -import com.zenfulcode.commercify.domain.enums.PaymentProvider; -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import com.zenfulcode.commercify.web.dto.request.payment.PaymentRequest; -import com.zenfulcode.commercify.web.dto.response.payment.PaymentResponse; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.Payment; -import com.zenfulcode.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.exception.PaymentProcessingException; -import com.zenfulcode.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.service.core.PaymentService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; -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.springframework.web.client.RestTemplate; - -import java.util.*; - -@Service -@RequiredArgsConstructor -@Slf4j -public class MobilePayService { - private final PaymentService paymentService; - private final MobilePayTokenService tokenService; - - private final OrderRepository orderRepository; - private final PaymentRepository paymentRepository; - - private final RestTemplate restTemplate; - - @Value("${mobilepay.subscription-key}") - private String subscriptionKey; - - @Value("${mobilepay.merchant-id}") - private String merchantId; - - @Value("${mobilepay.system-name}") - private String systemName; - - @Value("${mobilepay.api-url}") - private String apiUrl; - - - @Transactional - public PaymentResponse initiatePayment(PaymentRequest request) { - try { - Order order = orderRepository.findById(request.orderId()) - .orElseThrow(() -> new OrderNotFoundException(request.orderId())); - - // Create MobilePay payment request - Map paymentRequest = createMobilePayRequest(order, request); - - // Call MobilePay API - MobilePayResponse mobilePayResponse = createMobilePayPayment(paymentRequest); - - // Create and save payment entity - Payment payment = Payment.builder() - .orderId(order.getId()) - .totalAmount(order.getTotalAmount()) - .paymentProvider(PaymentProvider.MOBILEPAY) - .status(PaymentStatus.PENDING) - .paymentMethod(request.paymentMethod()) // 'WALLET' or 'CARD' - .mobilePayReference(mobilePayResponse.reference()) - .build(); - - Payment savedPayment = paymentRepository.save(payment); - - return new PaymentResponse( - savedPayment.getId(), - savedPayment.getStatus(), - mobilePayResponse.redirectUrl() - ); - } catch (Exception e) { - log.error("Error creating MobilePay payment", e); - throw new PaymentProcessingException("Failed to create MobilePay payment", e); - } - } - - @Transactional - public void handlePaymentCallback(String paymentReference, String status) { - Payment payment = paymentRepository.findByMobilePayReference(paymentReference) - .orElseThrow(() -> new PaymentProcessingException("Payment not found", null)); - - PaymentStatus newStatus = mapMobilePayStatus(status); - - // Update payment status and trigger confirmation email if needed - paymentService.handlePaymentStatusUpdate(payment.getOrderId(), newStatus); - } - - private HttpHeaders mobilePayRequestHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - headers.set("Authorization", "Bearer " + tokenService.getAccessToken()); - headers.set("Ocp-Apim-Subscription-Key", subscriptionKey); - headers.set("Merchant-Serial-Number", merchantId); - headers.set("Vipps-System-Name", systemName); - headers.set("Vipps-System-Version", "1.0"); - headers.set("Vipps-System-Plugin-Name", "commercify"); - headers.set("Vipps-System-Plugin-Version", "1.0"); - headers.set("Idempotency-Key", UUID.randomUUID().toString()); - return headers; - } - - @Retryable( - notRecoverable = {PaymentProcessingException.class}, - backoff = @Backoff(delay = 1000) - ) - private MobilePayResponse createMobilePayPayment(Map request) { - HttpHeaders headers = mobilePayRequestHeaders(); - HttpEntity> entity = new HttpEntity<>(request, headers); - - try { - ResponseEntity response = restTemplate.exchange( - apiUrl + "/epayment/v1/payments", - HttpMethod.POST, - entity, - MobilePayResponse.class - ); - - if (response.getBody() == null) { - throw new PaymentProcessingException("No response from MobilePay API", null); - } - - return response.getBody(); - } catch (Exception e) { - log.error("Error creating MobilePay payment: {}", e.getMessage()); - throw new PaymentProcessingException("Failed to create MobilePay payment", e); - } - } - - private Map createMobilePayRequest(Order order, PaymentRequest request) { - validationPaymentRequest(request); - - Map paymentRequest = new HashMap<>(); - - // Amount object - Map amount = new HashMap<>(); - String value = String.valueOf(Math.round(order.getTotalAmount() * 100)); // Convert to minor units - amount.put("value", value); // Convert to minor units - amount.put("currency", "DKK"); - paymentRequest.put("amount", amount); - - // Payment method object - Map paymentMethod = new HashMap<>(); - paymentMethod.put("type", request.paymentMethod()); - paymentRequest.put("paymentMethod", paymentMethod); - - // Customer object - Map customer = new HashMap<>(); - customer.put("phoneNumber", request.phoneNumber()); - paymentRequest.put("customer", customer); - - // Other fields - String reference = systemName + "-order-" + order.getId().toString() + "-" + value; - paymentRequest.put("reference", reference); - paymentRequest.put("returnUrl", request.returnUrl() + "?orderId=" + order.getId()); - paymentRequest.put("userFlow", "WEB_REDIRECT"); - paymentRequest.put("paymentDescription", "Order #" + order.getId()); - - return paymentRequest; - } - - private void validationPaymentRequest(PaymentRequest request) { - List errors = new ArrayList<>(); - - if (!Objects.equals(request.currency(), "DKK")) { - errors.add("Currency must be DKK"); - } - - if (request.paymentMethod() == null || request.paymentMethod().isEmpty()) { - errors.add("Payment method is required"); - } - - if (!Objects.equals(request.paymentMethod(), "WALLET") && !Objects.equals(request.paymentMethod(), "CARD")) { - errors.add("Invalid payment method"); - } - - if (request.returnUrl() == null || request.returnUrl().isEmpty()) { - errors.add("Return URL is required"); - } - - if (!errors.isEmpty()) { - throw new PaymentProcessingException("Invalid payment request: " + String.join(", ", errors), null); - } - } - - private PaymentStatus mapMobilePayStatus(String status) { - return switch (status.toUpperCase()) { - case "CREATED" -> PaymentStatus.PENDING; - case "AUTHORIZED" -> PaymentStatus.PAID; - case "ABORTED" -> PaymentStatus.CANCELLED; - case "EXPIRED" -> PaymentStatus.EXPIRED; - case "TERMINATED" -> PaymentStatus.TERMINATED; - default -> throw new PaymentProcessingException("Unknown MobilePay status: " + status, null); - }; - } -} - -record MobilePayResponse( - String redirectUrl, - String reference -) { -} - diff --git a/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayTokenService.java b/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayTokenService.java deleted file mode 100644 index 6f7568b..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/integration/mobilepay/MobilePayTokenService.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.zenfulcode.commercify.service.integration.mobilepay; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.zenfulcode.commercify.exception.PaymentProcessingException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.time.Instant; -import java.util.concurrent.locks.ReentrantLock; - -@Service -@RequiredArgsConstructor -@Slf4j -public class MobilePayTokenService { - private final RestTemplate restTemplate; - private final ReentrantLock tokenLock = new ReentrantLock(); - - @Value("${mobilepay.client-id}") - private String clientId; - - @Value("${mobilepay.client-secret}") - private String apiKey; - - @Value("${mobilepay.subscription-key}") - private String subscriptionKey; - - @Value("${mobilepay.merchant-id}") - private String merchantId; - - @Value("${mobilepay.system-name}") - private String systemName; - - @Value("${mobilepay.api-url}") - private String apiUrl; - - private String accessToken; - private Instant tokenExpiration; - - public String getAccessToken() { - if (shouldRefreshToken()) { - refreshAccessToken(); - } - return accessToken; - } - - private boolean shouldRefreshToken() { - return accessToken == null || tokenExpiration == null || - Instant.now().plusSeconds(60).isAfter(tokenExpiration); - } - - private void refreshAccessToken() { - tokenLock.lock(); - try { - // Double-check after acquiring lock - if (shouldRefreshToken()) { - MobilePayTokenResponse tokenResponse = requestNewAccessToken(); - accessToken = tokenResponse.accessToken(); - - // Parse expires_on timestamp for token expiration - try { - long expiresOn = Long.parseLong(tokenResponse.expiresOn()); - tokenExpiration = Instant.ofEpochSecond(expiresOn); - log.info("Access token refreshed, expires at: {}", tokenExpiration); - } catch (NumberFormatException e) { - log.error("Failed to parse token expiration timestamp", e); - // Fallback to a conservative expiration time - tokenExpiration = Instant.now().plusSeconds(3600); - } - } - } finally { - tokenLock.unlock(); - } - } - - private MobilePayTokenResponse requestNewAccessToken() { - try { - HttpHeaders headers = createTokenRequestHeaders(); - - ResponseEntity response = restTemplate.exchange( - apiUrl + "/accesstoken/get", - HttpMethod.POST, - new HttpEntity<>(headers), - MobilePayTokenResponse.class - ); - - if (response.getBody() == null) { - throw new PaymentProcessingException("No response from MobilePay API", null); - } - - return response.getBody(); - } catch (Exception e) { - log.error("Failed to obtain access token", e); - throw new PaymentProcessingException("Failed to obtain access token", e); - } - } - - private HttpHeaders createTokenRequestHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("client_id", clientId); - headers.set("client_secret", apiKey); - headers.set("Ocp-Apim-Subscription-Key", subscriptionKey); - headers.set("Merchant-Serial-Number", merchantId); - headers.set("Vipps-System-Name", systemName); - headers.set("Vipps-System-Version", "1.0"); - headers.set("Vipps-System-Plugin-Name", "commercify"); - headers.set("Vipps-System-Plugin-Version", "1.0"); - return headers; - } - - // Refresh token periodically to ensure it's always valid - @Scheduled(fixedRate = 3600000) // Every hour - public void scheduleTokenRefresh() { - if (shouldRefreshToken()) { - refreshAccessToken(); - } - } -} - -record MobilePayTokenResponse( - @JsonProperty("token_type") - String tokenType, - - @JsonProperty("expires_in") - String expiresIn, - - @JsonProperty("ext_expires_in") - String extExpiresIn, - - @JsonProperty("access_token") - String accessToken, - - @JsonProperty("expires_on") - String expiresOn, - - @JsonProperty("not_before") - String notBefore, - - @JsonProperty("resource") - String resource -) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeConfig.java b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeConfig.java deleted file mode 100644 index 6dd8eeb..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.zenfulcode.commercify.service.integration.stripe; - -import com.stripe.Stripe; -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class StripeConfig { - - @Value("${stripe.secret-test-key}") - private String stripeApiKey; - - @Value("${stripe.webhook-secret}") - private String webhookSecret; - - @PostConstruct - public void init() { - Stripe.apiKey = stripeApiKey; - } - - @Bean - public String stripeWebhookSecret() { - return webhookSecret; - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeController.java b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeController.java deleted file mode 100644 index d1df910..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeController.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.zenfulcode.commercify.service.integration.stripe; - -import com.zenfulcode.commercify.web.dto.request.payment.PaymentRequest; -import com.zenfulcode.commercify.web.dto.response.payment.PaymentResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/v1/payments/stripe") -@RequiredArgsConstructor -public class StripeController { - private final StripeService stripeService; - private final StripeWebhookHandler webhookHandler; - - @PostMapping("/create") - public ResponseEntity createPayment(@RequestBody PaymentRequest request) { - try { - PaymentResponse response = stripeService.initiatePayment(request); - return ResponseEntity.ok(response); - } catch (Exception e) { - return ResponseEntity.badRequest().body(PaymentResponse.FailedPayment()); - } - } - - @PostMapping("/webhook") - public ResponseEntity handleWebhook( - @RequestBody String payload, - @RequestHeader("Stripe-Signature") String sigHeader) { - webhookHandler.handleWebhookEvent(payload, sigHeader); - return ResponseEntity.ok().build(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeService.java b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeService.java deleted file mode 100644 index dad0aab..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeService.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.zenfulcode.commercify.service.integration.stripe; - -import com.stripe.exception.StripeException; -import com.stripe.model.checkout.Session; -import com.stripe.param.checkout.SessionCreateParams; -import com.zenfulcode.commercify.domain.enums.PaymentProvider; -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import com.zenfulcode.commercify.web.dto.request.payment.PaymentRequest; -import com.zenfulcode.commercify.web.dto.response.payment.PaymentResponse; -import com.zenfulcode.commercify.web.dto.common.ProductDTO; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.OrderLine; -import com.zenfulcode.commercify.domain.model.Payment; -import com.zenfulcode.commercify.domain.model.VariantOption; -import com.zenfulcode.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.exception.PaymentProcessingException; -import com.zenfulcode.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.service.core.ProductService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.List; - -@Service -@RequiredArgsConstructor -@Slf4j -public class StripeService { - private final OrderRepository orderRepository; - private final PaymentRepository paymentRepository; - private final ProductService productService; - - @Transactional - public PaymentResponse initiatePayment(PaymentRequest request) { - try { - Order order = orderRepository.findById(request.orderId()) - .orElseThrow(() -> new OrderNotFoundException(request.orderId())); - - Session session = createCheckoutSession(order, request); - - Payment payment = Payment.builder() - .orderId(order.getId()) - .totalAmount(order.getTotalAmount()) - .paymentProvider(PaymentProvider.STRIPE) - .status(PaymentStatus.PENDING) - .paymentMethod(request.paymentMethod()) - .build(); - - Payment savedPayment = paymentRepository.save(payment); - - return new PaymentResponse( - savedPayment.getId(), - savedPayment.getStatus(), - session.getUrl() - ); - } catch (Exception e) { - log.error("Error creating Stripe checkout session", e); - throw new PaymentProcessingException("Failed to create Stripe checkout session", e); - } - } - - private Session createCheckoutSession(Order order, PaymentRequest request) throws StripeException { - List lineItems = new ArrayList<>(); - - for (OrderLine line : order.getOrderLines()) { - ProductDTO product = productService.getProductById(line.getProductId()); - SessionCreateParams.LineItem.PriceData.ProductData.Builder productDataBuilder = SessionCreateParams.LineItem.PriceData.ProductData.builder() - .setName(product.getName()) - .setDescription(product.getDescription()) - .addImage(product.getImageUrl()); - - if (line.getProductVariant() != null) { - StringBuilder variantInfo = new StringBuilder(); - for (VariantOption option : line.getProductVariant().getOptions()) { - variantInfo.append(option.getName()) - .append(": ") - .append(option.getValue()) - .append(", "); - } - if (!variantInfo.isEmpty()) { - variantInfo.setLength(variantInfo.length() - 2); - productDataBuilder.putMetadata("variant", variantInfo.toString()); - } - } - - lineItems.add(SessionCreateParams.LineItem.builder() - .setPriceData(SessionCreateParams.LineItem.PriceData.builder() - .setCurrency(order.getCurrency().toLowerCase()) - .setUnitAmount((long) (line.getUnitPrice() * 100)) - .setProductData(productDataBuilder.build()) - .build()) - .setQuantity((long) line.getQuantity()) - .build()); - } - - SessionCreateParams params = SessionCreateParams.builder() - .setMode(SessionCreateParams.Mode.PAYMENT) - .setSuccessUrl(request.returnUrl() + "?success=true&orderId=" + order.getId()) - .setCancelUrl(request.returnUrl() + "?success=false&orderId=" + order.getId()) - .putMetadata("orderId", order.getId().toString()) - .addAllLineItem(lineItems) - .build(); - - return Session.create(params); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeWebhookHandler.java b/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeWebhookHandler.java deleted file mode 100644 index 6fe6397..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/integration/stripe/StripeWebhookHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.zenfulcode.commercify.service.integration.stripe; - -import com.stripe.exception.SignatureVerificationException; -import com.stripe.model.Event; -import com.stripe.model.checkout.Session; -import com.stripe.net.Webhook; -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.Payment; -import com.zenfulcode.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.exception.PaymentProcessingException; -import com.zenfulcode.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.repository.PaymentRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Slf4j -public class StripeWebhookHandler { - private final PaymentRepository paymentRepository; - private final OrderRepository orderRepository; - private final String stripeWebhookSecret; - - @Transactional - public void handleWebhookEvent(String payload, String sigHeader) { - Event event; - try { - event = Webhook.constructEvent(payload, sigHeader, stripeWebhookSecret); - } catch (SignatureVerificationException e) { - throw new PaymentProcessingException("Invalid signature", e); - } - - if ("checkout.session.completed".equals(event.getType())) { - Session session = (Session) event.getDataObjectDeserializer().getObject().get(); - handleSuccessfulPayment(session); - } - } - - private void handleSuccessfulPayment(Session session) { - String orderId = session.getMetadata().get("orderId"); - if (orderId == null) { - log.error("No order ID in session metadata"); - return; - } - - Order order = orderRepository.findById(Long.parseLong(orderId)) - .orElseThrow(() -> new OrderNotFoundException(Long.parseLong(orderId))); - - // Update order status - order.setStatus(OrderStatus.CONFIRMED); - orderRepository.save(order); - - // Update payment status - Payment payment = paymentRepository.findByOrderId(order.getId()) - .orElseThrow(() -> new RuntimeException("Payment not found for order: " + orderId)); - payment.setStatus(PaymentStatus.PAID); - paymentRepository.save(payment); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/validations/OrderValidationService.java b/src/main/java/com/zenfulcode/commercify/service/validations/OrderValidationService.java deleted file mode 100644 index 0229280..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/validations/OrderValidationService.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.zenfulcode.commercify.service.validations; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.web.dto.request.order.CreateOrderRequest; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.Product; -import com.zenfulcode.commercify.exception.InsufficientStockException; -import com.zenfulcode.commercify.exception.OrderValidationException; -import com.zenfulcode.commercify.component.flow.OrderStateFlow; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - -@Service -@AllArgsConstructor -public class OrderValidationService { - private OrderStateFlow orderStateFlow; - - public void validateCreateOrderRequest(CreateOrderRequest request) { - List errors = new ArrayList<>(); - - if (request.orderLines() == null || request.orderLines().isEmpty()) { - errors.add("Order must contain at least one item"); - } else { - request.orderLines().forEach(line -> { - if (line.quantity() <= 0) { - errors.add("Quantity must be greater than 0 for product: " + line.productId()); - } - }); - } - if (request.currency() == null || request.currency().isBlank()) { - errors.add("Currency is required"); - } - - if (request.shippingAddress() == null) { - errors.add("Shipping address is required"); - } - - if (request.billingAddress() != null) { - if (request.billingAddress().getStreet() == null || request.billingAddress().getStreet().isBlank()) { - errors.add("Billing address street is required"); - } - if (request.billingAddress().getCity() == null || request.billingAddress().getCity().isBlank()) { - errors.add("Billing address city is required"); - } - - if (request.billingAddress().getZipCode() == null || request.billingAddress().getZipCode().isBlank()) { - errors.add("Billing address zip code is required"); - } - if (request.billingAddress().getCountry() == null || request.billingAddress().getCountry().isBlank()) { - errors.add("Billing address country is required"); - } - } - - if (request.customerDetails() == null) { - errors.add("Customer details are required"); - } - - if (request.customerDetails() != null) { - if (request.customerDetails().getFirstName() == null || request.customerDetails().getFirstName().isBlank()) { - errors.add("Customer first name is required"); - } - if (request.customerDetails().getLastName() == null || request.customerDetails().getLastName().isBlank()) { - errors.add("Customer last name is required"); - } - if (request.customerDetails().getEmail() == null || request.customerDetails().getEmail().isBlank()) { - errors.add("Customer email is required"); - } - } - - if (!errors.isEmpty()) { - throw new OrderValidationException("Order validation failed: " + String.join(", ", errors)); - } - } - - public void validateStatusTransition(OrderStatus currentStatus, OrderStatus newStatus) { - if (!orderStateFlow.canTransition(currentStatus, newStatus)) { - throw new IllegalStateException( - String.format("Invalid status transition from %s to %s", currentStatus, newStatus) - ); - } - } - - public void validateOrderCancellation(Order order) { - if (orderStateFlow.canTransition(order.getStatus(), OrderStatus.CANCELLED)) { - throw new IllegalStateException( - String.format("Cannot cancel order in status: %s", order.getStatus()) - ); - } - } - - public void validateStockAvailability(Product product, int requestedQuantity) { - if (product.getStock() < requestedQuantity) { - throw new InsufficientStockException( - String.format("Insufficient stock for product %d. Available: %d, Requested: %d", - product.getId(), product.getStock(), requestedQuantity) - ); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/service/validations/ProductValidationService.java b/src/main/java/com/zenfulcode/commercify/service/validations/ProductValidationService.java deleted file mode 100644 index 06b9628..0000000 --- a/src/main/java/com/zenfulcode/commercify/service/validations/ProductValidationService.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.zenfulcode.commercify.service.validations; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; -import com.zenfulcode.commercify.web.dto.request.product.ProductVariantRequest; -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.ProductVariant; -import com.zenfulcode.commercify.exception.ProductDeletionException; -import com.zenfulcode.commercify.exception.ProductValidationException; -import com.zenfulcode.commercify.repository.OrderLineRepository; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -@Service -@AllArgsConstructor -public class ProductValidationService { - private final OrderLineRepository orderLineRepository; - private final OrderMapper orderMapper; - - public void validateProductRequest(ProductRequest request) { - List errors = new ArrayList<>(); - - if (request.name() == null || request.name().isBlank()) { - errors.add("Product name is required"); - } - if (request.price() == null || request.price().amount() == null || request.price().amount() < 0) { - errors.add("Valid unitPrice is required"); - } - if (request.stock() != null && request.stock() < 0) { - errors.add("Stock cannot be negative"); - } - - if (!errors.isEmpty()) { - throw new ProductValidationException(errors); - } - } - - public void validateVariantRequest(ProductVariantRequest request) { - List errors = new ArrayList<>(); - - if (request.sku() == null || request.sku().isBlank()) { - errors.add("SKU is required"); - } - if (request.options() == null || request.options().isEmpty()) { - errors.add("At least one variant option is required"); - } - - if (!errors.isEmpty()) { - throw new ProductValidationException(errors); - } - } - - public void validateVariantDeletion(ProductVariant variant) { - Set activeOrders = orderLineRepository.findActiveOrdersForVariant( - variant.getId(), - List.of(OrderStatus.PENDING, OrderStatus.CONFIRMED, OrderStatus.SHIPPED) - ); - - if (!activeOrders.isEmpty()) { - List activeOrderDTOs = activeOrders.stream() - .map(orderMapper) - .toList(); - - throw new ProductDeletionException( - "Cannot delete variant with active orders", - List.of("Variant has active orders"), - activeOrderDTOs - ); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java new file mode 100644 index 0000000..ba960fd --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java @@ -0,0 +1,21 @@ +package com.zenfulcode.commercify.shared.domain.event; + +import lombok.Getter; + +import java.time.Instant; +import java.util.UUID; + +@Getter +public abstract class DomainEvent { + private final String eventId; + private final Instant occurredOn; + private final String eventType; + private final int version; + + protected DomainEvent() { + this.eventId = UUID.randomUUID().toString(); + this.occurredOn = Instant.now(); + this.eventType = this.getClass().getSimpleName(); + this.version = 1; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java new file mode 100644 index 0000000..9210ad5 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.shared.domain.event; + +public interface DomainEventHandler { + void handle(T event); + boolean canHandle(DomainEvent event); +} + diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventPublisher.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventPublisher.java new file mode 100644 index 0000000..0473d52 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventPublisher.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.shared.domain.event; + +import java.util.List; + +public interface DomainEventPublisher { + void publish(DomainEvent event); + void publish(List events); +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventStore.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventStore.java new file mode 100644 index 0000000..6633828 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventStore.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.shared.domain.event; + +import java.util.List; + +public interface DomainEventStore { + void store(DomainEvent event); + List getEvents(String aggregateId, String aggregateType); +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainException.java new file mode 100644 index 0000000..693b9f9 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainException.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +public abstract class DomainException extends RuntimeException { + + public DomainException(String message) { + super(message); + } + + public DomainException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainInvariantViolationException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainInvariantViolationException.java new file mode 100644 index 0000000..5b57998 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainInvariantViolationException.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +import lombok.Getter; + +@Getter +public class DomainInvariantViolationException extends DomainException { + private final String invariantName; + + public DomainInvariantViolationException(String invariantName, String message) { + super(message); + this.invariantName = invariantName; + } + +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainValidationException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainValidationException.java new file mode 100644 index 0000000..a34be16 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainValidationException.java @@ -0,0 +1,17 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +import lombok.Getter; + +import java.util.Collections; +import java.util.List; + +@Getter +public abstract class DomainValidationException extends DomainException { + private final List violations; + + public DomainValidationException(String message, List violations) { + super(message); + this.violations = Collections.unmodifiableList(violations); + } + +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EntityNotFoundException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EntityNotFoundException.java new file mode 100644 index 0000000..3740431 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EntityNotFoundException.java @@ -0,0 +1,16 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +import lombok.Getter; + +@Getter +public class EntityNotFoundException extends DomainException { + private final String entityType; + private final Object entityId; + + public EntityNotFoundException(String entityType, Object entityId) { + super(String.format("%s with id %s not found", entityType, entityId)); + this.entityType = entityType; + this.entityId = entityId; + } + +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EventDeserializationException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EventDeserializationException.java new file mode 100644 index 0000000..b4855b4 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EventDeserializationException.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +public class EventDeserializationException extends DomainException { + public EventDeserializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EventSerializationException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EventSerializationException.java new file mode 100644 index 0000000..6ebb843 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EventSerializationException.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +public class EventSerializationException extends DomainException { + public EventSerializationException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java b/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java new file mode 100644 index 0000000..f9bba13 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java @@ -0,0 +1,25 @@ +package com.zenfulcode.commercify.shared.domain.model; + +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import org.springframework.data.annotation.Transient; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class AggregateRoot { + @Transient + private final List domainEvents = new ArrayList<>(); + + protected void registerEvent(DomainEvent event) { + domainEvents.add(event); + } + + public List getDomainEvents() { + return Collections.unmodifiableList(domainEvents); + } + + public void clearDomainEvents() { + domainEvents.clear(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/model/Money.java b/src/main/java/com/zenfulcode/commercify/shared/domain/model/Money.java new file mode 100644 index 0000000..f2a5e3d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/model/Money.java @@ -0,0 +1,45 @@ +package com.zenfulcode.commercify.shared.domain.model; + +import jakarta.persistence.Embeddable; +import lombok.Value; +import org.apache.commons.lang3.NotImplementedException; + +import java.math.BigDecimal; + +@Embeddable +@Value +public class Money { + BigDecimal amount; + String currency; + + public Money(BigDecimal amount, String currency) { + this.amount = amount; + this.currency = currency; + } + + public Money(double amount, String currency) { + this.amount = BigDecimal.valueOf(amount); + this.currency = currency; + } + + public Money() { + this.amount = new BigDecimal(0); + this.currency = "USD"; + } + + public Money add(Money other) { + if (!this.currency.equals(other.currency)) { + throw new IllegalArgumentException("Cannot add different currencies"); + } + return new Money(this.amount.add(other.amount), this.currency); + } + + public Money multiply(int quantity) { + return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency); + } + + public boolean isNegative() { + throw new NotImplementedException("is negative has not been implemented"); + } + +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java b/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java new file mode 100644 index 0000000..a85a418 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java @@ -0,0 +1,42 @@ +package com.zenfulcode.commercify.shared.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Entity +@Table(name = "domain_events") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class StoredEvent { + @Id + private String eventId; + + @Column(nullable = false) + private String eventType; + + @Column(nullable = false, columnDefinition = "TEXT") + private String eventData; + + @Column(nullable = false) + private Instant occurredOn; + + @Column + private String aggregateId; + + @Column + private String aggregateType; + + public StoredEvent(String eventId, String eventType, String eventData, Instant occurredOn) { + this.eventId = eventId; + this.eventType = eventType; + this.eventData = eventData; + this.occurredOn = occurredOn; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java b/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java new file mode 100644 index 0000000..362e307 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java @@ -0,0 +1,44 @@ +package com.zenfulcode.commercify.shared.domain.service; + +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.event.DomainEventHandler; +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.shared.domain.event.DomainEventStore; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class DefaultDomainEventPublisher implements DomainEventPublisher { + private final ApplicationEventPublisher applicationEventPublisher; + @Qualifier("enhancedEventStore") + private final DomainEventStore eventStore; + private final List> eventHandlers; + + @Override + public void publish(DomainEvent event) { + // Store event + eventStore.store(event); + + // Publish to Spring's event system + applicationEventPublisher.publishEvent(event); + + // Handle event + handleEvent(event); + } + + @Override + public void publish(List events) { + events.forEach(this::publish); + } + + private void handleEvent(DomainEvent event) { + eventHandlers.stream() + .filter(handler -> handler.canHandle(event)) + .forEach(handler -> handler.handle(event)); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventSerializer.java b/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventSerializer.java new file mode 100644 index 0000000..7648318 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventSerializer.java @@ -0,0 +1,58 @@ +package com.zenfulcode.commercify.shared.domain.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.exception.EventDeserializationException; +import com.zenfulcode.commercify.shared.domain.exception.EventSerializationException; +import com.zenfulcode.commercify.shared.domain.model.StoredEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EventSerializer { + private final ObjectMapper objectMapper; + private final EventTypeResolver eventTypeResolver; + + public StoredEvent serialize(DomainEvent event) { + try { + String eventData = objectMapper.writeValueAsString(event); + return new StoredEvent( + event.getEventId(), + event.getClass().getName(), + eventData, + event.getOccurredOn(), + getAggregateId(event), + getAggregateType(event) + ); + } catch (JsonProcessingException e) { + throw new EventSerializationException("Failed to serialize event", e); + } + } + + public DomainEvent deserialize(StoredEvent storedEvent) { + try { + Class eventClass = eventTypeResolver.resolveEventClass(storedEvent.getEventType()); + return (DomainEvent) objectMapper.readValue( + storedEvent.getEventData(), + eventClass + ); + } catch (Exception e) { + throw new EventDeserializationException( + "Failed to deserialize event: " + storedEvent.getEventType(), + e + ); + } + } + + private String getAggregateId(DomainEvent event) { + // Use reflection or annotation to get aggregate ID + return AggregateReference.extractId(event); + } + + private String getAggregateType(DomainEvent event) { + // Use reflection or annotation to get aggregate type + return AggregateReference.extractType(event); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventTypeResolver.java b/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventTypeResolver.java new file mode 100644 index 0000000..c1b91c1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventTypeResolver.java @@ -0,0 +1,25 @@ +package com.zenfulcode.commercify.shared.domain.service; + +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class EventTypeResolver { + private final Map> eventTypeMap = new ConcurrentHashMap<>(); + + public Class resolveEventClass(String eventType) throws ClassNotFoundException { + return eventTypeMap.computeIfAbsent(eventType, this::loadEventClass); + } + + @SuppressWarnings("unchecked") + private Class loadEventClass(String eventType) throws ClassNotFoundException { + Class loadedClass = Class.forName(eventType); + if (!DomainEvent.class.isAssignableFrom(loadedClass)) { + throw new IllegalArgumentException("Invalid event type: " + eventType); + } + return (Class) loadedClass; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EnhancedEventStore.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EnhancedEventStore.java new file mode 100644 index 0000000..f3783ad --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EnhancedEventStore.java @@ -0,0 +1,60 @@ +package com.zenfulcode.commercify.shared.infrastructure.persistence; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.event.DomainEventStore; +import com.zenfulcode.commercify.shared.domain.model.StoredEvent; +import com.zenfulcode.commercify.shared.domain.service.EventSerializer; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class EnhancedEventStore implements DomainEventStore { + private final EventStoreRepository repository; + private final ObjectMapper objectMapper; + private final EventSerializer eventSerializer; + + @Override + public void store(DomainEvent event) { + StoredEvent storedEvent = eventSerializer.serialize(event); + repository.save(storedEvent); + } + + @Override + public List getEvents(String aggregateId, String aggregateType) { + return repository.findByAggregateIdAndAggregateType(aggregateId, aggregateType) + .stream() + .map(eventSerializer::deserialize) + .collect(Collectors.toList()); + } + + public List getEventsSince(Instant since) { + return repository.findEventsSince(since) + .stream() + .map(eventSerializer::deserialize) + .collect(Collectors.toList()); + } + + public List getEventsByType(Class eventType) { + return repository.findByEventType(eventType.getName()) + .stream() + .map(event -> (T) eventSerializer.deserialize(event)) + .collect(Collectors.toList()); + } + + public Page getEventsByAggregateType(String aggregateType, Pageable pageable) { + return repository.findByAggregateType(aggregateType, pageable) + .map(eventSerializer::deserialize); + } + + public boolean hasEventOccurred(String aggregateId, String aggregateType, String eventType) { + return repository.hasEventType(aggregateId, aggregateType, eventType); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EventStoreRepository.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EventStoreRepository.java new file mode 100644 index 0000000..b8988c1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EventStoreRepository.java @@ -0,0 +1,55 @@ +package com.zenfulcode.commercify.shared.infrastructure.persistence; + +import com.zenfulcode.commercify.shared.domain.model.StoredEvent; +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; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.Instant; +import java.util.List; + +@Repository +public interface EventStoreRepository extends JpaRepository { + + List findByAggregateIdAndAggregateType(String aggregateId, String aggregateType); + + @Query("SELECT e FROM StoredEvent e WHERE e.occurredOn >= :since ORDER BY e.occurredOn ASC") + List findEventsSince(@Param("since") Instant since); + + @Query("SELECT e FROM StoredEvent e WHERE e.eventType = :eventType ORDER BY e.occurredOn ASC") + List findByEventType(@Param("eventType") String eventType); + + @Query("SELECT e FROM StoredEvent e WHERE e.aggregateType = :aggregateType ORDER BY e.occurredOn ASC") + Page findByAggregateType( + @Param("aggregateType") String aggregateType, + Pageable pageable + ); + + @Query(""" + SELECT e FROM StoredEvent e + WHERE e.aggregateId = :aggregateId + AND e.aggregateType = :aggregateType + AND e.occurredOn >= :since + ORDER BY e.occurredOn ASC + """) + List findByAggregateIdAndTypeSince( + @Param("aggregateId") String aggregateId, + @Param("aggregateType") String aggregateType, + @Param("since") Instant since + ); + + @Query(""" + SELECT COUNT(e) > 0 FROM StoredEvent e + WHERE e.aggregateId = :aggregateId + AND e.aggregateType = :aggregateType + AND e.eventType = :eventType + """) + boolean hasEventType( + @Param("aggregateId") String aggregateId, + @Param("aggregateType") String aggregateType, + @Param("eventType") String eventType + ); +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java new file mode 100644 index 0000000..ed457e5 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java @@ -0,0 +1,60 @@ +package com.zenfulcode.commercify.shared.infrastructure.persistence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.event.DomainEventStore; +import com.zenfulcode.commercify.shared.domain.exception.EventDeserializationException; +import com.zenfulcode.commercify.shared.domain.exception.EventSerializationException; +import com.zenfulcode.commercify.shared.domain.model.StoredEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class JpaDomainEventStore implements DomainEventStore { + private final EventStoreRepository repository; + private final ObjectMapper objectMapper; + + @Override + public void store(DomainEvent event) { + StoredEvent storedEvent = new StoredEvent( + event.getEventId(), + event.getEventType(), + serializeEvent(event), + event.getOccurredOn() + ); + repository.save(storedEvent); + } + + @Override + public List getEvents(String aggregateId, String aggregateType) { + return repository.findByAggregateIdAndAggregateType(aggregateId, aggregateType) + .stream() + .map(this::deserializeEvent) + .collect(Collectors.toList()); + } + + private String serializeEvent(DomainEvent event) { + try { + return objectMapper.writeValueAsString(event); + } catch (JsonProcessingException e) { + throw new EventSerializationException("Failed to serialize event", e); + } + } + + private DomainEvent deserializeEvent(StoredEvent storedEvent) { + try { + Class eventClass = Class.forName(storedEvent.getEventType()); + return (DomainEvent) objectMapper.readValue( + storedEvent.getEventData(), + eventClass + ); + } catch (Exception e) { + throw new EventDeserializationException("Failed to deserialize event", e); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/ProductIdConverter.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/ProductIdConverter.java new file mode 100644 index 0000000..dfe292f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/ProductIdConverter.java @@ -0,0 +1,19 @@ +package com.zenfulcode.commercify.shared.infrastructure.persistence; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class ProductIdConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ProductId productId) { + return productId == null ? null : productId.getValue(); + } + + @Override + public ProductId convertToEntityAttribute(String dbData) { + return dbData == null ? null : ProductId.of(dbData); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/interfaces/ApiResponse.java b/src/main/java/com/zenfulcode/commercify/shared/interfaces/ApiResponse.java new file mode 100644 index 0000000..ae93a1a --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/interfaces/ApiResponse.java @@ -0,0 +1,148 @@ +package com.zenfulcode.commercify.shared.interfaces; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; + +import java.time.Instant; +import java.util.List; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + private final String status; + private final T data; + private final ErrorInfo error; + private final MetaInfo meta; + private final Instant timestamp; + + private ApiResponse(String status, T data, ErrorInfo error, MetaInfo meta) { + this.status = status; + this.data = data; + this.error = error; + this.meta = meta; + this.timestamp = Instant.now(); + } + + // Success response with data + public static ApiResponse success(T data) { + return new ApiResponse<>("success", data, null, null); + } + + // Success response with data and metadata + public static ApiResponse success(T data, MetaInfo meta) { + return new ApiResponse<>("success", data, null, meta); + } + + // Error response + public static ApiResponse error(ErrorInfo error) { + return new ApiResponse<>("error", null, error, null); + } + + // Error response with http status + public static ApiResponse error(String message, String code, int httpStatus) { + ErrorInfo error = new ErrorInfo(message, code, httpStatus); + return new ApiResponse<>("error", null, error, null); + } + + // Validation error response + public static ApiResponse validationError(List validationErrors) { + ErrorInfo error = new ErrorInfo( + "Validation failed", + "VALIDATION_ERROR", + 400, + validationErrors + ); + return new ApiResponse<>("error", null, error, null); + } + + @Getter + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class ErrorInfo { + private final String message; + private final String code; + private final int httpStatus; + private final List validationErrors; + + public ErrorInfo(String message, String code, int httpStatus) { + this(message, code, httpStatus, null); + } + + public ErrorInfo(String message, String code, int httpStatus, + List validationErrors) { + this.message = message; + this.code = code; + this.httpStatus = httpStatus; + this.validationErrors = validationErrors; + } + } + + public record ValidationError(String field, String message) { + } + + @Getter + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class MetaInfo { + private final Integer page; + private final Integer size; + private final Long totalElements; + private final Integer totalPages; + private final String nextPage; + private final String previousPage; + + private MetaInfo(Builder builder) { + this.page = builder.page; + this.size = builder.size; + this.totalElements = builder.totalElements; + this.totalPages = builder.totalPages; + this.nextPage = builder.nextPage; + this.previousPage = builder.previousPage; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Integer page; + private Integer size; + private Long totalElements; + private Integer totalPages; + private String nextPage; + private String previousPage; + + public Builder page(Integer page) { + this.page = page; + return this; + } + + public Builder size(Integer size) { + this.size = size; + return this; + } + + public Builder totalElements(Long totalElements) { + this.totalElements = totalElements; + return this; + } + + public Builder totalPages(Integer totalPages) { + this.totalPages = totalPages; + return this; + } + + public Builder nextPage(String nextPage) { + this.nextPage = nextPage; + return this; + } + + public Builder previousPage(String previousPage) { + this.previousPage = previousPage; + return this; + } + + public MetaInfo build() { + return new MetaInfo(this); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java b/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..4ce1d45 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java @@ -0,0 +1,53 @@ +package com.zenfulcode.commercify.shared.interfaces.rest.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import com.zenfulcode.commercify.shared.domain.exception.EntityNotFoundException; +import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.List; +import java.util.stream.Collectors; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(DomainException.class) + public ResponseEntity> handleDomainException(DomainException ex) { + ApiResponse response = ApiResponse.error( + ex.getMessage(), + "DOMAIN_ERROR", + 400 + ); + return ResponseEntity.badRequest().body(response); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException( + MethodArgumentNotValidException ex) { + List validationErrors = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(error -> new ApiResponse.ValidationError( + error.getField(), + error.getDefaultMessage() + )) + .collect(Collectors.toList()); + + ApiResponse response = ApiResponse.validationError(validationErrors); + return ResponseEntity.badRequest().body(response); + } + + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity> handleNotFoundException( + EntityNotFoundException ex) { + ApiResponse response = ApiResponse.error( + ex.getMessage(), + "NOT_FOUND", + 404 + ); + return ResponseEntity.status(404).body(response); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/web/controller/AuthenticationController.java b/src/main/java/com/zenfulcode/commercify/web/controller/AuthenticationController.java deleted file mode 100644 index 17d1651..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/controller/AuthenticationController.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.zenfulcode.commercify.web.controller; - - -import com.zenfulcode.commercify.web.dto.request.auth.LoginUserRequest; -import com.zenfulcode.commercify.web.dto.request.auth.RegisterUserRequest; -import com.zenfulcode.commercify.web.dto.response.auth.AuthResponse; -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import com.zenfulcode.commercify.service.authentication.AuthenticationService; -import com.zenfulcode.commercify.service.authentication.JwtService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -@Slf4j -@RestController -@RequestMapping("/api/v1/auth") -@RequiredArgsConstructor -public class AuthenticationController { - private final JwtService jwtService; - private final AuthenticationService authenticationService; - - @PostMapping("/signup") - public ResponseEntity register(@RequestBody RegisterUserRequest registerRequest) { - try { - UserDTO user = authenticationService.registerUser(registerRequest); - - if (registerRequest.isGuest()) { - UserDTO authenticated = authenticationService.authenticate(new LoginUserRequest(registerRequest.email(), registerRequest.password())); - String jwt = jwtService.generateToken(authenticated); - return ResponseEntity.ok(AuthResponse.UserAuthenticated(authenticated, jwt, jwtService.getExpirationTime())); - } - - return ResponseEntity.ok(AuthResponse.UserAuthenticated(user, "", 0)); - } catch (RuntimeException e) { - log.error("Error registering user: {}", e.getMessage()); - return ResponseEntity.badRequest().body(AuthResponse.AuthenticationFailed(e.getMessage())); - } - } - - @PostMapping("/signin") - public ResponseEntity login(@RequestBody LoginUserRequest loginRequest) { - try { - UserDTO authenticatedUser = authenticationService.authenticate(loginRequest); - String jwtToken = jwtService.generateToken(authenticatedUser); - return ResponseEntity.ok(AuthResponse.UserAuthenticated(authenticatedUser, jwtToken, jwtService.getExpirationTime())); - } catch (Exception e) { - return ResponseEntity.badRequest().body(AuthResponse.AuthenticationFailed(e.getMessage())); - } - } - - @GetMapping("/me") - @PreAuthorize("hasRole('USER')") - public ResponseEntity getAuthenticatedUser(@RequestHeader("Authorization") String authHeader) { - return ResponseEntity.ok(authenticationService.getAuthenticatedUser(authHeader)); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/controller/OrderController.java b/src/main/java/com/zenfulcode/commercify/web/controller/OrderController.java deleted file mode 100644 index df3c0b7..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/controller/OrderController.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.zenfulcode.commercify.web.controller; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.exception.*; -import com.zenfulcode.commercify.service.core.OrderService; -import com.zenfulcode.commercify.web.viewmodel.OrderViewModel; -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; -import com.zenfulcode.commercify.web.dto.request.order.CreateOrderRequest; -import com.zenfulcode.commercify.web.dto.request.order.OrderStatusUpdateRequest; -import com.zenfulcode.commercify.web.dto.response.ErrorResponse; -import com.zenfulcode.commercify.web.dto.response.order.CreateOrderResponse; -import com.zenfulcode.commercify.web.dto.response.order.GetOrderResponse; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PagedResourcesAssembler; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.Set; - -@RestController -@RequestMapping("/api/v1/orders") -@AllArgsConstructor -@Slf4j -public class OrderController { - - private final OrderService orderService; - private final PagedResourcesAssembler pagedResourcesAssembler; - - private static final Set VALID_SORT_FIELDS = Set.of( - "id", "userId", "status", "currency", "totalAmount", "createdAt", "updatedAt" - ); - - @PreAuthorize("hasRole('USER') and #userId == authentication.principal.id") - @PostMapping("/{userId}") - public ResponseEntity createOrder(@PathVariable Long userId, @RequestBody CreateOrderRequest orderRequest) { - try { - OrderDTO orderDTO = orderService.createOrder(userId, orderRequest); - return ResponseEntity.ok(CreateOrderResponse.from(OrderViewModel.fromDTO(orderDTO))); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest() - .body(CreateOrderResponse.from("Invalid request: " + e.getMessage())); - } catch (ProductNotFoundException e) { - return ResponseEntity.badRequest() - .body(CreateOrderResponse.from("Product not found: " + e.getMessage())); - } catch (InsufficientStockException e) { - return ResponseEntity.badRequest() - .body(CreateOrderResponse.from("Insufficient stock: " + e.getMessage())); - } catch (OrderValidationException e) { - return ResponseEntity.badRequest() - .body(CreateOrderResponse.from(e.getMessage())); - } catch (Exception e) { - log.error("Error creating order", e); - return ResponseEntity.internalServerError() - .body(CreateOrderResponse.from("Error creating order: " + e.getMessage())); - } - } - - @PostMapping - public ResponseEntity createOrder(@RequestBody CreateOrderRequest orderRequest) { - try { - OrderDTO orderDTO = orderService.createOrder(orderRequest); - return ResponseEntity.ok(CreateOrderResponse.from(OrderViewModel.fromDTO(orderDTO))); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest() - .body(CreateOrderResponse.from("Invalid request: " + e.getMessage())); - } catch (ProductNotFoundException e) { - return ResponseEntity.badRequest() - .body(CreateOrderResponse.from("Product not found: " + e.getMessage())); - } catch (InsufficientStockException e) { - return ResponseEntity.badRequest() - .body(CreateOrderResponse.from("Insufficient stock: " + e.getMessage())); - } catch (OrderValidationException e) { - return ResponseEntity.badRequest() - .body(CreateOrderResponse.from(e.getMessage())); - } catch (Exception e) { - log.error("Error creating order", e); - return ResponseEntity.internalServerError() - .body(CreateOrderResponse.from("Error creating order: " + e.getMessage())); - } - } - - @PreAuthorize("hasRole('USER') and #userId == authentication.principal.id or hasRole('ADMIN')") - @GetMapping("/user/{userId}") - public ResponseEntity getOrdersByUserId( - @PathVariable Long userId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "id") String sortBy, - @RequestParam(defaultValue = "DESC") String sortDirection - ) { - try { - validateSortField(sortBy); - Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); - PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); - - Page orders = orderService.getOrdersByUserId(userId, pageRequest) - .map(OrderViewModel::fromDTO); - - return ResponseEntity.ok(pagedResourcesAssembler.toModel(orders)); - } catch (IllegalArgumentException e) { - log.error("Invalid request parameters", e); - return ResponseEntity.badRequest() - .body(new ErrorResponse("Invalid request parameters: " + e.getMessage())); - } catch (Exception e) { - log.error("Error retrieving orders", e); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error retrieving orders: " + e.getMessage())); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @GetMapping - public ResponseEntity getAllOrders( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "id") String sortBy, - @RequestParam(defaultValue = "DESC") String sortDirection - ) { - try { - validateSortField(sortBy); - Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); - PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); - - Page orders = orderService.getAllOrders(pageRequest) - .map(OrderViewModel::fromDTO); - - return ResponseEntity.ok(pagedResourcesAssembler.toModel(orders)); - } catch (IllegalArgumentException e) { - log.error("Invalid request parameters", e); - return ResponseEntity.badRequest() - .body(new ErrorResponse("Invalid request parameters: " + e.getMessage())); - } catch (Exception e) { - log.error("Error retrieving orders", e); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error retrieving orders: " + e.getMessage())); - } - } - - @PreAuthorize("hasRole('USER') and @orderService.isOrderOwnedByUser(#orderId, authentication.principal.id) or hasRole('ADMIN')") - @GetMapping("/{orderId}") - public ResponseEntity getOrderById(@PathVariable Long orderId) { - try { - OrderDetailsDTO orderDetails = orderService.getOrderById(orderId); - return ResponseEntity.ok(GetOrderResponse.from(orderDetails)); - } catch (OrderNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (Exception e) { - log.error("Error retrieving order", e); - return ResponseEntity.badRequest().build(); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @PutMapping("/{orderId}/status") - public ResponseEntity updateOrderStatus( - @PathVariable Long orderId, - @Validated @RequestBody OrderStatusUpdateRequest request - ) { - try { - OrderStatus orderStatus = OrderStatus.valueOf(request.status().toUpperCase()); - orderService.updateOrderStatus(orderId, orderStatus); - return ResponseEntity.ok().build(); - } catch (OrderNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse("Invalid order status: " + e.getMessage())); - } catch (IllegalStateException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse("Invalid status transition: " + e.getMessage())); - } catch (Exception e) { - log.error("Error updating order status", e); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error updating order status: " + e.getMessage())); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @DeleteMapping("/{orderId}") - public ResponseEntity cancelOrder(@PathVariable Long orderId) { - try { - orderService.cancelOrder(orderId); - return ResponseEntity.ok().build(); - } catch (OrderNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (IllegalStateException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse("Cannot cancel order: " + e.getMessage())); - } catch (Exception e) { - log.error("Error canceling order", e); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error canceling order: " + e.getMessage())); - } - } - - private void validateSortField(String sortBy) { - if (!VALID_SORT_FIELDS.contains(sortBy)) { - throw new InvalidSortFieldException("Invalid sort field: " + sortBy); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/controller/PaymentController.java b/src/main/java/com/zenfulcode/commercify/web/controller/PaymentController.java deleted file mode 100644 index 500c1e8..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/controller/PaymentController.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.zenfulcode.commercify.web.controller; - -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import com.zenfulcode.commercify.service.core.PaymentService; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -@CrossOrigin -@RestController -@RequestMapping("/api/v1/payments") -@AllArgsConstructor -@Slf4j -public class PaymentController { - private final PaymentService paymentService; - - @PostMapping("/{orderId}/status") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity updatePaymentStatus( - @PathVariable Long orderId, - @RequestParam PaymentStatus status) { - try { - paymentService.handlePaymentStatusUpdate(orderId, status); - return ResponseEntity.ok("Payment status updated successfully"); - } catch (Exception e) { - log.error("Error updating payment status", e); - return ResponseEntity.badRequest().body("Error updating payment status"); - } - } - - @GetMapping("/{orderId}/status") - public ResponseEntity getPaymentStatus(@PathVariable Long orderId) { - PaymentStatus status = paymentService.getPaymentStatus(orderId); - return ResponseEntity.ok(status); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/controller/ProductController.java b/src/main/java/com/zenfulcode/commercify/web/controller/ProductController.java deleted file mode 100644 index 9fb7156..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/controller/ProductController.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.zenfulcode.commercify.web.controller; - -import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; -import com.zenfulcode.commercify.web.dto.request.product.ProductVariantRequest; -import com.zenfulcode.commercify.web.dto.response.ErrorResponse; -import com.zenfulcode.commercify.web.dto.response.ValidationErrorResponse; -import com.zenfulcode.commercify.web.dto.response.product.ProductDeletionErrorResponse; -import com.zenfulcode.commercify.web.dto.response.product.ProductUpdateResponse; -import com.zenfulcode.commercify.web.dto.common.ProductDTO; -import com.zenfulcode.commercify.web.dto.common.ProductUpdateResult; -import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; -import com.zenfulcode.commercify.exception.InvalidSortFieldException; -import com.zenfulcode.commercify.exception.ProductDeletionException; -import com.zenfulcode.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.exception.ProductValidationException; -import com.zenfulcode.commercify.service.core.ProductService; -import com.zenfulcode.commercify.service.ProductVariantService; -import com.zenfulcode.commercify.web.viewmodel.ProductVariantViewModel; -import com.zenfulcode.commercify.web.viewmodel.ProductViewModel; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PagedResourcesAssembler; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.PagedModel; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.Set; - -@RestController -@RequestMapping("/api/v1/products") -@AllArgsConstructor -@Slf4j -public class ProductController { - private final ProductService productService; - private final ProductVariantService variantService; - private final PagedResourcesAssembler productPageAssembler; - private final PagedResourcesAssembler variantPageAssembler; - - private static final Set VALID_SORT_FIELDS = Set.of( - "id", "name", "stock", "createdAt", "updatedAt" - ); - - @PreAuthorize("hasRole('ADMIN')") - @GetMapping - public ResponseEntity>> getAllProducts( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "id") String sortBy, - @RequestParam(defaultValue = "DESC") String sortDirection - ) { - validateSortField(sortBy); - Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); - PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); - - Page products = productService.getAllProducts(pageRequest) - .map(ProductViewModel::fromDTO); - - return ResponseEntity.ok(productPageAssembler.toModel(products)); - } - - @GetMapping("/active") - public ResponseEntity>> getActiveProducts( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "id") String sortBy, - @RequestParam(defaultValue = "DESC") String sortDirection - ) { - validateSortField(sortBy); - Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); - PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); - - Page products = productService.getActiveProducts(pageRequest) - .map(ProductViewModel::fromDTO); - - return ResponseEntity.ok(productPageAssembler.toModel(products)); - } - - @GetMapping("/{id}") - public ResponseEntity getProductById(@PathVariable Long id) { - try { - ProductDTO product = productService.getProductById(id); - return ResponseEntity.ok(ProductViewModel.fromDTO(product)); - } catch (ProductNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (Exception e) { - log.error("Error retrieving product {}", e.getMessage()); - return ResponseEntity.internalServerError().build(); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @PostMapping - public ResponseEntity createProduct(@Validated @RequestBody ProductRequest request) { - try { - ProductDTO product = productService.saveProduct(request); - return ResponseEntity.ok(ProductViewModel.fromDTO(product)); - } catch (ProductValidationException e) { - return ResponseEntity.badRequest().body(new ValidationErrorResponse(e.getErrors())); - } catch (Exception e) { - log.error("Error creating product {}", e.getMessage()); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error creating product: " + e.getMessage())); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @PutMapping("/{id}") - public ResponseEntity updateProduct(@PathVariable Long id, @Validated @RequestBody ProductRequest request) { - try { - ProductUpdateResult result = productService.updateProduct(id, request); - if (!result.getWarnings().isEmpty()) { - return ResponseEntity.ok() - .body(new ProductUpdateResponse( - ProductViewModel.fromDTO(result.getProduct()), - "Product updated with warnings", - result.getWarnings() - )); - } - return ResponseEntity.ok(ProductViewModel.fromDTO(result.getProduct())); - } catch (ProductNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (ProductValidationException e) { - return ResponseEntity.badRequest() - .body(new ValidationErrorResponse(e.getErrors())); - } catch (Exception e) { - log.error("Error updating product {}", e.getMessage()); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error updating product: " + e.getMessage())); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/{id}/reactivate") - public ResponseEntity reactivateProduct(@PathVariable Long id) { - try { - productService.reactivateProduct(id); - return ResponseEntity.ok("Product reactivated"); - } catch (ProductNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (Exception e) { - log.error("Error reactivating product {}", e.getMessage()); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error reactivating product: " + e.getMessage())); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/{id}/deactivate") - public ResponseEntity deactivateProduct(@PathVariable Long id) { - try { - productService.deactivateProduct(id); - return ResponseEntity.ok("Product deactivated"); - } catch (ProductNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (Exception e) { - log.error("Error deactivating product {}", e.getMessage()); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error deactivating product: " + e.getMessage())); - } - } - - @PreAuthorize("hasRole('ADMIN')") - @DeleteMapping("/{id}") - public ResponseEntity deleteProduct(@PathVariable Long id) { - try { - productService.deleteProduct(id); - return ResponseEntity.ok().build(); - } catch (ProductNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (ProductDeletionException e) { - return ResponseEntity.badRequest() - .body(new ProductDeletionErrorResponse( - "Cannot delete product", - e.getIssues(), - e.getActiveOrders() - )); - } catch (Exception e) { - log.error("Error deleting product {}", e.getMessage()); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error deleting product: " + e.getMessage())); - } - } - - // Variant endpoints - @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/{productId}/variants") - public ResponseEntity addVariant( - @PathVariable Long productId, - @Validated @RequestBody ProductVariantRequest request - ) { - try { - ProductVariantEntityDto variant = variantService.addVariant(productId, request); - return ResponseEntity.ok(ProductVariantViewModel.fromDTO(variant)); - } catch (ProductNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (ProductValidationException e) { - return ResponseEntity.badRequest() - .body(new ValidationErrorResponse(e.getErrors())); - } catch (Exception e) { - log.error("Error adding variant {}", e.getMessage()); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error adding variant: " + e.getMessage())); - } - } - - // TODO: DELETING variant options instead of just updating them - @PreAuthorize("hasRole('ADMIN')") - @PutMapping("/{productId}/variants/{variantId}") - public ResponseEntity updateVariant( - @PathVariable Long productId, - @PathVariable Long variantId, - @Validated @RequestBody ProductVariantRequest request - ) { - try { - ProductVariantEntityDto variant = variantService.updateVariant(productId, variantId, request); - return ResponseEntity.ok(ProductVariantViewModel.fromDTO(variant)); - } catch (ProductNotFoundException | IllegalArgumentException e) { - return ResponseEntity.notFound().build(); - } catch (ProductValidationException e) { - return ResponseEntity.badRequest() - .body(new ValidationErrorResponse(e.getErrors())); - } catch (Exception e) { - log.error("Error updating variant{}", e.getMessage()); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error updating variant: " + e.getMessage())); - } - } - - // TODO: the variant doesnt seem to get deleted - @PreAuthorize("hasRole('ADMIN')") - @DeleteMapping("/{productId}/variants/{variantId}") - public ResponseEntity deleteVariant(@PathVariable Long productId, @PathVariable Long variantId) { - try { - variantService.deleteVariant(productId, variantId); - return ResponseEntity.ok("Deleted"); - } catch (ProductNotFoundException | IllegalArgumentException e) { - return ResponseEntity.notFound().build(); - } catch (ProductDeletionException e) { - return ResponseEntity.badRequest() - .body(new ProductDeletionErrorResponse( - "Cannot delete variant", - e.getIssues(), - e.getActiveOrders() - )); - } catch (Exception e) { - log.error("Error deleting variant {}", e.getMessage()); - return ResponseEntity.internalServerError() - .body(new ErrorResponse("Error deleting variant: " + e.getMessage())); - } - } - - @GetMapping("/{productId}/variants") - public ResponseEntity>> getProductVariants( - @PathVariable Long productId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size - ) { - try { - PageRequest pageRequest = PageRequest.of(page, size); - Page variants = variantService.getProductVariants(productId, pageRequest) - .map(ProductVariantViewModel::fromDTO); - - return ResponseEntity.ok(variantPageAssembler.toModel(variants)); - } catch (ProductNotFoundException e) { - return ResponseEntity.notFound().build(); - } catch (Exception e) { - log.error("Error retrieving variants {}", e.getMessage()); - return ResponseEntity.internalServerError().build(); - } - } - - @GetMapping("/{productId}/variants/{variantId}") - public ResponseEntity getVariant( - @PathVariable Long productId, - @PathVariable Long variantId - ) { - try { - ProductVariantEntityDto variant = variantService.getVariantDto(productId, variantId); - return ResponseEntity.ok(ProductVariantViewModel.fromDTO(variant)); - } catch (ProductNotFoundException | IllegalArgumentException e) { - return ResponseEntity.notFound().build(); - } catch (Exception e) { - log.error("Error retrieving variant {}", e.getMessage()); - return ResponseEntity.internalServerError().build(); - } - } - - private void validateSortField(String sortBy) { - if (!VALID_SORT_FIELDS.contains(sortBy)) { - throw new InvalidSortFieldException("Invalid sort field: " + sortBy); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/controller/UserManagementController.java b/src/main/java/com/zenfulcode/commercify/web/controller/UserManagementController.java deleted file mode 100644 index 745b92e..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/controller/UserManagementController.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.zenfulcode.commercify.web.controller; - - -import com.zenfulcode.commercify.web.dto.common.AddressDTO; -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import com.zenfulcode.commercify.service.core.UserManagementService; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PagedResourcesAssembler; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.PagedModel; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/v1/users") -@RequiredArgsConstructor -public class UserManagementController { - private final UserManagementService userManagementService; - private final PagedResourcesAssembler pagedResourcesAssembler; - - @GetMapping("/{id}") - @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") - public ResponseEntity getUserById(@PathVariable Long id) { - return ResponseEntity.ok(userManagementService.getUserById(id)); - } - - @GetMapping - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity>> getAllUsers( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "id") String sortBy, - @RequestParam(defaultValue = "DESC") String sortDirection - ) { - Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); - PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); - - Page users = userManagementService.getAllUsers(pageRequest); - - return ResponseEntity.ok(pagedResourcesAssembler.toModel(users)); - } - - @PutMapping("/{id}") - @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") - public ResponseEntity updateUser(@PathVariable Long id, @RequestBody UserDTO userDTO) { - return ResponseEntity.ok(userManagementService.updateUser(id, userDTO)); - } - - @DeleteMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity deleteUser(@PathVariable Long id) { - userManagementService.deleteUser(id); - return ResponseEntity.ok().build(); - } - - @PostMapping("/{id}/address") - @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") - public ResponseEntity setAddress(@PathVariable Long id, @RequestBody AddressDTO request) { - return ResponseEntity.ok(userManagementService.setDefaultAddress(id, request)); - } - - @DeleteMapping("/{id}/address") - @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") - public ResponseEntity removeAddress(@PathVariable Long id) { - return ResponseEntity.ok(userManagementService.removeDefaultAddress(id)); - } - - @PostMapping("/{id}/roles") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity addRoleToUser(@PathVariable Long id, @RequestBody String role) { - return ResponseEntity.ok(userManagementService.addRoleToUser(id, role)); - } - - @DeleteMapping("/{id}/roles") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity removeRoleFromUser(@PathVariable Long id, @RequestBody String role) { - return ResponseEntity.ok(userManagementService.removeRoleFromUser(id, role)); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/AddressDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/AddressDTO.java deleted file mode 100644 index ba103ff..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/AddressDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -@Builder -@Data -@AllArgsConstructor -public class AddressDTO { - private Long id; - private String street; - private String city; - private String state; - private String zipCode; - private String country; -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/CustomerDetailsDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/CustomerDetailsDTO.java deleted file mode 100644 index 177730f..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/CustomerDetailsDTO.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -@Builder -@Data -@AllArgsConstructor -public class CustomerDetailsDTO { - private String firstName; - private String lastName; - private String email; - private String phone; -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDTO.java deleted file mode 100644 index 12aa4f4..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -import java.time.Instant; - -@Data -@Builder -@AllArgsConstructor -public class OrderDTO { - private long id; - private Long userId; - private String currency; - private double totalAmount; - private OrderStatus orderStatus; - private int orderLinesAmount; - private Instant createdAt; - private Instant updatedAt; -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDetailsDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDetailsDTO.java deleted file mode 100644 index 7f1ecb2..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderDetailsDTO.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -import java.util.List; - -@Builder -@Data -@AllArgsConstructor -public class OrderDetailsDTO { - private OrderDTO order; - private List orderLines; - private CustomerDetailsDTO customerDetails; - private AddressDTO shippingAddress; - private AddressDTO billingAddress; -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderLineDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderLineDTO.java deleted file mode 100644 index 5ef5a3b..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/OrderLineDTO.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -@AllArgsConstructor -public class OrderLineDTO { - private Long id; - private Long productId; - private Long variantId; - private Integer quantity; - private Double unitPrice; - private String currency; - private ProductDTO product; - private ProductVariantEntityDto variant; -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDTO.java deleted file mode 100644 index ef7387f..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -import java.util.List; - -@Builder -@Data -@AllArgsConstructor -public class ProductDTO { - private Long id; - private String name; - private String description; - private Integer stock; - private Boolean active; - private String imageUrl; - private Double unitPrice; - private String currency; - private List variants; -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDeletionValidationResult.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDeletionValidationResult.java deleted file mode 100644 index af5a6b5..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductDeletionValidationResult.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.util.List; - -@Data -@AllArgsConstructor -public class ProductDeletionValidationResult { - private boolean canDelete; - private List issues; - private List activeOrders; - - public boolean canDelete() { - return activeOrders.isEmpty() && issues.isEmpty(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductUpdateResult.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductUpdateResult.java deleted file mode 100644 index 83a6ca0..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductUpdateResult.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -import java.util.List; - -@Data -@AllArgsConstructor -@Builder -public class ProductUpdateResult { - private ProductDTO product; - private List warnings; - - public static ProductUpdateResult withWarnings(ProductDTO product, List warnings) { - return new ProductUpdateResult(product, warnings); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductVariantEntityDto.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductVariantEntityDto.java deleted file mode 100644 index 46c1e60..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/ProductVariantEntityDto.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import com.zenfulcode.commercify.domain.model.ProductVariant; -import lombok.Builder; -import lombok.Data; - -import java.io.Serializable; -import java.util.Set; - -/** - * DTO for {@link ProductVariant} - */ -@Builder -@Data -public class ProductVariantEntityDto implements Serializable { - Long id; - String sku; - Integer stock; - String imageUrl; - Double unitPrice; - Set options; -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/UserDTO.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/UserDTO.java deleted file mode 100644 index c16034f..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/UserDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -import java.util.Date; -import java.util.List; - -@Builder -@Data -@AllArgsConstructor -public class UserDTO { - private Long id; - private String email; - private String firstName; - private String lastName; - private Date createdAt; - private AddressDTO defaultAddress; - private List roles; -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/common/VariantOptionEntityDto.java b/src/main/java/com/zenfulcode/commercify/web/dto/common/VariantOptionEntityDto.java deleted file mode 100644 index e56dc1b..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/common/VariantOptionEntityDto.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.zenfulcode.commercify.web.dto.common; - -import com.zenfulcode.commercify.domain.model.VariantOption; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; - -/** - * DTO for {@link VariantOption} - */ -@Builder -@Data -@AllArgsConstructor -public class VariantOptionEntityDto { - private Long id; - private String name; - private String value; -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/AddressMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/AddressMapper.java deleted file mode 100644 index 79ee70d..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/AddressMapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zenfulcode.commercify.web.dto.mapper; - -import com.zenfulcode.commercify.web.dto.common.AddressDTO; -import com.zenfulcode.commercify.domain.model.Address; -import org.springframework.stereotype.Service; - -import java.util.function.Function; - -@Service -public class AddressMapper implements Function { - @Override - public AddressDTO apply(Address address) { - return AddressDTO.builder() - .id(address.getId()) - .street(address.getStreet()) - .city(address.getCity()) - .state(address.getState()) - .zipCode(address.getZipCode()) - .country(address.getCountry()) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderLineMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderLineMapper.java deleted file mode 100644 index 8f57747..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderLineMapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zenfulcode.commercify.web.dto.mapper; - -import com.zenfulcode.commercify.web.dto.common.OrderLineDTO; -import com.zenfulcode.commercify.domain.model.OrderLine; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.function.Function; - -@Service -@AllArgsConstructor -public class OrderLineMapper implements Function { - @Override - public OrderLineDTO apply(OrderLine orderLine) { - return OrderLineDTO.builder() - .id(orderLine.getId()) - .quantity(orderLine.getQuantity()) - .productId(orderLine.getProductId()) - .unitPrice(orderLine.getUnitPrice()) - .build(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderMapper.java deleted file mode 100644 index 7f31b1e..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/OrderMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.zenfulcode.commercify.web.dto.mapper; - -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import com.zenfulcode.commercify.domain.model.Order; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.function.Function; - -@Service -@RequiredArgsConstructor -public class OrderMapper implements Function { - - @Override - public OrderDTO apply(Order order) { - return OrderDTO.builder() - .id(order.getId()) - .userId(order.getUserId()) - .orderStatus(order.getStatus()) - .createdAt(order.getCreatedAt()) - .updatedAt(order.getUpdatedAt()) - .currency(order.getCurrency() != null ? order.getCurrency() : null) - .totalAmount(order.getTotalAmount() != null ? order.getTotalAmount() : 0.0) - .orderLinesAmount(order.getOrderLines() != null ? order.getOrderLines().size() : 0) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductMapper.java deleted file mode 100644 index 36f2a96..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductMapper.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.zenfulcode.commercify.web.dto.mapper; - -import com.zenfulcode.commercify.domain.model.Product; -import com.zenfulcode.commercify.web.dto.common.ProductDTO; -import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.function.Function; - -@Service -@RequiredArgsConstructor -public class ProductMapper implements Function { - private final ProductVariantMapper variantMapper; - - @Override - public ProductDTO apply(Product product) { - final List variants = product.getVariants().stream().map(variantMapper).toList(); - - return ProductDTO.builder() - .id(product.getId()) - .name(product.getName()) - .description(product.getDescription()) - .stock(product.getStock()) - .active(product.getActive()) - .imageUrl(product.getImageUrl()) - .unitPrice(product.getUnitPrice()) - .currency(product.getCurrency()) - .variants(variants) - .build(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductVariantMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductVariantMapper.java deleted file mode 100644 index 368884f..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/ProductVariantMapper.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.zenfulcode.commercify.web.dto.mapper; - -import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; -import com.zenfulcode.commercify.web.dto.common.VariantOptionEntityDto; -import com.zenfulcode.commercify.domain.model.ProductVariant; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class ProductVariantMapper implements Function { - private final VariantOptionMapper variantOptionMapper; - - @Override - public ProductVariantEntityDto apply(ProductVariant productVariant) { - Set options = productVariant.getOptions().stream() - .map(variantOptionMapper).collect(Collectors.toSet()); - - return ProductVariantEntityDto.builder() - .id(productVariant.getId()) - .sku(productVariant.getSku()) - .stock(productVariant.getStock()) - .imageUrl(productVariant.getImageUrl()) - .unitPrice(productVariant.getUnitPrice()) - .options(options) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/UserMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/UserMapper.java deleted file mode 100644 index e5320bf..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/UserMapper.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.zenfulcode.commercify.web.dto.mapper; - -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import com.zenfulcode.commercify.domain.model.User; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.stereotype.Service; - -import java.util.Date; -import java.util.function.Function; - -@Service -@RequiredArgsConstructor -public class UserMapper implements Function { - private final AddressMapper addressDTOMapper; - - @Override - public UserDTO apply(User user) { - UserDTO.UserDTOBuilder userBuilder = UserDTO.builder() - .id(user.getId()) - .email(user.getEmail()) - .firstName(user.getFirstName()) - .lastName(user.getLastName()) - .roles(user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()) - .createdAt(Date.from(user.getCreatedAt())); - - if (user.getDefaultAddress() != null) { - userBuilder.defaultAddress(addressDTOMapper.apply(user.getDefaultAddress())); - } - - return userBuilder.build(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/VariantOptionMapper.java b/src/main/java/com/zenfulcode/commercify/web/dto/mapper/VariantOptionMapper.java deleted file mode 100644 index 8f86229..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/mapper/VariantOptionMapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zenfulcode.commercify.web.dto.mapper; - -import com.zenfulcode.commercify.web.dto.common.VariantOptionEntityDto; -import com.zenfulcode.commercify.domain.model.VariantOption; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.function.Function; - -@Service -@RequiredArgsConstructor -public class VariantOptionMapper implements Function { - - @Override - public VariantOptionEntityDto apply(VariantOption product) { - return VariantOptionEntityDto.builder() - .id(product.getId()) - .name(product.getName()) - .value(product.getValue()) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/LoginUserRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/LoginUserRequest.java deleted file mode 100644 index 45f8a83..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/LoginUserRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.auth; - -public record LoginUserRequest(String email, String password) { -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/RegisterUserRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/RegisterUserRequest.java deleted file mode 100644 index 9cc77b0..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/auth/RegisterUserRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.auth; - -import com.zenfulcode.commercify.web.dto.common.AddressDTO; - -import java.util.UUID; - -public record RegisterUserRequest( - String email, - String password, - String firstName, - String lastName, - Boolean isGuest, - AddressDTO defaultAddress) { - // Set a secure default password - public RegisterUserRequest { - if (password == null || password.isBlank()) { - password = UUID.randomUUID().toString(); - } - - if (isGuest == null) { - isGuest = false; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderLineRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderLineRequest.java deleted file mode 100644 index 3873b57..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderLineRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.order; - -public record CreateOrderLineRequest( - Long productId, - Long variantId, - Integer quantity -) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderRequest.java deleted file mode 100644 index 0b111e2..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/order/CreateOrderRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.order; - -import com.zenfulcode.commercify.web.dto.common.AddressDTO; -import com.zenfulcode.commercify.web.dto.common.CustomerDetailsDTO; - -import java.util.List; - -public record CreateOrderRequest( - String currency, - CustomerDetailsDTO customerDetails, - List orderLines, - AddressDTO shippingAddress, - AddressDTO billingAddress -) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/order/OrderStatusUpdateRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/order/OrderStatusUpdateRequest.java deleted file mode 100644 index 3119d07..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/order/OrderStatusUpdateRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.order; - -public record OrderStatusUpdateRequest(String status) { -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/payment/PaymentRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/payment/PaymentRequest.java deleted file mode 100644 index 570f0cd..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/payment/PaymentRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.payment; - -public record PaymentRequest(Long orderId, - String currency, - String paymentMethod, - String returnUrl, - String phoneNumber) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/product/CreateVariantOptionRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/CreateVariantOptionRequest.java deleted file mode 100644 index 09916ba..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/product/CreateVariantOptionRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.product; - -public record CreateVariantOptionRequest( - String name, - String value -) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/product/PriceRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/PriceRequest.java deleted file mode 100644 index 1e95816..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/product/PriceRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.product; - -public record PriceRequest( - String currency, - Double amount -) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductRequest.java deleted file mode 100644 index aafaed9..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.product; - -import java.util.List; - -public record ProductRequest( - String name, - String description, - Integer stock, - String imageUrl, - Boolean active, - PriceRequest price, - List variants -) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductVariantRequest.java b/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductVariantRequest.java deleted file mode 100644 index 5a5f1c7..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/request/product/ProductVariantRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zenfulcode.commercify.web.dto.request.product; - -import java.util.List; - -public record ProductVariantRequest( - String sku, - Integer stock, - String imageUrl, - Double unitPrice, - List options -) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/ErrorResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/ErrorResponse.java deleted file mode 100644 index 47a963b..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/ErrorResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class ErrorResponse { - private String message; -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/ValidationErrorResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/ValidationErrorResponse.java deleted file mode 100644 index becfe67..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/ValidationErrorResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.util.List; - -@Data -@AllArgsConstructor -public class ValidationErrorResponse { - private List errors; -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/AuthResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/AuthResponse.java deleted file mode 100644 index 8208a5f..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/AuthResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.auth; - - -import com.zenfulcode.commercify.web.dto.common.UserDTO; - -public record AuthResponse(UserDTO user, String token, long expiresIn, String message) { - public static AuthResponse UserAuthenticated(UserDTO user, String token, long expiresIn) { - return new AuthResponse(user, token, expiresIn, "User authenticated"); - } - - public static AuthResponse AuthenticationFailed(String message) { - return new AuthResponse(null, null, 0, message); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/JwtErrorResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/JwtErrorResponse.java deleted file mode 100644 index 8ca857e..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/JwtErrorResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.auth; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -public class JwtErrorResponse { - private int status; - private String message; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime timestamp; - private String error; - - public JwtErrorResponse(String message, String error) { - this.status = 401; - this.message = message; - this.error = error; - this.timestamp = LocalDateTime.now(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/RegisterUserResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/RegisterUserResponse.java deleted file mode 100644 index 31716d6..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/auth/RegisterUserResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.auth; - - -import com.zenfulcode.commercify.web.dto.common.UserDTO; - -public record RegisterUserResponse(UserDTO user, String message) { - public static RegisterUserResponse RegistrationFailed(String message) { - return new RegisterUserResponse(null, message); - } - - public static RegisterUserResponse UserRegistered(UserDTO user) { - return new RegisterUserResponse(user, "User registered successfully"); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/order/CreateOrderResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/order/CreateOrderResponse.java deleted file mode 100644 index 4c19e3f..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/order/CreateOrderResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.order; - - -import com.zenfulcode.commercify.web.viewmodel.OrderViewModel; - -public record CreateOrderResponse( - OrderViewModel order, - String message) { - public static CreateOrderResponse from(OrderViewModel order) { - return new CreateOrderResponse(order, "Order created successfully"); - } - - public static CreateOrderResponse from(String message) { - return new CreateOrderResponse(null, message); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/order/GetOrderResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/order/GetOrderResponse.java deleted file mode 100644 index d017418..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/order/GetOrderResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.order; - - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; -import com.zenfulcode.commercify.web.viewmodel.OrderLineViewModel; - -import java.time.Instant; -import java.util.List; - -public record GetOrderResponse( - Long id, - Long userId, - OrderStatus orderStatus, - String currency, - Double totalAmount, - Instant createdAt, - Instant updatedAt, - List orderLines -) { - public static GetOrderResponse from(OrderDetailsDTO orderDetails) { - OrderDTO order = orderDetails.getOrder(); - return new GetOrderResponse( - order.getId(), - order.getUserId(), - order.getOrderStatus(), - order.getCurrency(), - order.getTotalAmount(), - order.getCreatedAt(), - order.getUpdatedAt(), - orderDetails.getOrderLines().stream() - .map(OrderLineViewModel::fromDTO) - .toList() - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/CancelPaymentResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/CancelPaymentResponse.java deleted file mode 100644 index 4dbfab5..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/CancelPaymentResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.payment; - -public record CancelPaymentResponse(boolean success, String message) { - - public static CancelPaymentResponse PaymentNotFound() { - return new CancelPaymentResponse(false, "Payment not found"); - } - - public static CancelPaymentResponse PaymentAlreadyPaid() { - return new CancelPaymentResponse(false, "Payment already paid"); - } - - public static CancelPaymentResponse PaymentAlreadyCanceled() { - return new CancelPaymentResponse(false, "Payment already canceled"); - } - - public static CancelPaymentResponse InvalidPaymentProvider() { - return new CancelPaymentResponse(false, "Invalid payment provider"); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/PaymentResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/PaymentResponse.java deleted file mode 100644 index 6797b10..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/payment/PaymentResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.payment; - - -import com.zenfulcode.commercify.domain.enums.PaymentStatus; - -public record PaymentResponse(Long paymentId, PaymentStatus status, String redirectUrl) { - public static PaymentResponse FailedPayment() { - return new PaymentResponse(-1L, PaymentStatus.FAILED, ""); - } -} - diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductDeletionErrorResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductDeletionErrorResponse.java deleted file mode 100644 index 63ba579..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductDeletionErrorResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.product; - -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.util.List; - -@Data -@AllArgsConstructor -public class ProductDeletionErrorResponse { - private String message; - private List issues; - private List activeOrders; -} diff --git a/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductUpdateResponse.java b/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductUpdateResponse.java deleted file mode 100644 index beeb12f..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/dto/response/product/ProductUpdateResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.zenfulcode.commercify.web.dto.response.product; - -import com.zenfulcode.commercify.web.viewmodel.ProductViewModel; - -import java.util.List; - -public record ProductUpdateResponse( - ProductViewModel product, - String message, - List warnings -) { -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderLineViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderLineViewModel.java deleted file mode 100644 index 473851a..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderLineViewModel.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.zenfulcode.commercify.web.viewmodel; - -import com.zenfulcode.commercify.web.dto.common.OrderLineDTO; -import com.zenfulcode.commercify.web.dto.common.ProductDTO; - -public record OrderLineViewModel( - String name, - String description, - int quantity, - String imageUrl, - double unitPrice, - ProductVariantViewModel variant -) { - public static OrderLineViewModel fromDTO(OrderLineDTO orderLineDTO) { - ProductDTO product = orderLineDTO.getProduct(); - return new OrderLineViewModel( - product.getName(), - product.getDescription(), - orderLineDTO.getQuantity(), - product.getImageUrl(), - orderLineDTO.getUnitPrice(), - orderLineDTO.getVariant() != null ? - ProductVariantViewModel.fromDTO(orderLineDTO.getVariant()) : - null - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderViewModel.java deleted file mode 100644 index d083965..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/viewmodel/OrderViewModel.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.zenfulcode.commercify.web.viewmodel; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.web.dto.common.OrderDTO; - -import java.time.Instant; - -public record OrderViewModel( - long id, - Long userId, - int orderLinesAmount, - double totalPrice, - String currency, - OrderStatus orderStatus, - Instant createdAt -) { - public static OrderViewModel fromDTO(OrderDTO orderDTO) { - return new OrderViewModel( - orderDTO.getId(), - orderDTO.getUserId(), - orderDTO.getOrderLinesAmount(), - orderDTO.getTotalAmount(), - orderDTO.getCurrency(), - orderDTO.getOrderStatus(), - orderDTO.getCreatedAt() - ); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/web/viewmodel/PriceViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/PriceViewModel.java deleted file mode 100644 index b1423f2..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/viewmodel/PriceViewModel.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.zenfulcode.commercify.web.viewmodel; - -public record PriceViewModel(String currency, - double amount) { - -} diff --git a/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductVariantViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductVariantViewModel.java deleted file mode 100644 index b1b1b61..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductVariantViewModel.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.zenfulcode.commercify.web.viewmodel; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.zenfulcode.commercify.web.dto.common.ProductVariantEntityDto; - -import java.util.List; -import java.util.stream.Collectors; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ProductVariantViewModel( - long id, - String sku, - List options -) { - public static ProductVariantViewModel fromDTO(ProductVariantEntityDto dto) { - return new ProductVariantViewModel( - dto.getId(), - dto.getSku(), - dto.getOptions().stream() - .map(VariantOptionViewModel::fromDTO) - .collect(Collectors.toList()) - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductViewModel.java deleted file mode 100644 index cae8ef6..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/viewmodel/ProductViewModel.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.zenfulcode.commercify.web.viewmodel; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.zenfulcode.commercify.web.dto.common.ProductDTO; - -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ProductViewModel( - long id, - String name, - String description, - int stock, - String imageUrl, - boolean active, - PriceViewModel price, - List variants -) { - public static ProductViewModel fromDTO(ProductDTO productDTO) { - return new ProductViewModel( - productDTO.getId(), - productDTO.getName(), - productDTO.getDescription() != null ? productDTO.getDescription() : null, - productDTO.getStock(), - productDTO.getImageUrl() != null ? productDTO.getImageUrl() : null, - productDTO.getActive(), - new PriceViewModel( - productDTO.getCurrency(), - productDTO.getUnitPrice() - ), - productDTO.getVariants().stream() - .map(ProductVariantViewModel::fromDTO) - .toList() - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/web/viewmodel/VariantOptionViewModel.java b/src/main/java/com/zenfulcode/commercify/web/viewmodel/VariantOptionViewModel.java deleted file mode 100644 index 53cc7a6..0000000 --- a/src/main/java/com/zenfulcode/commercify/web/viewmodel/VariantOptionViewModel.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.zenfulcode.commercify.web.viewmodel; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.zenfulcode.commercify.web.dto.common.VariantOptionEntityDto; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record VariantOptionViewModel( - String name, - String value -) { - public static VariantOptionViewModel fromDTO(VariantOptionEntityDto dto) { - return new VariantOptionViewModel( - dto.getName(), - dto.getValue() - ); - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/dto/mapper/OrderMapperTest.java b/src/test/java/com/zenfulcode/commercify/dto/mapper/OrderMapperTest.java deleted file mode 100644 index 70a443e..0000000 --- a/src/test/java/com/zenfulcode/commercify/dto/mapper/OrderMapperTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.zenfulcode.commercify.dto.mapper; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.OrderLine; -import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.Instant; -import java.util.LinkedHashSet; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; - -@ExtendWith(MockitoExtension.class) -class OrderMapperTest { - - @InjectMocks - private OrderMapper orderMapper; - - private Order order; - - @BeforeEach - void setUp() { - Instant now = Instant.now(); - - Set orderLines = new LinkedHashSet<>(); - OrderLine orderLine = OrderLine.builder() - .id(1L) - .productId(1L) - .quantity(2) - .unitPrice(99.99) - .currency("USD") - .build(); - orderLines.add(orderLine); - - order = Order.builder() - .id(1L) - .userId(1L) - .orderLines(orderLines) - .status(OrderStatus.PENDING) - .currency("USD") - .totalAmount(199.98) - .createdAt(now) - .updatedAt(now) - .build(); - } - - @Test - @DisplayName("Should map OrderEntity to OrderDTO correctly") - void apply_Success() { - OrderDTO result = orderMapper.apply(order); - - assertNotNull(result); - assertEquals(order.getId(), result.getId()); - assertEquals(order.getUserId(), result.getUserId()); - assertEquals(order.getStatus(), result.getOrderStatus()); - assertEquals(order.getCurrency(), result.getCurrency()); - assertEquals(order.getTotalAmount(), result.getTotalAmount()); - assertEquals(order.getCreatedAt(), result.getCreatedAt()); - assertEquals(order.getUpdatedAt(), result.getUpdatedAt()); - assertEquals(order.getOrderLines().size(), result.getOrderLinesAmount()); - } - - @Test - @DisplayName("Should handle OrderEntity with null values") - void apply_HandlesNullValues() { - Order emptyOrder = Order.builder() - .id(1L) - .build(); - - OrderDTO result = orderMapper.apply(emptyOrder); - - assertNotNull(result); - assertEquals(1L, result.getId()); - assertEquals(0, result.getOrderLinesAmount()); - assertNull(result.getUserId()); - assertNull(result.getCurrency()); - assertEquals(0.0, result.getTotalAmount()); - } - - @Test - @DisplayName("Should handle null totalAmount correctly") - void apply_HandlesNullTotalAmount() { - order.setTotalAmount(null); - - OrderDTO result = orderMapper.apply(order); - - assertNotNull(result); - assertEquals(0.0, result.getTotalAmount()); - } - - @Test - @DisplayName("Should handle null orderLines correctly") - void apply_HandlesNullOrderLines() { - order.setOrderLines(null); - - OrderDTO result = orderMapper.apply(order); - - assertNotNull(result); - assertEquals(0, result.getOrderLinesAmount()); - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/entity/OrderEntityTest.java b/src/test/java/com/zenfulcode/commercify/entity/OrderEntityTest.java deleted file mode 100644 index 2053cb1..0000000 --- a/src/test/java/com/zenfulcode/commercify/entity/OrderEntityTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.zenfulcode.commercify.entity; - - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.OrderLine; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.time.DateTimeException; -import java.time.Instant; -import java.time.LocalDateTime; -import java.util.HashSet; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; - -class OrderEntityTest { - - private Order order; - - @BeforeEach - void setUp() { - Set orderLines = new HashSet<>(); - OrderLine orderLine = new OrderLine(); - orderLine.setProductId(1L); - orderLine.setQuantity(2); - orderLine.setUnitPrice(99.99); - orderLine.setCurrency("USD"); - orderLines.add(orderLine); - - order = Order.builder() - .id(1L) - .userId(1L) - .orderLines(orderLines) - .status(OrderStatus.PENDING) - .currency("USD") - .totalAmount(199.98) - .build(); - - // Set up bidirectional relationship - orderLines.forEach(line -> line.setOrder(order)); - } - - @Test - @DisplayName("Should create order with builder pattern") - void testOrderBuilder() { - assertNotNull(order); - assertEquals(1L, order.getId()); - assertEquals(1L, order.getUserId()); - assertEquals(OrderStatus.PENDING, order.getStatus()); - assertEquals("USD", order.getCurrency()); - assertEquals(199.98, order.getTotalAmount()); - } - - @Test - @DisplayName("Should manage order lines correctly") - void testOrderLines() { - assertEquals(1, order.getOrderLines().size()); - OrderLine firstLine = order.getOrderLines().stream().findFirst().orElse(null); - assertEquals(2, firstLine.getQuantity()); - assertEquals(99.99, firstLine.getUnitPrice()); - assertEquals(order, firstLine.getOrder()); - } - - @Test - @DisplayName("Should handle empty order lines") - void testEmptyOrderLines() { - order.setOrderLines(new HashSet<>()); - assertTrue(order.getOrderLines().isEmpty()); - } - - @Test - @DisplayName("Should update status correctly") - void testStatusUpdate() { - order.setStatus(OrderStatus.CONFIRMED); - assertEquals(OrderStatus.CONFIRMED, order.getStatus()); - } - - @Test - @DisplayName("Should handle timestamps correctly") - void testTimestamps() { - LocalDateTime now = LocalDateTime.now(); - - assertThrows(DateTimeException.class, () -> order.setCreatedAt(Instant.from(now))); - assertThrows(DateTimeException.class, () -> order.setUpdatedAt(Instant.from(now))); - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/entity/ProductEntityTest.java b/src/test/java/com/zenfulcode/commercify/entity/ProductEntityTest.java deleted file mode 100644 index dcde14a..0000000 --- a/src/test/java/com/zenfulcode/commercify/entity/ProductEntityTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.zenfulcode.commercify.entity; - - -import com.zenfulcode.commercify.domain.model.Product; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class ProductEntityTest { - - private Product product; - - @BeforeEach - void setUp() { - product = com.zenfulcode.commercify.domain.model.Product.builder() - .id(1L) - .name("Test Product") - .description("Test Description") - .stock(10) - .active(true) - .imageUrl("test-image.jpg") - .currency("USD") - .unitPrice(99.99) - .build(); - } - - @Test - @DisplayName("Should create product with builder pattern") - void testProductBuilder() { - assertNotNull(product); - assertEquals("Test Product", product.getName()); - assertEquals("Test Description", product.getDescription()); - assertEquals(10, product.getStock()); - assertTrue(product.getActive()); - assertEquals("USD", product.getCurrency()); - assertEquals(99.99, product.getUnitPrice()); - } - - @Test - @DisplayName("Should update product fields") - void testProductUpdate() { - product.setName("Updated Product"); - product.setStock(5); - product.setUnitPrice(149.99); - - assertEquals("Updated Product", product.getName()); - assertEquals(5, product.getStock()); - assertEquals(149.99, product.getUnitPrice()); - } - - @Test - @DisplayName("Should handle null values") - void testNullValues() { - Product emptyProduct = new Product(); - assertNull(emptyProduct.getId()); - assertNull(emptyProduct.getName()); - assertNull(emptyProduct.getStock()); - assertNull(emptyProduct.getActive()); - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/factory/ProductFactoryTest.java b/src/test/java/com/zenfulcode/commercify/factory/ProductFactoryTest.java deleted file mode 100644 index 849a016..0000000 --- a/src/test/java/com/zenfulcode/commercify/factory/ProductFactoryTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.zenfulcode.commercify.factory; - -import com.zenfulcode.commercify.component.factory.ProductFactory; -import com.zenfulcode.commercify.web.dto.request.product.PriceRequest; -import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; -import com.zenfulcode.commercify.domain.model.Product; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.*; - -@ExtendWith(MockitoExtension.class) -class ProductFactoryTest { - - @InjectMocks - private ProductFactory productFactory; - - private ProductRequest createRequest; - - @BeforeEach - void setUp() { - PriceRequest createPriceRequest = new PriceRequest("USD", 99.99); - createRequest = new ProductRequest( - "Test Product", - "Test Description", - 10, - "test-image.jpg", - true, - createPriceRequest, - new ArrayList<>() - ); - - PriceRequest priceRequest = new PriceRequest( - "USD", - 99.99 - ); - ProductRequest updateRequest = new ProductRequest( - "Updated Product", - "Updated Description", - 20, - "updated-image.jpg", - true, - priceRequest, - new ArrayList<>() - ); - } - - @Nested - @DisplayName("Create Product Tests") - class CreateProductTests { - - @Test - @DisplayName("Should create product from request") - void testCreateFromRequest() { - Product result = productFactory.createFromRequest(createRequest); - - assertNotNull(result); - assertEquals("Test Product", result.getName()); - assertEquals("Test Description", result.getDescription()); - assertEquals(10, result.getStock()); - assertTrue(result.getActive()); - assertEquals("test-image.jpg", result.getImageUrl()); - assertEquals("USD", result.getCurrency()); - assertEquals(99.99, result.getUnitPrice()); - } - - @Test - @DisplayName("Should handle null stock in create request") - void testCreateFromRequestWithNullStock() { - ProductRequest requestWithNullStock = new ProductRequest( - "Test Product", - "Test Description", - null, - "test-image.jpg", - true, - new PriceRequest("USD", 99.99), - new ArrayList<>() - ); - - Product result = productFactory.createFromRequest(requestWithNullStock); - - assertNotNull(result); - assertEquals(0, result.getStock()); - } - - @Test - @DisplayName("Should preserve unitPrice information in create request") - void testCreateFromRequestPriceInfo() { - PriceRequest priceRequest = new PriceRequest("EUR", 149.99); - ProductRequest requestWithDifferentPrice = new ProductRequest( - "Test Product", - "Test Description", - 10, - "test-image.jpg", - true, - priceRequest, - new ArrayList<>() - ); - - Product result = productFactory.createFromRequest(requestWithDifferentPrice); - - assertEquals("EUR", result.getCurrency()); - assertEquals(149.99, result.getUnitPrice()); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/flow/OrderStateFlowTest.java b/src/test/java/com/zenfulcode/commercify/flow/OrderStateFlowTest.java deleted file mode 100644 index da51e5a..0000000 --- a/src/test/java/com/zenfulcode/commercify/flow/OrderStateFlowTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.zenfulcode.commercify.flow; - -import com.zenfulcode.commercify.component.flow.OrderStateFlow; -import com.zenfulcode.commercify.component.flow.PaymentStateFlow; -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; - -class StateFlowTest { - - @Nested - @DisplayName("Order State Flow Tests") - class OrderStateFlowTest { - private final OrderStateFlow orderStateFlow = new OrderStateFlow(); - - @Test - @DisplayName("PENDING order can transition to CONFIRMED or CANCELLED") - void testPendingTransitions() { - Set validTransitions = orderStateFlow.getValidTransitions(OrderStatus.PENDING); - assertTrue(orderStateFlow.canTransition(OrderStatus.PENDING, OrderStatus.CONFIRMED)); - assertTrue(orderStateFlow.canTransition(OrderStatus.PENDING, OrderStatus.CANCELLED)); - assertEquals(2, validTransitions.size()); - assertTrue(validTransitions.contains(OrderStatus.CONFIRMED)); - assertTrue(validTransitions.contains(OrderStatus.CANCELLED)); - } - - @Test - @DisplayName("CONFIRMED order can transition to SHIPPED or CANCELLED") - void testConfirmedTransitions() { - Set validTransitions = orderStateFlow.getValidTransitions(OrderStatus.CONFIRMED); - assertTrue(orderStateFlow.canTransition(OrderStatus.CONFIRMED, OrderStatus.SHIPPED)); - assertTrue(orderStateFlow.canTransition(OrderStatus.CONFIRMED, OrderStatus.CANCELLED)); - assertEquals(2, validTransitions.size()); - } - - @Test - @DisplayName("SHIPPED order can transition to COMPLETED or RETURNED") - void testShippedTransitions() { - Set validTransitions = orderStateFlow.getValidTransitions(OrderStatus.SHIPPED); - assertTrue(orderStateFlow.canTransition(OrderStatus.SHIPPED, OrderStatus.COMPLETED)); - assertTrue(orderStateFlow.canTransition(OrderStatus.SHIPPED, OrderStatus.RETURNED)); - assertEquals(2, validTransitions.size()); - } - - @Test - @DisplayName("Terminal states cannot transition") - void testTerminalStates() { - OrderStatus[] terminalStates = { - OrderStatus.COMPLETED, OrderStatus.CANCELLED, OrderStatus.RETURNED, OrderStatus.REFUNDED - }; - - for (OrderStatus status : terminalStates) { - assertTrue(orderStateFlow.isTerminalState(status)); - assertTrue(orderStateFlow.getValidTransitions(status).isEmpty()); - } - } - - @Test - @DisplayName("Invalid transitions are not allowed") - void testInvalidTransitions() { - assertFalse(orderStateFlow.canTransition(OrderStatus.PENDING, OrderStatus.COMPLETED)); - assertFalse(orderStateFlow.canTransition(OrderStatus.CONFIRMED, OrderStatus.RETURNED)); - assertFalse(orderStateFlow.canTransition(OrderStatus.SHIPPED, OrderStatus.CANCELLED)); - assertFalse(orderStateFlow.canTransition(OrderStatus.COMPLETED, OrderStatus.SHIPPED)); - } - } - - @Nested - @DisplayName("Payment State Flow Tests") - class PaymentStateFlowTest { - private final PaymentStateFlow paymentStateFlow = new PaymentStateFlow(); - - @Test - @DisplayName("PENDING payment can transition to PAID, FAILED, CANCELLED, or EXPIRED") - void testPendingTransitions() { - Set validTransitions = paymentStateFlow.getValidTransitions(PaymentStatus.PENDING); - assertTrue(paymentStateFlow.canTransition(PaymentStatus.PENDING, PaymentStatus.PAID)); - assertTrue(paymentStateFlow.canTransition(PaymentStatus.PENDING, PaymentStatus.FAILED)); - assertTrue(paymentStateFlow.canTransition(PaymentStatus.PENDING, PaymentStatus.CANCELLED)); - assertTrue(paymentStateFlow.canTransition(PaymentStatus.PENDING, PaymentStatus.EXPIRED)); - assertEquals(4, validTransitions.size()); - } - - @Test - @DisplayName("PAID payment can only transition to REFUNDED") - void testPaidTransitions() { - Set validTransitions = paymentStateFlow.getValidTransitions(PaymentStatus.PAID); - assertTrue(paymentStateFlow.canTransition(PaymentStatus.PAID, PaymentStatus.REFUNDED)); - assertEquals(1, validTransitions.size()); - assertTrue(validTransitions.contains(PaymentStatus.REFUNDED)); - } - - @Test - @DisplayName("Terminal payment states cannot transition") - void testTerminalStates() { - PaymentStatus[] terminalStates = { - PaymentStatus.FAILED, PaymentStatus.CANCELLED, PaymentStatus.REFUNDED, - PaymentStatus.EXPIRED, PaymentStatus.TERMINATED - }; - - for (PaymentStatus status : terminalStates) { - assertTrue(paymentStateFlow.isTerminalState(status)); - assertTrue(paymentStateFlow.getValidTransitions(status).isEmpty()); - } - } - - @Test - @DisplayName("Invalid payment transitions are not allowed") - void testInvalidTransitions() { - assertFalse(paymentStateFlow.canTransition(PaymentStatus.PENDING, PaymentStatus.TERMINATED)); - assertFalse(paymentStateFlow.canTransition(PaymentStatus.PAID, PaymentStatus.CANCELLED)); - assertFalse(paymentStateFlow.canTransition(PaymentStatus.FAILED, PaymentStatus.PAID)); - assertFalse(paymentStateFlow.canTransition(PaymentStatus.REFUNDED, PaymentStatus.PENDING)); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/integration/mobilepay/MobilePayServiceTest.java b/src/test/java/com/zenfulcode/commercify/integration/mobilepay/MobilePayServiceTest.java deleted file mode 100644 index 33f45a0..0000000 --- a/src/test/java/com/zenfulcode/commercify/integration/mobilepay/MobilePayServiceTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.zenfulcode.commercify.integration.mobilepay; - -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import com.zenfulcode.commercify.domain.model.Payment; -import com.zenfulcode.commercify.exception.PaymentProcessingException; -import com.zenfulcode.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.service.core.PaymentService; -import com.zenfulcode.commercify.service.integration.mobilepay.MobilePayService; -import com.zenfulcode.commercify.service.integration.mobilepay.MobilePayTokenService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.web.client.RestTemplate; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class MobilePayServiceTest { - - @Mock - private PaymentRepository paymentRepository; - - @Mock - private PaymentService paymentService; - - @Mock - private RestTemplate restTemplate; - - @Mock - private MobilePayTokenService tokenService; - - @InjectMocks - private MobilePayService mobilePayService; - - private Payment payment; - private static final String PAYMENT_REFERENCE = "test-reference"; - - @BeforeEach - void setUp() { - payment = Payment.builder() - .id(1L) - .orderId(1L) - .mobilePayReference(PAYMENT_REFERENCE) - .status(PaymentStatus.PENDING) - .build(); - } - - @Test - @DisplayName("Should handle successful payment callback") - void handlePaymentCallback_Success() { - when(paymentRepository.findByMobilePayReference(PAYMENT_REFERENCE)) - .thenReturn(Optional.of(payment)); - - mobilePayService.handlePaymentCallback(PAYMENT_REFERENCE, "AUTHORIZED"); - - verify(paymentService).handlePaymentStatusUpdate(eq(1L), eq(PaymentStatus.PAID)); - } - - @Test - @DisplayName("Should handle payment not found in callback") - void handlePaymentCallback_PaymentNotFound() { - when(paymentRepository.findByMobilePayReference(PAYMENT_REFERENCE)) - .thenReturn(Optional.empty()); - - assertThrows(PaymentProcessingException.class, () -> - mobilePayService.handlePaymentCallback(PAYMENT_REFERENCE, "AUTHORIZED")); - } - - @Test - @DisplayName("Should handle aborted payment") - void handlePaymentCallback_Aborted() { - when(paymentRepository.findByMobilePayReference(PAYMENT_REFERENCE)) - .thenReturn(Optional.of(payment)); - - mobilePayService.handlePaymentCallback(PAYMENT_REFERENCE, "ABORTED"); - - verify(paymentService).handlePaymentStatusUpdate(eq(1L), eq(PaymentStatus.CANCELLED)); - } - - @Test - @DisplayName("Should handle expired payment") - void handlePaymentCallback_Expired() { - when(paymentRepository.findByMobilePayReference(PAYMENT_REFERENCE)) - .thenReturn(Optional.of(payment)); - - mobilePayService.handlePaymentCallback(PAYMENT_REFERENCE, "EXPIRED"); - - verify(paymentService).handlePaymentStatusUpdate(eq(1L), eq(PaymentStatus.EXPIRED)); - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/service/AuthenticationServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/AuthenticationServiceTest.java deleted file mode 100644 index 8f4791e..0000000 --- a/src/test/java/com/zenfulcode/commercify/service/AuthenticationServiceTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.zenfulcode.commercify.service; - - -import com.zenfulcode.commercify.web.dto.request.auth.LoginUserRequest; -import com.zenfulcode.commercify.web.dto.request.auth.RegisterUserRequest; -import com.zenfulcode.commercify.web.dto.common.AddressDTO; -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import com.zenfulcode.commercify.web.dto.mapper.UserMapper; -import com.zenfulcode.commercify.domain.model.User; -import com.zenfulcode.commercify.repository.AddressRepository; -import com.zenfulcode.commercify.repository.UserRepository; -import com.zenfulcode.commercify.service.authentication.AuthenticationService; -import com.zenfulcode.commercify.service.authentication.JwtService; -import com.zenfulcode.commercify.service.core.UserManagementService; -import com.zenfulcode.commercify.service.email.EmailConfirmationService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class AuthenticationServiceTest { - - @Mock - private UserRepository userRepository; - - @Mock - private AddressRepository addressRepository; - @Mock - private AuthenticationManager authenticationManager; - - @Mock - private UserMapper userMapper; - - @Mock - private JwtService jwtService; - - @Mock - private PasswordEncoder passwordEncoder; - - @Mock - private UserManagementService userManagementService; - - @Mock - private EmailConfirmationService emailConfirmationService; - - @InjectMocks - private AuthenticationService authenticationService; - - private RegisterUserRequest registerRequest; - private User user; - private UserDTO userDTO; - private LoginUserRequest loginRequest; - - @BeforeEach - void setUp() { - AddressDTO shippingAddress = AddressDTO.builder() - .street("123 Test St") - .city("Test City") - .state("Test State") - .zipCode("12345") - .country("Test Country") - .build(); - - registerRequest = new RegisterUserRequest( - "test@example.com", - "password123", - "John", - "Doe", - false, - shippingAddress - ); - - user = User.builder() - .id(1L) - .email("test@example.com") - .password("encoded_password") - .firstName("John") - .lastName("Doe") - .roles(List.of("USER")) - .emailConfirmed(true) - .build(); - - userDTO = UserDTO.builder() - .id(1L) - .email("test@example.com") - .firstName("John") - .lastName("Doe") - .roles(List.of("USER")) - .defaultAddress(shippingAddress) - .build(); - - loginRequest = new LoginUserRequest("test@example.com", "password123"); - } - - @Test - @DisplayName("Should successfully register a new user") - void registerUser_Success() { - when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); - when(passwordEncoder.encode(anyString())).thenReturn("encoded_password"); - when(userRepository.save(any(User.class))).thenReturn(user); - when(userMapper.apply(any(User.class))).thenReturn(userDTO); - - UserDTO result = authenticationService.registerUser(registerRequest); - - assertNotNull(result); - assertEquals("test@example.com", result.getEmail()); - assertEquals("John", result.getFirstName()); - assertEquals("Doe", result.getLastName()); - - verify(userRepository).findByEmail("test@example.com"); - verify(userRepository).save(any(User.class)); - verify(userMapper).apply(any(User.class)); - } - - @Test - void registerUser_NoPasswordProvided_ShouldSetDefaultPassword() { - // Arrange - RegisterUserRequest request = new RegisterUserRequest( - "test@example.com", "", "Test", "User", - false, - null); - - when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty()); - when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword"); - when(userRepository.save(any(User.class))).thenReturn(user); - when(userMapper.apply(any(User.class))).thenReturn(userDTO); - - // Act - UserDTO result = authenticationService.registerUser(request); - - // Assert - assertNotNull(result); - assertEquals("test@example.com", result.getEmail()); - verify(passwordEncoder, times(1)).encode(anyString()); - verify(userRepository, times(1)).save(any(User.class)); - } - - @Test - @DisplayName("Should throw exception when registering with existing email") - void registerUser_ExistingEmail() { - when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); - - assertThrows(RuntimeException.class, () -> authenticationService.registerUser(registerRequest)); - - verify(userRepository).findByEmail("test@example.com"); - verify(userRepository, never()).save(any(User.class)); - } - - @Test - @DisplayName("Should successfully authenticate user") - void authenticate_Success() { - when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); - when(userMapper.apply(any(User.class))).thenReturn(userDTO); - when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true); - - UserDTO result = authenticationService.authenticate(loginRequest); - - assertNotNull(result); - assertEquals("test@example.com", result.getEmail()); - verify(authenticationManager).authenticate( - new UsernamePasswordAuthenticationToken("test@example.com", "password123") - ); - } - - @Test - @DisplayName("Should get authenticated user details") - void getAuthenticatedUser_Success() { - String jwt = "Bearer valid_jwt_token"; - when(jwtService.extractUsername(anyString())).thenReturn("test@example.com"); - when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user)); - when(userMapper.apply(any(User.class))).thenReturn(userDTO); - - UserDTO result = authenticationService.getAuthenticatedUser(jwt); - - assertNotNull(result); - assertEquals("test@example.com", result.getEmail()); - verify(jwtService).extractUsername("valid_jwt_token"); - verify(userRepository).findByEmail("test@example.com"); - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/service/PaymentServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/PaymentServiceTest.java deleted file mode 100644 index c8b8af9..0000000 --- a/src/test/java/com/zenfulcode/commercify/service/PaymentServiceTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.zenfulcode.commercify.service; - -import com.zenfulcode.commercify.domain.enums.PaymentStatus; -import com.zenfulcode.commercify.web.dto.common.OrderDetailsDTO; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.Payment; -import com.zenfulcode.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.repository.PaymentRepository; -import com.zenfulcode.commercify.service.core.PaymentService; -import com.zenfulcode.commercify.service.email.EmailService; -import com.zenfulcode.commercify.service.core.OrderService; -import jakarta.mail.MessagingException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class PaymentServiceTest { - - @Mock - private PaymentRepository paymentRepository; - - @Mock - private OrderRepository orderRepository; - - @Mock - private EmailService emailService; - - @Mock - private OrderService orderService; - - @InjectMocks - private PaymentService paymentService; - - private Payment payment; - private Order order; - private OrderDetailsDTO orderDetails; - - @BeforeEach - void setUp() { - payment = Payment.builder() - .id(1L) - .orderId(1L) - .status(PaymentStatus.PENDING) - .totalAmount(199.99) - .build(); - - order = Order.builder() - .id(1L) - .userId(1L) - .totalAmount(199.99) - .build(); - - orderDetails = new OrderDetailsDTO(null, null, null, null, null); // Simplified for testing - } - - @Test - @DisplayName("Should update payment status successfully") - void handlePaymentStatusUpdate_Success() { - when(paymentRepository.findByOrderId(1L)).thenReturn(Optional.of(payment)); - when(paymentRepository.save(any(Payment.class))).thenReturn(payment); - when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); - - paymentService.handlePaymentStatusUpdate(1L, PaymentStatus.PAID); - - verify(paymentRepository).save(payment); - assertThat(payment.getStatus()).isEqualTo(PaymentStatus.PAID); - } - - @Test - @DisplayName("Should send confirmation email when payment is successful") - void handlePaymentStatusUpdate_SendsEmail() throws MessagingException { - when(paymentRepository.findByOrderId(1L)).thenReturn(Optional.of(payment)); - when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); - when(orderService.getOrderById(1L)).thenReturn(orderDetails); - - paymentService.handlePaymentStatusUpdate(1L, PaymentStatus.PAID); - - verify(emailService).sendOrderConfirmation(orderDetails); - } - - @Test - @DisplayName("Should not send email for non-successful payment status") - void handlePaymentStatusUpdate_NoEmailForNonSuccess() throws MessagingException { - when(paymentRepository.findByOrderId(1L)).thenReturn(Optional.of(payment)); - - paymentService.handlePaymentStatusUpdate(1L, PaymentStatus.FAILED); - - verify(emailService, never()).sendOrderConfirmation(any()); - } - - @Test - @DisplayName("Should handle payment not found") - void handlePaymentStatusUpdate_PaymentNotFound() { - when(paymentRepository.findByOrderId(1L)).thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, () -> - paymentService.handlePaymentStatusUpdate(1L, PaymentStatus.PAID)); - } - - @Test - @DisplayName("Should handle email sending failure gracefully") - void handlePaymentStatusUpdate_EmailFailure() throws MessagingException { - when(paymentRepository.findByOrderId(1L)).thenReturn(Optional.of(payment)); - when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); - when(orderService.getOrderById(1L)).thenReturn(orderDetails); - doThrow(new MessagingException("Failed to send email")) - .when(emailService).sendOrderConfirmation(any()); - - paymentService.handlePaymentStatusUpdate(1L, PaymentStatus.PAID); - - verify(paymentRepository).save(payment); - assertThat(payment.getStatus()).isEqualTo(PaymentStatus.PAID); - } - - @Test - @DisplayName("Should get payment status successfully") - void getPaymentStatus_Success() { - when(paymentRepository.findByOrderId(1L)).thenReturn(Optional.of(payment)); - - PaymentStatus status = paymentService.getPaymentStatus(1L); - - assertThat(status).isEqualTo(PaymentStatus.PENDING); - } - - @Test - @DisplayName("Should return NOT_FOUND for non-existent payment") - void getPaymentStatus_NotFound() { - when(paymentRepository.findByOrderId(1L)).thenReturn(Optional.empty()); - - PaymentStatus status = paymentService.getPaymentStatus(1L); - - assertThat(status).isEqualTo(PaymentStatus.NOT_FOUND); - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/service/UserAddressServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/UserAddressServiceTest.java deleted file mode 100644 index 1230910..0000000 --- a/src/test/java/com/zenfulcode/commercify/service/UserAddressServiceTest.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.zenfulcode.commercify.service; - -import com.zenfulcode.commercify.web.dto.common.AddressDTO; -import com.zenfulcode.commercify.web.dto.common.UserDTO; -import com.zenfulcode.commercify.web.dto.mapper.AddressMapper; -import com.zenfulcode.commercify.web.dto.mapper.UserMapper; -import com.zenfulcode.commercify.domain.model.Address; -import com.zenfulcode.commercify.domain.model.User; -import com.zenfulcode.commercify.repository.AddressRepository; -import com.zenfulcode.commercify.repository.UserRepository; -import com.zenfulcode.commercify.service.core.UserManagementService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class UserAddressServiceTest { - - @Mock - private UserRepository userRepository; - - @Mock - private AddressRepository addressRepository; - - @Mock - private UserMapper userMapper; - - @Mock - private AddressMapper addressMapper; - - @InjectMocks - private UserManagementService userManagementService; - - private User user; - private Address shippingAddress; - private AddressDTO address; - private UserDTO userDTO; - - @BeforeEach - void setUp() { - user = User.builder() - .id(1L) - .email("test@example.com") - .firstName("John") - .lastName("Doe") - .build(); - - shippingAddress = Address.builder() - .id(1L) - .street("123 Ship St") - .city("Ship City") - .state("Ship State") - .zipCode("12345") - .country("Ship Country") - .build(); - - address = AddressDTO.builder() - .street("789 Test St") - .city("Test City") - .state("Test State") - .zipCode("54321") - .country("Test Country") - .build(); - - userDTO = UserDTO.builder() - .id(1L) - .email("test@example.com") - .firstName("John") - .lastName("Doe") - .build(); - } - - @Nested - @DisplayName("Shipping Address Tests") - class ShippingAddressTests { - - @Test - @DisplayName("Should set shipping address successfully") - void setShippingAddress_Success() { - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(userRepository.save(any(User.class))).thenReturn(user); - - userManagementService.setDefaultAddress(1L, address); - - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); - verify(userRepository).save(userCaptor.capture()); - - Address savedAddress = userCaptor.getValue().getDefaultAddress(); - assertEquals(address.getStreet(), savedAddress.getStreet()); - assertEquals(address.getCity(), savedAddress.getCity()); - assertEquals(address.getState(), savedAddress.getState()); - assertEquals(address.getZipCode(), savedAddress.getZipCode()); - assertEquals(address.getCountry(), savedAddress.getCountry()); - } - - @Test - @DisplayName("Should remove shipping address successfully") - void removeShippingAddress_Success() { - user.setDefaultAddress(shippingAddress); - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(userRepository.save(any(User.class))).thenReturn(user); - when(userMapper.apply(any(User.class))).thenReturn(userDTO); - - userManagementService.removeDefaultAddress(1L); - - verify(userRepository).save(user); - assertNull(user.getDefaultAddress()); - } - - @Test - @DisplayName("Should throw exception when user not found - shipping") - void setShippingAddress_UserNotFound() { - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, - () -> userManagementService.setDefaultAddress(1L, address)); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/service/order/OrderServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/order/OrderServiceTest.java deleted file mode 100644 index 9c09e06..0000000 --- a/src/test/java/com/zenfulcode/commercify/service/order/OrderServiceTest.java +++ /dev/null @@ -1,260 +0,0 @@ -package com.zenfulcode.commercify.service.order; - -import com.zenfulcode.commercify.domain.enums.OrderStatus; -import com.zenfulcode.commercify.domain.model.Order; -import com.zenfulcode.commercify.domain.model.OrderLine; -import com.zenfulcode.commercify.domain.model.Product; -import com.zenfulcode.commercify.exception.OrderNotFoundException; -import com.zenfulcode.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.repository.OrderRepository; -import com.zenfulcode.commercify.repository.OrderShippingInfoRepository; -import com.zenfulcode.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.service.StockManagementService; -import com.zenfulcode.commercify.service.core.OrderService; -import com.zenfulcode.commercify.service.validations.OrderValidationService; -import com.zenfulcode.commercify.web.dto.common.AddressDTO; -import com.zenfulcode.commercify.web.dto.common.CustomerDetailsDTO; -import com.zenfulcode.commercify.web.dto.common.OrderDTO; -import com.zenfulcode.commercify.web.dto.mapper.OrderMapper; -import com.zenfulcode.commercify.web.dto.request.order.CreateOrderLineRequest; -import com.zenfulcode.commercify.web.dto.request.order.CreateOrderRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; - -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class OrderServiceTest { - - @Mock - private OrderRepository orderRepository; - @Mock - private ProductRepository productRepository; - @Mock - private OrderMapper orderMapper; - @Mock - private OrderValidationService validationService; - @Mock - private StockManagementService stockService; - @Mock - private OrderShippingInfoRepository orderShippingInfoRepository; - - @InjectMocks - private OrderService orderService; - - private Order order; - private OrderDTO orderDTO; - private CreateOrderRequest createOrderRequest; - private Product productEntity; - - @BeforeEach - void setUp() { - productEntity = com.zenfulcode.commercify.domain.model.Product.builder() - .id(1L) - .name("Test Product") - .active(true) - .stock(10) - .unitPrice(99.99) - .currency("USD") - .build(); - - OrderLine orderLine = OrderLine.builder() - .id(1L) - .productId(1L) - .quantity(2) - .unitPrice(99.99) - .currency("USD") - .build(); - - CustomerDetailsDTO customerDetailsDTO = CustomerDetailsDTO.builder() - .firstName("Test") - .lastName("User") - .email("test@email.com") - .phone("1234567890") - .build(); - - order = Order.builder() - .id(1L) - .userId(1L) - .status(OrderStatus.PENDING) - .currency("USD") - .totalAmount(199.98) - .orderLines(Set.of(orderLine)) - .createdAt(Instant.now()) - .build(); - - orderDTO = OrderDTO.builder() - .id(1L) - .userId(1L) - .orderStatus(OrderStatus.PENDING) - .currency("USD") - .totalAmount(199.98) - .build(); - - AddressDTO addressDTO = AddressDTO.builder() - .street("Test Street") - .city("Test City") - .state("Test State") - .zipCode("12345") - .country("Test Country") - .build(); - - CreateOrderLineRequest orderLineRequest = new CreateOrderLineRequest(1L, null, 2); - createOrderRequest = new CreateOrderRequest("USD", customerDetailsDTO, List.of(orderLineRequest), addressDTO, null); - } - - @Nested - @DisplayName("Create Order Tests") - class CreateOrderTests { - - @Test - @DisplayName("Should create order successfully") - void createOrder_Success() { - when(productRepository.findAllById(any())).thenReturn(List.of(productEntity)); - when(orderRepository.save(any())).thenReturn(order); - when(orderMapper.apply(any())).thenReturn(orderDTO); - - OrderDTO result = orderService.createOrder(1L, createOrderRequest); - - assertNotNull(result); - assertEquals(orderDTO.getId(), result.getId()); - assertEquals(orderDTO.getTotalAmount(), result.getTotalAmount()); - verify(validationService).validateCreateOrderRequest(createOrderRequest); - } - - @Test - @DisplayName("Should throw exception when product not found") - void createOrder_ProductNotFound() { - when(productRepository.findAllById(any())).thenReturn(Collections.emptyList()); - - assertThrows(ProductNotFoundException.class, - () -> orderService.createOrder(1L, createOrderRequest)); - } - } - - @Nested - @DisplayName("Update Order Status Tests") - class UpdateOrderStatusTests { - - @Test - @DisplayName("Should update order status successfully") - void updateOrderStatus_Success() { - when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); - when(orderRepository.save(any())).thenReturn(order); - - assertDoesNotThrow(() -> - orderService.updateOrderStatus(1L, OrderStatus.CONFIRMED)); - assertEquals(OrderStatus.CONFIRMED, order.getStatus()); - } - - @Test - @DisplayName("Should throw exception when order not found") - void updateOrderStatus_OrderNotFound() { - when(orderRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(OrderNotFoundException.class, - () -> orderService.updateOrderStatus(1L, OrderStatus.CONFIRMED)); - } - } - - @Nested - @DisplayName("Get Orders Tests") - class GetOrdersTests { - - @Test - @DisplayName("Should get orders by user ID") - void getOrdersByUserId_Success() { - PageRequest pageRequest = PageRequest.of(0, 10); - Page orderPage = new PageImpl<>(List.of(order)); - - when(orderRepository.findByUserId(1L, pageRequest)).thenReturn(orderPage); - when(orderMapper.apply(any())).thenReturn(orderDTO); - - Page result = orderService.getOrdersByUserId(1L, pageRequest); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - verify(orderRepository).findByUserId(1L, pageRequest); - } - - @Test - @DisplayName("Should get all orders") - void getAllOrders_Success() { - PageRequest pageRequest = PageRequest.of(0, 10); - Page orderPage = new PageImpl<>(List.of(order)); - - when(orderRepository.findAll(pageRequest)).thenReturn(orderPage); - when(orderMapper.apply(any())).thenReturn(orderDTO); - - Page result = orderService.getAllOrders(pageRequest); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - } - } - - @Nested - @DisplayName("Cancel Order Tests") - class CancelOrderTests { - - @Test - @DisplayName("Should cancel order successfully") - void cancelOrder_Success() { - when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); - doNothing().when(validationService).validateOrderCancellation(order); - when(orderRepository.save(any())).thenReturn(order); - - assertDoesNotThrow(() -> orderService.cancelOrder(1L)); - assertEquals(OrderStatus.CANCELLED, order.getStatus()); - verify(stockService).restoreStockLevels(order.getOrderLines()); - } - - @Test - @DisplayName("Should throw exception when cancelling non-existent order") - void cancelOrder_OrderNotFound() { - when(orderRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(OrderNotFoundException.class, - () -> orderService.cancelOrder(1L)); - verify(stockService, never()).restoreStockLevels(any()); - } - } - - @Nested - @DisplayName("Order Ownership Tests") - class OrderOwnershipTests { - - @Test - @DisplayName("Should verify order ownership") - void isOrderOwnedByUser_Success() { - when(orderRepository.existsByIdAndUserId(1L, 1L)).thenReturn(true); - - assertTrue(orderService.isOrderOwnedByUser(1L, 1L)); - } - - @Test - @DisplayName("Should verify order not owned by user") - void isOrderOwnedByUser_NotOwned() { - when(orderRepository.existsByIdAndUserId(1L, 2L)).thenReturn(false); - - assertFalse(orderService.isOrderOwnedByUser(1L, 2L)); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/zenfulcode/commercify/service/product/ProductServiceTest.java b/src/test/java/com/zenfulcode/commercify/service/product/ProductServiceTest.java deleted file mode 100644 index 70ff9d2..0000000 --- a/src/test/java/com/zenfulcode/commercify/service/product/ProductServiceTest.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.zenfulcode.commercify.service.product; - -import com.zenfulcode.commercify.web.dto.request.product.PriceRequest; -import com.zenfulcode.commercify.web.dto.request.product.ProductRequest; -import com.zenfulcode.commercify.domain.model.Product; -import com.zenfulcode.commercify.web.dto.common.ProductDTO; -import com.zenfulcode.commercify.web.dto.mapper.ProductMapper; -import com.zenfulcode.commercify.exception.ProductNotFoundException; -import com.zenfulcode.commercify.component.factory.ProductFactory; -import com.zenfulcode.commercify.repository.ProductRepository; -import com.zenfulcode.commercify.service.ProductDeletionService; -import com.zenfulcode.commercify.service.core.ProductService; -import com.zenfulcode.commercify.service.validations.ProductValidationService; -import com.zenfulcode.commercify.service.ProductVariantService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class ProductServiceTest { - - @Mock - private ProductRepository productRepository; - @Mock - private ProductMapper productMapper; - @Mock - private ProductFactory productFactory; - @Mock - private ProductValidationService validationService; - @Mock - private ProductVariantService variantService; - - @InjectMocks - private ProductService productService; - - private Product productEntity; - private ProductDTO productDTO; - private ProductRequest productRequest; - - @BeforeEach - void setUp() { - productEntity = com.zenfulcode.commercify.domain.model.Product.builder() - .id(1L) - .name("Test Product") - .description("Test Description") - .stock(10) - .active(true) - .imageUrl("test-image.jpg") - .currency("USD") - .unitPrice(99.99) - .build(); - - productDTO = ProductDTO.builder() - .id(1L) - .name("Test Product") - .description("Test Description") - .stock(10) - .active(true) - .imageUrl("test-image.jpg") - .currency("USD") - .unitPrice(99.99) - .variants(new ArrayList<>()) - .build(); - - productRequest = new ProductRequest( - "Test Product", - "Test Description", - 10, - "test-image.jpg", - true, - new PriceRequest("USD", 99.99), - new ArrayList<>() - ); - } - - @Nested - @DisplayName("Create Product Tests") - class CreateProductTests { - - @Test - @DisplayName("Should create product successfully") - void createProduct_Success() { - when(productFactory.createFromRequest(any())).thenReturn(productEntity); - when(productRepository.save(any())).thenReturn(productEntity); - when(productMapper.apply(any())).thenReturn(productDTO); - - ProductDTO result = productService.saveProduct(productRequest); - - assertNotNull(result); - assertEquals(productDTO.getName(), result.getName()); - assertEquals(productDTO.getUnitPrice(), result.getUnitPrice()); - verify(validationService).validateProductRequest(productRequest); - } - } - - @Nested - @DisplayName("Retrieve Product Tests") - class RetrieveProductTests { - - @Test - @DisplayName("Should get product by ID") - void getProductById_Success() { - when(productRepository.findById(1L)).thenReturn(Optional.of(productEntity)); - when(productMapper.apply(productEntity)).thenReturn(productDTO); - - ProductDTO result = productService.getProductById(1L); - - assertNotNull(result); - assertEquals(1L, result.getId()); - assertEquals("Test Product", result.getName()); - } - - @Test - @DisplayName("Should throw exception when product not found") - void getProductById_NotFound() { - when(productRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(ProductNotFoundException.class, () -> productService.getProductById(1L)); - } - - @Test - @DisplayName("Should get all active products") - void getActiveProducts_Success() { - PageRequest pageRequest = PageRequest.of(0, 10); - List products = List.of(productEntity); - Page productPage = new PageImpl<>(products); - - when(productRepository.queryAllByActiveTrue(pageRequest)).thenReturn(productPage); - when(productMapper.apply(any())).thenReturn(productDTO); - - Page result = productService.getActiveProducts(pageRequest); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - verify(productRepository).queryAllByActiveTrue(pageRequest); - } - } - - @Nested - @DisplayName("Update Product Tests") - class UpdateProductTests { - - @Test - @DisplayName("Should update product successfully") - void updateProduct_Success() { - when(productRepository.findById(1L)).thenReturn(Optional.of(productEntity)); - when(productRepository.save(any())).thenReturn(productEntity); - when(productMapper.apply(any())).thenReturn(productDTO); - - var result = productService.updateProduct(1L, productRequest); - - assertNotNull(result); - assertEquals(productDTO, result.getProduct()); - assertTrue(result.getWarnings().isEmpty()); - } - - @Test - @DisplayName("Should throw exception when updating non-existent product") - void updateProduct_NotFound() { - when(productRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(ProductNotFoundException.class, - () -> productService.updateProduct(1L, productRequest)); - } - } - - @Nested - @DisplayName("Delete Product Tests") - class DeleteProductTests { - @Mock - private ProductDeletionService productDeletionService; - - @BeforeEach - void setUp() { - productService = new ProductService( - productRepository, - productMapper, - productFactory, - validationService, - variantService, - productDeletionService - ); - } - - @Test - @DisplayName("Should delete product successfully") - void deleteProduct_Success() { - when(productRepository.findById(1L)).thenReturn(Optional.of(productEntity)); - doNothing().when(productDeletionService).validateAndDelete(productEntity); - - assertDoesNotThrow(() -> productService.deleteProduct(1L)); - verify(productDeletionService).validateAndDelete(productEntity); - } - - @Test - @DisplayName("Should throw exception when deleting non-existent product") - void deleteProduct_NotFound() { - when(productRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(ProductNotFoundException.class, () -> productService.deleteProduct(1L)); - verify(productDeletionService, never()).validateAndDelete(any()); - } - } - - @Nested - @DisplayName("Product Status Tests") - class ProductStatusTests { - - @Test - @DisplayName("Should activate product") - void activateProduct_Success() { - when(productRepository.findById(1L)).thenReturn(Optional.of(productEntity)); - - assertDoesNotThrow(() -> productService.reactivateProduct(1L)); - assertTrue(productEntity.getActive()); - verify(productRepository).save(productEntity); - } - - @Test - @DisplayName("Should deactivate product") - void deactivateProduct_Success() { - when(productRepository.findById(1L)).thenReturn(Optional.of(productEntity)); - - assertDoesNotThrow(() -> productService.deactivateProduct(1L)); - assertFalse(productEntity.getActive()); - verify(productRepository).save(productEntity); - } - } -} \ No newline at end of file From 37ccccb23e2b0c011b1a9b0c17269d73784e2f98 Mon Sep 17 00:00:00 2001 From: GustavH Date: Fri, 3 Jan 2025 00:09:44 +0100 Subject: [PATCH 03/57] Order and products services taking shape --- .../config/ApplicationConfiguration.java | 53 ----- .../config/JwtAuthenticationFilter.java | 96 --------- .../commercify/config/SecurityConfig.java | 62 ------ .../order/domain/event/OrderCreatedEvent.java | 27 +++ .../domain/event/OrderStatusChangedEvent.java | 44 ++++ .../InvalidOrderStateTransitionException.java | 28 +++ .../exception/OrderValidationException.java | 15 ++ .../commercify/order/domain/model/Order.java | 193 ++++++++++++++++++ .../order/domain/model/OrderLine.java | 77 +++++++ .../order/domain/model/OrderShippingInfo.java | 98 +++++++++ .../order/domain/model/OrderStatus.java | 12 ++ .../repository/OrderLineRepository.java | 35 ++++ .../service/DefaultOrderPricingStrategy.java | 39 ++++ .../domain/service/OrderDomainService.java | 123 +++++++++++ .../domain/service/OrderPricingStrategy.java | 13 ++ .../order/domain/service/OrderStateFlow.java | 52 +++++ .../service/OrderValidationService.java | 119 +++++++++++ .../order/domain/valueobject/Address.java | 24 +++ .../domain/valueobject/CustomerDetails.java | 20 ++ .../domain/valueobject/OrderDetails.java | 60 ++++++ .../order/domain/valueobject/OrderId.java | 30 +++ .../domain/valueobject/OrderLineDetails.java | 43 ++++ .../order/domain/valueobject/OrderLineId.java | 30 +++ .../exception/InvalidPriceException.java | 13 ++ .../exception/ProductValidationException.java | 6 +- .../product/domain/model/ProductVariant.java | 25 ++- .../DefaultProductInventoryPolicy.java | 1 - .../service/DefaultProductPricingPolicy.java | 13 +- .../domain/service/ProductDomainService.java | 78 +------ .../domain/service/ProductFactory.java | 44 ++-- .../ProductInventoryPolicy.java | 2 +- .../ProductPricingPolicy.java | 2 +- .../persistence/JpaOrderLineRepository.java | 53 +++++ .../SpringDataJpaOrderLineRepository.java | 59 ++++++ .../commercify/shared/domain/model/Money.java | 126 ++++++++++-- src/main/resources/application.properties | 2 +- 36 files changed, 1373 insertions(+), 344 deletions(-) delete mode 100644 src/main/java/com/zenfulcode/commercify/config/ApplicationConfiguration.java delete mode 100644 src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/exception/InvalidOrderStateTransitionException.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/exception/OrderValidationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderLineRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/service/DefaultOrderPricingStrategy.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/service/OrderPricingStrategy.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/service/OrderStateFlow.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/valueobject/Address.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/valueobject/CustomerDetails.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/exception/InvalidPriceException.java rename src/main/java/com/zenfulcode/commercify/product/domain/{policies => service}/ProductInventoryPolicy.java (88%) rename src/main/java/com/zenfulcode/commercify/product/domain/{policies => service}/ProductPricingPolicy.java (90%) create mode 100644 src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java diff --git a/src/main/java/com/zenfulcode/commercify/config/ApplicationConfiguration.java b/src/main/java/com/zenfulcode/commercify/config/ApplicationConfiguration.java deleted file mode 100644 index ce9cca6..0000000 --- a/src/main/java/com/zenfulcode/commercify/config/ApplicationConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.zenfulcode.commercify.config; - - -import com.zenfulcode.commercify.repository.UserRepository; -import lombok.AllArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.web.client.RestTemplate; - -@Configuration -@AllArgsConstructor -public class ApplicationConfiguration { - private final UserRepository userRepository; - - @Bean - UserDetailsService userDetailsService() { - return username -> userRepository.findByEmail(username) - .orElseThrow(() -> new BadCredentialsException("User not found")); - } - - @Bean - BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { - return config.getAuthenticationManager(); - } - - @Bean - AuthenticationProvider authenticationProvider() { - DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - - authProvider.setUserDetailsService(userDetailsService()); - authProvider.setPasswordEncoder(passwordEncoder()); - - return authProvider; - } - - @Bean - public RestTemplate restTemplate() { - return new RestTemplate(); - } -} - diff --git a/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java b/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java deleted file mode 100644 index 522c569..0000000 --- a/src/main/java/com/zenfulcode/commercify/config/JwtAuthenticationFilter.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.zenfulcode.commercify.config; - - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.ExpiredJwtException; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.AllArgsConstructor; -import lombok.NonNull; -import org.springframework.http.MediaType; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Component -@AllArgsConstructor -public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtService jwtService; - private final UserDetailsService userDetailsService; - private final ObjectMapper objectMapper; - - @Override - protected void doFilterInternal( - @NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain - ) throws ServletException, IOException { - final String authHeader = request.getHeader("Authorization"); - - try { - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - final String jwt = authHeader.substring(7); - final String userEmail = jwtService.extractUsername(jwt); - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (userEmail != null && authentication == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); - - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - - filterChain.doFilter(request, response); - } catch (ExpiredJwtException e) { - handleExpiredJwtException(response, e); - } catch (Exception e) { - handleGenericJwtException(response, e); - } - } - - private void handleExpiredJwtException(HttpServletResponse response, ExpiredJwtException e) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - - JwtErrorResponse errorResponse = new JwtErrorResponse( - "JWT token has expired", - "Token Expired" - ); - - objectMapper.writeValue(response.getOutputStream(), errorResponse); - } - - private void handleGenericJwtException(HttpServletResponse response, Exception e) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - - JwtErrorResponse errorResponse = new JwtErrorResponse( - "Invalid JWT token", - e.getMessage() - ); - - objectMapper.writeValue(response.getOutputStream(), errorResponse); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java deleted file mode 100644 index 7fd57ef..0000000 --- a/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.zenfulcode.commercify.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.retry.annotation.EnableRetry; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import java.util.List; - -@Configuration -@EnableWebSecurity -@EnableMethodSecurity -@RequiredArgsConstructor -@EnableRetry -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthFilter; - private final AuthenticationProvider authenticationProvider; - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(req -> req - .requestMatchers( - "/api/v1/auth/**", - "/api/v1/products/active", - "/api/v1/products/{id}", - "/api/v1/orders", - "/api/v1/payments/mobilepay/create", - "/api/v1/payments/stripe/create").permitAll() - .anyRequest().authenticated() - ) - .sessionManagement(smc -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authenticationProvider(authenticationProvider) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of("*")); - configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); - configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } -} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java new file mode 100644 index 0000000..1a0fc29 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java @@ -0,0 +1,27 @@ +package com.zenfulcode.commercify.order.domain.event; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import lombok.Getter; + +import java.time.Instant; + +@Getter +public class OrderCreatedEvent extends DomainEvent { + private final OrderId orderId; + private final Long userId; + private final String currency; + private final Instant createdAt; + + public OrderCreatedEvent(OrderId orderId, Long userId, String currency) { + this.orderId = orderId; + this.userId = userId; + this.currency = currency; + this.createdAt = Instant.now(); + } + + @Override + public String getEventType() { + return "ORDER_CREATED"; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java new file mode 100644 index 0000000..285182b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java @@ -0,0 +1,44 @@ +package com.zenfulcode.commercify.order.domain.event; + +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import lombok.Getter; + +import java.time.Instant; + +@Getter +public class OrderStatusChangedEvent extends DomainEvent { + private final OrderId orderId; + private final OrderStatus oldStatus; + private final OrderStatus newStatus; + private final Instant changedAt; + + public OrderStatusChangedEvent( + OrderId orderId, + OrderStatus oldStatus, + OrderStatus newStatus + ) { + this.orderId = orderId; + this.oldStatus = oldStatus; + this.newStatus = newStatus; + this.changedAt = Instant.now(); + } + + @Override + public String getEventType() { + return "ORDER_STATUS_CHANGED"; + } + + public boolean isCompletedTransition() { + return newStatus == OrderStatus.COMPLETED; + } + + public boolean isCancellationTransition() { + return newStatus == OrderStatus.CANCELLED; + } + + public boolean isShippingTransition() { + return newStatus == OrderStatus.SHIPPED; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/exception/InvalidOrderStateTransitionException.java b/src/main/java/com/zenfulcode/commercify/order/domain/exception/InvalidOrderStateTransitionException.java new file mode 100644 index 0000000..7d97adb --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/exception/InvalidOrderStateTransitionException.java @@ -0,0 +1,28 @@ +package com.zenfulcode.commercify.order.domain.exception; + +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import lombok.Getter; + +@Getter +public class InvalidOrderStateTransitionException extends DomainException { + private final OrderId orderId; + private final OrderStatus currentStatus; + private final OrderStatus targetStatus; + + public InvalidOrderStateTransitionException( + OrderId orderId, + OrderStatus currentStatus, + OrderStatus targetStatus, + String message + ) { + super(String.format("Invalid order status transition from %s to %s for order %s: %s", + currentStatus, targetStatus, orderId, message)); + + this.orderId = orderId; + this.currentStatus = currentStatus; + this.targetStatus = targetStatus; + } + +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/exception/OrderValidationException.java b/src/main/java/com/zenfulcode/commercify/order/domain/exception/OrderValidationException.java new file mode 100644 index 0000000..4651cec --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/exception/OrderValidationException.java @@ -0,0 +1,15 @@ +package com.zenfulcode.commercify.order.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainValidationException; + +import java.util.List; + +public class OrderValidationException extends DomainValidationException { + public OrderValidationException(String message) { + super(message, List.of(message)); + } + + public OrderValidationException(List violations) { + super("Order validation failed: " + String.join(", ", violations), violations); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java new file mode 100644 index 0000000..6dee077 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -0,0 +1,193 @@ +package com.zenfulcode.commercify.order.domain.model; + +import com.zenfulcode.commercify.order.domain.event.OrderCreatedEvent; +import com.zenfulcode.commercify.order.domain.event.OrderStatusChangedEvent; +import com.zenfulcode.commercify.order.domain.exception.InvalidOrderStateTransitionException; +import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; +import com.zenfulcode.commercify.shared.domain.model.Money; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "orders") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Order extends AggregateRoot { + @EmbeddedId + private OrderId id; + + @Column(name = "user_id") + private Long userId; + + @OneToMany( + mappedBy = "order", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private Set orderLines = new HashSet<>(); + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private OrderStatus status; + + private String currency; + + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "order_shipping_info_id") + private OrderShippingInfo orderShippingInfo; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "subtotal")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) + private Money subtotal; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "shipping_cost")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) + private Money shippingCost; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "tax")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) + private Money tax; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "total_amount")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) + private Money totalAmount; + + @CreationTimestamp + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private Instant updatedAt; + + // Factory method + public static Order create( + Long userId, + String currency, + OrderShippingInfo shippingInfo + ) { + Order order = new Order(); + order.id = OrderId.generate(); + order.userId = userId; + order.currency = currency; + order.status = OrderStatus.PENDING; + order.orderShippingInfo = shippingInfo; + order.totalAmount = Money.zero("USD"); + + // Register domain event + order.registerEvent(new OrderCreatedEvent( + order.getId(), + order.getUserId(), + order.getCurrency() + )); + + return order; + } + + public void addOrderLine(OrderLine orderLine) { + orderLines.add(orderLine); + orderLine.setOrder(this); + recalculateTotal(); + } + + public void removeOrderLine(OrderLine orderLine) { + orderLines.remove(orderLine); + orderLine.setOrder(null); + recalculateTotal(); + } + + public void updateStatus(OrderStatus newStatus) { + if (!canTransitionTo(newStatus)) { + throw new InvalidOrderStateTransitionException( + id, + status, + newStatus, + "Invalid order status transition" + ); + } + + OrderStatus oldStatus = this.status; + this.status = newStatus; + + registerEvent(new OrderStatusChangedEvent( + this.id, + oldStatus, + newStatus + )); + } + + public void updateTotal() { + this.totalAmount = subtotal + .add(shippingCost != null ? shippingCost : Money.zero(currency)) + .add(tax != null ? tax : Money.zero(currency)); + } + + public void setSubtotal(Money subtotal) { + validateSameCurrency(subtotal); + this.subtotal = subtotal; + } + + public void setShippingCost(Money shippingCost) { + validateSameCurrency(shippingCost); + this.shippingCost = shippingCost; + } + + public void setTax(Money tax) { + validateSameCurrency(tax); + this.tax = tax; + } + + private void validateSameCurrency(Money money) { + if (!currency.equals(money.getCurrency())) { + throw new OrderValidationException( + String.format("Currency mismatch: Expected %s but got %s", + currency, money.getCurrency()) + ); + } + } + + private boolean canTransitionTo(OrderStatus newStatus) { + return switch (status) { + case PENDING -> Set.of(OrderStatus.CONFIRMED, OrderStatus.CANCELLED).contains(newStatus); + case CONFIRMED -> Set.of(OrderStatus.SHIPPED, OrderStatus.CANCELLED).contains(newStatus); + case SHIPPED -> Set.of(OrderStatus.COMPLETED, OrderStatus.RETURNED).contains(newStatus); + default -> false; // Terminal states + }; + } + + private void recalculateTotal() { + this.totalAmount = orderLines.stream() + .map(OrderLine::getTotal) + .reduce(Money.zero(currency), Money::add); + } + + public boolean canBeCancelled() { + return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED; + } + + public boolean isCompleted() { + return status == OrderStatus.COMPLETED; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java new file mode 100644 index 0000000..f30d2e6 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java @@ -0,0 +1,77 @@ +package com.zenfulcode.commercify.order.domain.model; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderLineId; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.model.Money; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Objects; + +@Entity +@Table(name = "order_lines") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OrderLine { + @EmbeddedId + private OrderLineId id; + + @Column(name = "product_id", nullable = false) + private ProductId productId; + + @Column(nullable = false) + private Integer quantity; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "unit_price")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) + private Money unitPrice; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_variant_id") + private ProductVariant productVariant; + + @Setter + @ManyToOne(optional = false) + @JoinColumn(name = "order_id") + private Order order; + + // Factory method + public static OrderLine create( + ProductId productId, + ProductVariant variant, + Integer quantity, + Money unitPrice + ) { + OrderLine line = new OrderLine(); + line.id = OrderLineId.generate(); + line.productId = productId; + line.productVariant = variant; + line.quantity = quantity; + line.unitPrice = unitPrice; + return line; + } + + public Money getTotal() { + return unitPrice.multiply(quantity); + } + + // Equals and hashCode based on business identity + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof OrderLine that)) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java new file mode 100644 index 0000000..af0ecc0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java @@ -0,0 +1,98 @@ +package com.zenfulcode.commercify.order.domain.model; + +import com.zenfulcode.commercify.order.domain.valueobject.Address; +import com.zenfulcode.commercify.order.domain.valueobject.CustomerDetails; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "order_shipping_info") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OrderShippingInfo { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "customer_first_name") + private String customerFirstName; + + @Column(name = "customer_last_name") + private String customerLastName; + + @Column(name = "customer_email") + private String customerEmail; + + @Column(name = "customer_phone") + private String customerPhone; + + // Shipping address + @Column(name = "shipping_street", nullable = false) + private String shippingStreet; + + @Column(name = "shipping_city", nullable = false) + private String shippingCity; + + @Column(name = "shipping_state") + private String shippingState; + + @Column(name = "shipping_zip", nullable = false) + private String shippingZip; + + @Column(name = "shipping_country", nullable = false) + private String shippingCountry; + + // Billing address + @Column(name = "billing_street") + private String billingStreet; + + @Column(name = "billing_city") + private String billingCity; + + @Column(name = "billing_state") + private String billingState; + + @Column(name = "billing_zip") + private String billingZip; + + @Column(name = "billing_country") + private String billingCountry; + + public static OrderShippingInfo create( + CustomerDetails customerDetails, + Address shippingAddress, + Address billingAddress + ) { + OrderShippingInfo info = new OrderShippingInfo(); + + // Set customer details + info.customerFirstName = customerDetails.firstName(); + info.customerLastName = customerDetails.lastName(); + info.customerEmail = customerDetails.email(); + info.customerPhone = customerDetails.phone(); + + // Set shipping address + info.shippingStreet = shippingAddress.street(); + info.shippingCity = shippingAddress.city(); + info.shippingState = shippingAddress.state(); + info.shippingZip = shippingAddress.zipCode(); + info.shippingCountry = shippingAddress.country(); + + // Set billing address if provided + if (billingAddress != null) { + info.billingStreet = billingAddress.street(); + info.billingCity = billingAddress.city(); + info.billingState = billingAddress.state(); + info.billingZip = billingAddress.zipCode(); + info.billingCountry = billingAddress.country(); + } + + return info; + } + + public boolean hasBillingAddress() { + return billingStreet != null && !billingStreet.isBlank(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java new file mode 100644 index 0000000..00518c9 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.order.domain.model; + +public enum OrderStatus { + PENDING, // Order has been created but not yet confirmed + CONFIRMED, // Order has been confirmed by the customer + SHIPPED, // Order has been shipped + COMPLETED, // Order has been delivered + CANCELLED, // Order has been cancelled + FAILED, // Order has failed + REFUNDED, // Order has been refunded + RETURNED // Order has been returned +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderLineRepository.java new file mode 100644 index 0000000..68db133 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderLineRepository.java @@ -0,0 +1,35 @@ +package com.zenfulcode.commercify.order.domain.repository; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderLine; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public interface OrderLineRepository { + OrderLine save(OrderLine orderLine); + + List findByOrderId(OrderId orderId); + + Set findActiveOrdersForProduct( + ProductId productId, + Collection statuses + ); + + Set findActiveOrdersForVariant( + Long variantId, + Collection statuses + ); + + boolean hasActiveOrders( + ProductId productId + ); + + boolean hasActiveOrdersForVariant( + ProductId variantId + ); +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/DefaultOrderPricingStrategy.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/DefaultOrderPricingStrategy.java new file mode 100644 index 0000000..3f418e8 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/DefaultOrderPricingStrategy.java @@ -0,0 +1,39 @@ +package com.zenfulcode.commercify.order.domain.service; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderLine; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +public class DefaultOrderPricingStrategy implements OrderPricingStrategy { + private static final BigDecimal TAX_RATE = new BigDecimal("0.20"); // 20% tax + private static final Money FREE_SHIPPING_THRESHOLD = Money.of(new BigDecimal("100"), "USD"); + private static final Money STANDARD_SHIPPING = Money.of(new BigDecimal("10"), "USD"); + + @Override + public Money calculateSubtotal(Order order) { + return order.getOrderLines().stream() + .map(OrderLine::getTotal) + .reduce(Money.zero(order.getCurrency()), Money::add); + } + + @Override + public Money calculateShippingCost(Order order) { + Money subtotal = calculateSubtotal(order); + if (subtotal.isGreaterThanOrEqual(FREE_SHIPPING_THRESHOLD)) { + return Money.zero(order.getCurrency()); + } + return STANDARD_SHIPPING; + } + + @Override + public Money calculateTax(Order order) { + Money subtotal = calculateSubtotal(order); + return subtotal.multiply(TAX_RATE); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java new file mode 100644 index 0000000..ab2516f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -0,0 +1,123 @@ +package com.zenfulcode.commercify.order.domain.service; + +import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderLine; +import com.zenfulcode.commercify.order.domain.model.OrderShippingInfo; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.OrderDetails; +import com.zenfulcode.commercify.order.domain.valueobject.OrderLineDetails; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class OrderDomainService { + private final OrderStateFlow orderStateFlow; + private final OrderPricingStrategy pricingStrategy; + private final OrderValidationService validationService; + + public Order createOrder(OrderDetails orderDetails, List products, List variants) { + // Create order with shipping info + OrderShippingInfo shippingInfo = OrderShippingInfo.create( + orderDetails.customerDetails(), + orderDetails.shippingAddress(), + orderDetails.billingAddress() + ); + + Order order = Order.create( + orderDetails.customerId(), + orderDetails.currency(), + shippingInfo + ); + + // Map products and variants for lookup + Map productMap = products.stream() + .collect(Collectors.toMap(Product::getId, Function.identity())); + + Map variantMap = variants.stream() + .collect(Collectors.toMap(ProductVariant::getId, Function.identity())); + + // Add order lines with validation + for (OrderLineDetails lineDetails : orderDetails.orderLines()) { + Product product = productMap.get(lineDetails.productId()); + // Using validationService for stock validation + validationService.validateStock(product, lineDetails.quantity()); + + ProductVariant variant = null; + if (lineDetails.variantId() != null) { + variant = variantMap.get(lineDetails.variantId()); + validationService.validateVariant(variant, product, lineDetails); + } + + Money linePrice = calculateLinePrice(product, variant, lineDetails.quantity()); + OrderLine line = OrderLine.create( + product.getId(), + variant, + lineDetails.quantity(), + linePrice + ); + + order.addOrderLine(line); + } + + // Apply pricing + applyPricing(order); + + // Using validationService for order validation + validationService.validateCreateOrder(order); + + return order; + } + + private void applyPricing(Order order) { + Money subtotal = pricingStrategy.calculateSubtotal(order); + order.setSubtotal(subtotal); + + Money shippingCost = pricingStrategy.calculateShippingCost(order); + order.setShippingCost(shippingCost); + + Money tax = pricingStrategy.calculateTax(order); + order.setTax(tax); + + order.updateTotal(); + } + + public void updateOrderStatus(Order order, OrderStatus newStatus) { + // Using validationService for status transition validation + validationService.validateStatusTransition(order, newStatus); + + if (newStatus == OrderStatus.CANCELLED) { + validationService.validateOrderCancellation(order); + } else if (newStatus == OrderStatus.COMPLETED) { + validationService.validateOrderCompletion(order); + } + + order.updateStatus(newStatus); + } + + private Money calculateLinePrice(Product product, ProductVariant variant, int quantity) { + Money unitPrice = variant != null ? + variant.getEffectivePrice() : + product.getPrice(); + return unitPrice.multiply(quantity); + } + + private void validateOrderState(Order order) { + if (order.getOrderLines().isEmpty()) { + throw new OrderValidationException("Order must contain at least one item"); + } + if (order.getTotalAmount().isLessThanOrEqual(Money.zero(order.getCurrency()))) { + throw new OrderValidationException("Order total must be greater than zero"); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderPricingStrategy.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderPricingStrategy.java new file mode 100644 index 0000000..ed6c93c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderPricingStrategy.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.order.domain.service; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.shared.domain.model.Money; + +public interface OrderPricingStrategy { + Money calculateSubtotal(Order order); + + Money calculateShippingCost(Order order); + + Money calculateTax(Order order); +} + diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderStateFlow.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderStateFlow.java new file mode 100644 index 0000000..799c50f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderStateFlow.java @@ -0,0 +1,52 @@ +package com.zenfulcode.commercify.order.domain.service; + +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.Set; + +@Component +public class OrderStateFlow { + private final EnumMap> validTransitions; + + public OrderStateFlow() { + validTransitions = new EnumMap<>(OrderStatus.class); + + // Initial state -> Confirmed or Cancelled + validTransitions.put(OrderStatus.PENDING, Set.of( + OrderStatus.CONFIRMED, + OrderStatus.CANCELLED + )); + + // Confirmed -> Shipped or Cancelled + validTransitions.put(OrderStatus.CONFIRMED, Set.of( + OrderStatus.SHIPPED, + OrderStatus.CANCELLED + )); + + // Shipped -> Completed or Returned + validTransitions.put(OrderStatus.SHIPPED, Set.of( + OrderStatus.COMPLETED, + OrderStatus.RETURNED + )); + + // Terminal states + validTransitions.put(OrderStatus.COMPLETED, Set.of()); + validTransitions.put(OrderStatus.CANCELLED, Set.of()); + validTransitions.put(OrderStatus.RETURNED, Set.of()); + validTransitions.put(OrderStatus.REFUNDED, Set.of()); + } + + public boolean canTransition(OrderStatus currentState, OrderStatus newState) { + return validTransitions.get(currentState).contains(newState); + } + + public Set getValidTransitions(OrderStatus currentState) { + return validTransitions.get(currentState); + } + + public boolean isTerminalState(OrderStatus state) { + return validTransitions.get(state).isEmpty(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java new file mode 100644 index 0000000..fb1e9d8 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java @@ -0,0 +1,119 @@ +package com.zenfulcode.commercify.order.domain.service; + +import com.zenfulcode.commercify.order.domain.exception.InvalidOrderStateTransitionException; +import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderLine; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.OrderLineDetails; +import com.zenfulcode.commercify.product.domain.exception.InsufficientStockException; +import com.zenfulcode.commercify.product.domain.exception.VariantNotFoundException; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class OrderValidationService { + private final OrderStateFlow orderStateFlow; + + public void validateCreateOrder(Order order) { + List violations = new ArrayList<>(); + + if (order.getOrderLines().isEmpty()) { + violations.add("Order must contain at least one item"); + } + + if (order.getCurrency() == null || order.getCurrency().isBlank()) { + violations.add("Currency is required"); + } + + if (order.getOrderShippingInfo() == null) { + violations.add("Shipping information is required"); + } + + validateOrderLines(order.getOrderLines(), violations); + + if (!violations.isEmpty()) { + throw new OrderValidationException(violations); + } + } + + private void validateOrderLines(Set orderLines, List violations) { + for (OrderLine line : orderLines) { + if (line.getQuantity() <= 0) { + violations.add("Quantity must be greater than 0 for product: " + line.getProductId()); + } + if (!line.getUnitPrice().isPositive()) { + violations.add("Unit price must be greater than 0 for product: " + line.getProductId()); + } + } + } + + public void validateStatusTransition(Order order, OrderStatus newStatus) { + if (!orderStateFlow.canTransition(order.getStatus(), newStatus)) { + throw new InvalidOrderStateTransitionException( + order.getId(), + order.getStatus(), + newStatus, + "Invalid status transition" + ); + } + } + + public void validateOrderCancellation(Order order) { + if (!order.canBeCancelled()) { + throw new OrderValidationException( + "Cannot cancel order in status: " + order.getStatus() + ); + } + } + + public void validateOrderCompletion(Order order) { + if (order.getStatus() != OrderStatus.SHIPPED) { + throw new OrderValidationException( + "Cannot complete order that hasn't been shipped" + ); + } + } + + public void validateStock(Product product, int requestedQuantity) { + if (!product.hasEnoughStock(requestedQuantity)) { + throw new InsufficientStockException( + product.getId(), + requestedQuantity, + product.getStock() + ); + } + } + + public void validateVariant(ProductVariant variant, Product product, OrderLineDetails lineDetails) { + List violations = new ArrayList<>(); + + if (variant == null) { + throw new VariantNotFoundException(lineDetails.variantId()); + } + + if (!variant.belongsTo(product)) { + violations.add(String.format("Variant %s does not belong to product %s", + variant.getId(), product.getId())); + } + + if (!variant.hasEnoughStock(lineDetails.quantity())) { + throw new InsufficientStockException( + variant.getId(), + lineDetails.quantity(), + variant.getStock() + ); + } + + if (!violations.isEmpty()) { + throw new OrderValidationException(violations); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/Address.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/Address.java new file mode 100644 index 0000000..6141c73 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/Address.java @@ -0,0 +1,24 @@ +package com.zenfulcode.commercify.order.domain.valueobject; + +public record Address( + String street, + String city, + String state, + String zipCode, + String country +) { + public Address { + if (street == null || street.isBlank()) { + throw new IllegalArgumentException("Street is required"); + } + if (city == null || city.isBlank()) { + throw new IllegalArgumentException("City is required"); + } + if (zipCode == null || zipCode.isBlank()) { + throw new IllegalArgumentException("Zip code is required"); + } + if (country == null || country.isBlank()) { + throw new IllegalArgumentException("Country is required"); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/CustomerDetails.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/CustomerDetails.java new file mode 100644 index 0000000..91f17ac --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/CustomerDetails.java @@ -0,0 +1,20 @@ +package com.zenfulcode.commercify.order.domain.valueobject; + +public record CustomerDetails( + String firstName, + String lastName, + String email, + String phone +) { + public CustomerDetails { + if (firstName == null || firstName.isBlank()) { + throw new IllegalArgumentException("First name is required"); + } + if (lastName == null || lastName.isBlank()) { + throw new IllegalArgumentException("Last name is required"); + } + if (email == null || email.isBlank()) { + throw new IllegalArgumentException("Email is required"); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java new file mode 100644 index 0000000..0d68a3c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java @@ -0,0 +1,60 @@ +package com.zenfulcode.commercify.order.domain.valueobject; + +import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; +import com.zenfulcode.commercify.shared.domain.model.Money; + +import java.util.ArrayList; +import java.util.List; + +public record OrderDetails( + Long customerId, + String currency, + CustomerDetails customerDetails, + Address shippingAddress, + Address billingAddress, + List orderLines +) { + public OrderDetails { + validate(customerId, currency, customerDetails, shippingAddress, orderLines); + } + + private void validate( + Long customerId, + String currency, + CustomerDetails customerDetails, + Address shippingAddress, + List orderLines + ) { + List violations = new ArrayList<>(); + + if (customerId == null) { + violations.add("Customer ID is required"); + } + + if (currency == null || currency.isBlank()) { + violations.add("Currency is required"); + } + + if (customerDetails == null) { + violations.add("Customer details are required"); + } + + if (shippingAddress == null) { + violations.add("Shipping address is required"); + } + + if (orderLines == null || orderLines.isEmpty()) { + violations.add("Order must contain at least one item"); + } + + if (!violations.isEmpty()) { + throw new OrderValidationException(violations); + } + } + + public Money calculateSubtotal() { + return orderLines.stream() + .map(OrderLineDetails::calculateTotal) + .reduce(Money.zero(currency), Money::add); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java new file mode 100644 index 0000000..655d828 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java @@ -0,0 +1,30 @@ +package com.zenfulcode.commercify.order.domain.valueobject; + +import jakarta.persistence.Embeddable; +import lombok.Value; + +import java.util.Objects; +import java.util.UUID; + +@Value +@Embeddable +public class OrderId { + String value; + + private OrderId(String value) { + this.value = Objects.requireNonNull(value); + } + + public static OrderId generate() { + return new OrderId(UUID.randomUUID().toString()); + } + + public static OrderId of(String value) { + return new OrderId(value); + } + + // Required by JPA + protected OrderId() { + this.value = null; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java new file mode 100644 index 0000000..a10e50d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java @@ -0,0 +1,43 @@ +package com.zenfulcode.commercify.order.domain.valueobject; + +import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.model.Money; + +import java.util.ArrayList; +import java.util.List; + +public record OrderLineDetails( + ProductId productId, + ProductId variantId, + int quantity, + Money unitPrice +) { + public OrderLineDetails { + validate(productId, quantity); + } + + private void validate(ProductId productId, int quantity) { + List violations = new ArrayList<>(); + + if (productId == null) { + violations.add("Product ID is required"); + } + + if (quantity <= 0) { + violations.add("Quantity must be greater than zero"); + } + + if (!violations.isEmpty()) { + throw new OrderValidationException(violations); + } + } + + public Money calculateTotal() { + return unitPrice.multiply(quantity); + } + + public boolean hasVariant() { + return variantId != null; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java new file mode 100644 index 0000000..f5c58e2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java @@ -0,0 +1,30 @@ +package com.zenfulcode.commercify.order.domain.valueobject; + +import jakarta.persistence.Embeddable; +import lombok.Value; + +import java.util.Objects; +import java.util.UUID; + +@Value +@Embeddable +public class OrderLineId { + String value; + + private OrderLineId(String value) { + this.value = Objects.requireNonNull(value); + } + + public static OrderLineId generate() { + return new OrderLineId(UUID.randomUUID().toString()); + } + + public static OrderLineId of(String value) { + return new OrderLineId(value); + } + + // Required by JPA + protected OrderLineId() { + this.value = null; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/exception/InvalidPriceException.java b/src/main/java/com/zenfulcode/commercify/product/domain/exception/InvalidPriceException.java new file mode 100644 index 0000000..0f2cbc0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/exception/InvalidPriceException.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.product.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; + +public class InvalidPriceException extends DomainException { + public InvalidPriceException(String message) { + super(message); + } + + public InvalidPriceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductValidationException.java b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductValidationException.java index 225663f..4092a96 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductValidationException.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/exception/ProductValidationException.java @@ -5,7 +5,11 @@ import java.util.List; public class ProductValidationException extends DomainValidationException { + public ProductValidationException(String message) { + super(message, List.of(message)); + } + public ProductValidationException(List violations) { - super("Product validation failed", violations); + super("Product validation failed: " + String.join(", ", violations), violations); } } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java index 42d38d6..e594c64 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java @@ -1,5 +1,6 @@ package com.zenfulcode.commercify.product.domain.model; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.model.Money; import jakarta.persistence.*; import lombok.*; @@ -16,9 +17,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class ProductVariant { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @EmbeddedId + private ProductId id; @Column(nullable = false, unique = true) private String sku; @@ -45,6 +45,7 @@ public class ProductVariant { // Factory method public static ProductVariant create(String sku, Integer stock, Money price) { ProductVariant variant = new ProductVariant(); + variant.id = ProductId.generate(); variant.sku = Objects.requireNonNull(sku, "SKU is required"); variant.stock = stock; variant.price = price; @@ -64,18 +65,30 @@ public void addOption(String name, String value) { } public boolean hasActiveOrders() { - // This would typically check a repository or domain service + // TODO: This would typically check a repository or domain service return false; } - public Money getEffectivePrice() { - return price != null ? price : product.getPrice(); + public boolean belongsTo(Product product) { + return this.product.getId().equals(product.getId()); + } + + public boolean hasEnoughStock(int requestedQuantity) { + if (stock == null) { + // If variant doesn't manage its own stock, delegate to product + return product.hasEnoughStock(requestedQuantity); + } + return stock >= requestedQuantity; } public int getEffectiveStock() { return stock != null ? stock : product.getStock(); } + public Money getEffectivePrice() { + return price != null ? price : product.getPrice(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java index fe635ae..068f9c5 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java @@ -4,7 +4,6 @@ import com.zenfulcode.commercify.product.domain.event.LowStockEvent; import com.zenfulcode.commercify.product.domain.event.StockCorrectionEvent; import com.zenfulcode.commercify.product.domain.model.Product; -import com.zenfulcode.commercify.product.domain.policies.ProductInventoryPolicy; import com.zenfulcode.commercify.product.domain.valueobject.InventoryAdjustment; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductPricingPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductPricingPolicy.java index ce721af..d243499 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductPricingPolicy.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductPricingPolicy.java @@ -1,8 +1,8 @@ package com.zenfulcode.commercify.product.domain.service; +import com.zenfulcode.commercify.product.domain.exception.InvalidPriceException; import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.model.ProductVariant; -import com.zenfulcode.commercify.product.domain.policies.ProductPricingPolicy; import com.zenfulcode.commercify.product.domain.valueobject.VariantSpecification; import com.zenfulcode.commercify.shared.domain.model.Money; import lombok.RequiredArgsConstructor; @@ -20,8 +20,7 @@ public class DefaultProductPricingPolicy implements ProductPricingPolicy { @Override public void applyDefaultPricing(Product product) { // Apply minimum margin check - if (product.getPrice().getAmount() - .compareTo(calculateMinimumPrice(product)) < 0) { + if (product.getPrice().isLessThan(calculateMinimumPrice(product))) { throw new InvalidPriceException("Price does not meet minimum margin requirements"); } } @@ -41,18 +40,20 @@ public Money calculateVariantPrice(Product product, VariantSpecification spec) { return spec.price(); } + + // TODO: Rework this method @Override public Money validateAndAdjustPrice(Product product, ProductVariant variant, Money newPrice) { // Ensure variant price meets minimum margin - if (newPrice.getAmount().compareTo(calculateMinimumPrice(product)) < 0) { + if (newPrice.isLessThan(calculateMinimumPrice(product))) { throw new InvalidPriceException("Variant price does not meet minimum margin requirements"); } return newPrice; } + // TODO: Rework this method private Money calculateMinimumPrice(Product product) { - // Implementation of minimum price calculation based on costs and margin - return Money.of(BigDecimal.TEN, product.getPrice().getCurrency()); // Simplified example + return product.getPrice().multiply(MINIMUM_MARGIN_PERCENTAGE); } } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java index 4978633..92ed4fe 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java @@ -1,10 +1,9 @@ package com.zenfulcode.commercify.product.domain.service; -import com.zenfulcode.commercify.product.domain.exception.ProductValidationException; +import com.zenfulcode.commercify.order.domain.repository.OrderLineRepository; import com.zenfulcode.commercify.product.domain.exception.VariantNotFoundException; -import com.zenfulcode.commercify.product.domain.model.*; -import com.zenfulcode.commercify.product.domain.policies.ProductInventoryPolicy; -import com.zenfulcode.commercify.product.domain.policies.ProductPricingPolicy; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; import com.zenfulcode.commercify.product.domain.valueobject.*; import com.zenfulcode.commercify.shared.domain.model.Money; import lombok.RequiredArgsConstructor; @@ -19,52 +18,20 @@ public class ProductDomainService { private final OrderLineRepository orderLineRepository; private final ProductInventoryPolicy inventoryPolicy; private final ProductPricingPolicy pricingPolicy; - private final SkuGenerator skuGenerator; private final ProductFactory productFactory; /** * Creates a new product with validation and enrichment */ public Product createProduct(ProductSpecification spec) { - validateProductSpecification(spec); - - Product product = productFactory.createProduct(spec); - - // Apply any default product policies - pricingPolicy.applyDefaultPricing(product); - inventoryPolicy.initializeInventory(product); - - // Create variants if specified - if (spec.hasVariants()) { - createProductVariants(product, spec.variantSpecs()); - } - - return product; + return productFactory.createProduct(spec); } /** * Handles complex variant creation logic */ public void createProductVariants(Product product, List variantSpecs) { - for (VariantSpecification spec : variantSpecs) { - validateVariantSpecification(spec); - - String sku = skuGenerator.generateSku(product, spec); - Money variantPrice = pricingPolicy.calculateVariantPrice(product, spec); - - ProductVariant variant = ProductVariant.create( - sku, - spec.stock(), - variantPrice - ); - - // Add variant options - spec.options().forEach(option -> - variant.addOption(option.name(), option.value()) - ); - - product.addVariant(variant); - } + productFactory.createVariants(product, variantSpecs); } /** @@ -135,39 +102,6 @@ public void updateVariantPrices(Product product, List update } } - private void validateProductSpecification(ProductSpecification spec) { - List violations = new ArrayList<>(); - - if (spec.name() == null || spec.name().isBlank()) { - violations.add("Product name is required"); - } - if (spec.price() == null || spec.price().isNegative()) { - violations.add("Valid product price is required"); - } - if (spec.initialStock() < 0) { - violations.add("Initial stock cannot be negative"); - } - - if (!violations.isEmpty()) { - throw new ProductValidationException(violations); - } - } - - private void validateVariantSpecification(VariantSpecification spec) { - List violations = new ArrayList<>(); - - if (spec.options() == null || spec.options().isEmpty()) { - violations.add("Variant must have at least one option"); - } - if (spec.stock() != null && spec.stock() < 0) { - violations.add("Variant stock cannot be negative"); - } - - if (!violations.isEmpty()) { - throw new ProductValidationException(violations); - } - } - private void validateInventoryAdjustment(InventoryAdjustment adjustment) { if (adjustment.quantity() < 0) { throw new IllegalArgumentException("Adjustment quantity cannot be negative"); @@ -185,7 +119,7 @@ private void validatePriceUpdates(List updates) { } public void updateProduct(Product product, ProductUpdateSpec productUpdateSpec) { - + // TODO: Implement product update logic } } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java index 038b1c1..dc267dd 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java @@ -38,8 +38,10 @@ public Product createProduct(ProductSpecification spec) { return product; } - private void createVariants(Product product, List specs) { + public void createVariants(Product product, List specs) { for (VariantSpecification spec : specs) { + validateVariantSpecification(spec); + String sku = skuGenerator.generateSku(product, spec); Money variantPrice = pricingPolicy.calculateVariantPrice(product, spec); @@ -59,6 +61,21 @@ private void createVariants(Product product, List specs) { } } + private void validateVariantSpecification(VariantSpecification spec) { + List violations = new ArrayList<>(); + + if (spec.options() == null || spec.options().isEmpty()) { + violations.add("Variant must have at least one option"); + } + if (spec.stock() != null && spec.stock() < 0) { + violations.add("Variant stock cannot be negative"); + } + + if (!violations.isEmpty()) { + throw new ProductValidationException(violations); + } + } + private void validateSpecification(ProductSpecification spec) { List violations = new ArrayList<>(); @@ -76,29 +93,4 @@ private void validateSpecification(ProductSpecification spec) { throw new ProductValidationException(violations); } } - - public ProductVariant createVariantFromTemplate( - Product product, - ProductVariant template, - VariantSpecification spec) { - - String sku = skuGenerator.generateSku(product, spec); - Money variantPrice = template.getPrice(); - if (spec.price() != null) { - variantPrice = pricingPolicy.calculateVariantPrice(product, spec); - } - - ProductVariant variant = ProductVariant.builder() - .sku(sku) - .stock(spec.stock()) - .price(variantPrice) - .imageUrl(spec.imageUrl() != null ? spec.imageUrl() : template.getImageUrl()) - .build(); - - spec.options().forEach(option -> - variant.addOption(option.name(), option.value()) - ); - - return variant; - } } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductInventoryPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductInventoryPolicy.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductInventoryPolicy.java rename to src/main/java/com/zenfulcode/commercify/product/domain/service/ProductInventoryPolicy.java index f3b4cdb..ef8dbba 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductInventoryPolicy.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductInventoryPolicy.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.product.domain.policies; +package com.zenfulcode.commercify.product.domain.service; import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.valueobject.InventoryAdjustment; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductPricingPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductPricingPolicy.java similarity index 90% rename from src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductPricingPolicy.java rename to src/main/java/com/zenfulcode/commercify/product/domain/service/ProductPricingPolicy.java index e0cba62..fd34c74 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/policies/ProductPricingPolicy.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductPricingPolicy.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.product.domain.policies; +package com.zenfulcode.commercify.product.domain.service; import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.model.ProductVariant; diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java new file mode 100644 index 0000000..9e3b978 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java @@ -0,0 +1,53 @@ +package com.zenfulcode.commercify.product.infrastructure.persistence; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderLine; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.repository.OrderLineRepository; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +@Repository +@RequiredArgsConstructor +public class JpaOrderLineRepository implements OrderLineRepository { + + private final SpringDataJpaOrderLineRepository repository; + + @Override + public OrderLine save(OrderLine orderLine) { + return repository.save(orderLine); + } + + @Override + public List findByOrderId(OrderId orderId) { + return repository.findByOrderId(orderId); + } + + @Override + public Set findActiveOrdersForProduct( + ProductId productId, Collection statuses) { + return repository.findActiveOrdersForProduct(productId.getValue(), statuses); + } + + @Override + public Set findActiveOrdersForVariant( + Long variantId, Collection statuses) { + return repository.findActiveOrdersForVariant(variantId, statuses); + } + + @Override + public boolean hasActiveOrders(ProductId productId) { + return repository.hasActiveOrders(productId.getValue()); + } + + @Override + public boolean hasActiveOrdersForVariant(ProductId variantId) { + return repository.hasActiveOrdersForVariant(variantId); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java new file mode 100644 index 0000000..0ce216f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java @@ -0,0 +1,59 @@ +package com.zenfulcode.commercify.product.infrastructure.persistence; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderLine; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +@Repository +interface SpringDataJpaOrderLineRepository extends JpaRepository { + + List findByOrderId(OrderId orderId); + + @Query(""" + SELECT DISTINCT ol.order FROM OrderLine ol + WHERE ol.productId = :productId + AND ol.order.status IN :statuses + """) + Set findActiveOrdersForProduct( + @Param("productId") String productId, + @Param("statuses") Collection statuses + ); + + @Query(""" + SELECT DISTINCT ol.order FROM OrderLine ol + JOIN ol.productVariant v + WHERE v.id = :variantId + AND ol.order.status IN :statuses + """) + Set findActiveOrdersForVariant( + @Param("variantId") Long variantId, + @Param("statuses") Collection statuses + ); + + @Query(""" + SELECT COUNT(ol) > 0 FROM OrderLine ol + WHERE ol.productId = :productId + """) + boolean hasActiveOrders( + @Param("productId") String productId + ); + + @Query(""" + SELECT COUNT(ol) > 0 FROM OrderLine ol + JOIN ol.productVariant v + WHERE v.id = :variantId + """) + boolean hasActiveOrdersForVariant( + @Param("variantId") ProductId variantId + ); +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/model/Money.java b/src/main/java/com/zenfulcode/commercify/shared/domain/model/Money.java index f2a5e3d..f9cf37a 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/model/Money.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/model/Money.java @@ -1,45 +1,135 @@ package com.zenfulcode.commercify.shared.domain.model; import jakarta.persistence.Embeddable; -import lombok.Value; -import org.apache.commons.lang3.NotImplementedException; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; @Embeddable -@Value +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Money { - BigDecimal amount; - String currency; + private BigDecimal amount; + private String currency; public Money(BigDecimal amount, String currency) { - this.amount = amount; - this.currency = currency; + this.amount = Objects.requireNonNull(amount).setScale(2, RoundingMode.HALF_UP); + this.currency = Objects.requireNonNull(currency); + validate(); } public Money(double amount, String currency) { - this.amount = BigDecimal.valueOf(amount); - this.currency = currency; + this(BigDecimal.valueOf(amount), currency); } - public Money() { - this.amount = new BigDecimal(0); - this.currency = "USD"; + private void validate() { + if (currency == null || currency.isBlank()) { + throw new IllegalArgumentException("Currency is required"); + } + if (currency.length() != 3) { + throw new IllegalArgumentException("Currency must be a 3-letter ISO code"); + } + } + + public static Money zero(String currency) { + return new Money(BigDecimal.ZERO, currency); + } + + public static Money of(BigDecimal amount, String currency) { + return new Money(amount, currency); + } + + public static Money of(double amount, String currency) { + return new Money(BigDecimal.valueOf(amount), currency); } public Money add(Money other) { - if (!this.currency.equals(other.currency)) { - throw new IllegalArgumentException("Cannot add different currencies"); - } + validateSameCurrency(other); return new Money(this.amount.add(other.amount), this.currency); } - public Money multiply(int quantity) { - return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency); + public Money subtract(Money other) { + validateSameCurrency(other); + return new Money(this.amount.subtract(other.amount), this.currency); + } + + public Money multiply(BigDecimal multiplier) { + return new Money(this.amount.multiply(multiplier), this.currency); + } + + public Money multiply(int multiplier) { + return multiply(BigDecimal.valueOf(multiplier)); + } + + public Money multiply(double multiplier) { + return multiply(BigDecimal.valueOf(multiplier)); + } + + public Money percentage(BigDecimal percentage) { + BigDecimal multiplier = percentage.divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP); + return multiply(multiplier); + } + + public boolean isGreaterThan(Money other) { + validateSameCurrency(other); + return this.amount.compareTo(other.amount) > 0; + } + + public boolean isGreaterThanOrEqual(Money other) { + validateSameCurrency(other); + return this.amount.compareTo(other.amount) >= 0; + } + + public boolean isLessThan(Money other) { + validateSameCurrency(other); + return this.amount.compareTo(other.amount) < 0; + } + + public boolean isLessThanOrEqual(Money other) { + validateSameCurrency(other); + return this.amount.compareTo(other.amount) <= 0; + } + + public boolean isZero() { + return amount.compareTo(BigDecimal.ZERO) == 0; + } + + public boolean isPositive() { + return amount.compareTo(BigDecimal.ZERO) > 0; } public boolean isNegative() { - throw new NotImplementedException("is negative has not been implemented"); + return amount.compareTo(BigDecimal.ZERO) < 0; } + private void validateSameCurrency(Money other) { + if (!this.currency.equals(other.currency)) { + throw new IllegalArgumentException( + String.format("Currency mismatch: %s vs %s", this.currency, other.currency) + ); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Money money = (Money) o; + return amount.compareTo(money.amount) == 0 && + currency.equals(money.currency); + } + + @Override + public int hashCode() { + return Objects.hash(amount, currency); + } + + @Override + public String toString() { + return amount.toString() + " " + currency; + } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4807a31..a069488 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ spring.application.name=commercify server.port=6091 # Database configuration -spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATABASE_NAME:commercifydb}?createDatabaseIfNotExist=true +spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATABASE_NAME:commercify_ddd_db}?createDatabaseIfNotExist=true spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver From 25281fa8fe1368715debbf7a98407ed39172823e Mon Sep 17 00:00:00 2001 From: GustavH Date: Fri, 3 Jan 2025 12:16:18 +0100 Subject: [PATCH 04/57] Application runs, but many hibernate database issues --- .../api/product/dto/ProductDtoMapper.java | 2 +- .../dto/response/ProductDetailResponse.java | 4 +- .../order/domain/event/OrderCreatedEvent.java | 2 + .../domain/event/OrderStatusChangedEvent.java | 2 + .../commercify/order/domain/model/Order.java | 13 ++-- .../order/domain/model/OrderLine.java | 22 ++----- .../repository/OrderLineRepository.java | 5 +- .../order/domain/valueobject/OrderId.java | 38 ++++++++---- .../domain/valueobject/OrderLineDetails.java | 3 +- .../order/domain/valueobject/OrderLineId.java | 38 ++++++++---- .../domain/event/LargeStockIncreaseEvent.java | 2 + .../product/domain/event/LowStockEvent.java | 2 + .../domain/event/ProductCreatedEvent.java | 2 + .../domain/event/StockCorrectionEvent.java | 2 + .../product/domain/model/Product.java | 5 ++ .../product/domain/model/ProductVariant.java | 6 +- .../domain/valueobject/CategoryId.java | 40 +++++++++---- .../product/domain/valueobject/ProductId.java | 40 +++++++++---- .../product/domain/valueobject/VariantId.java | 48 +++++++++++++++ .../persistence/JpaOrderLineRepository.java | 9 +-- .../persistence/JpaProductRepository.java | 12 ++-- .../SpringDataJpaOrderLineRepository.java | 9 +-- .../SpringDataJpaProductRepository.java | 12 ++-- .../domain/event/AggregateReference.java | 57 ++++++++++++++++++ .../shared/domain/event/DomainEvent.java | 2 +- .../domain/event/DomainEventHandler.java | 4 +- .../shared/domain/model/AggregateRoot.java | 3 +- .../shared/domain/model/StoredEvent.java | 10 +++- .../service/DefaultDomainEventPublisher.java | 4 +- .../domain/valueobject/AggregateId.java | 11 ++++ .../persistence/EnhancedEventStore.java | 60 ------------------- .../persistence/JpaDomainEventStore.java | 56 ++++++++--------- .../persistence/ProductIdConverter.java | 19 ------ .../service/EventSerializer.java | 7 +-- .../service/EventTypeResolver.java | 27 +++++++-- src/main/resources/application.properties | 3 +- 36 files changed, 362 insertions(+), 219 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/event/AggregateReference.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/valueobject/AggregateId.java delete mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EnhancedEventStore.java delete mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/ProductIdConverter.java rename src/main/java/com/zenfulcode/commercify/shared/{domain => infrastructure}/service/EventSerializer.java (91%) rename src/main/java/com/zenfulcode/commercify/shared/{domain => infrastructure}/service/EventTypeResolver.java (50%) diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java index 7312ce5..d6b17c2 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java @@ -115,7 +115,7 @@ private ProductSummaryResponse.ProductPriceResponse toPriceResponse(Money price) public ProductDetailResponse toDetailResponse(Product product) { return new ProductDetailResponse( - product.getId().getValue(), + product.getId(), product.getName(), product.getDescription(), product.getStock(), diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java index fe35a1c..72088a0 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java @@ -1,9 +1,11 @@ package com.zenfulcode.commercify.api.product.dto.response; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; + import java.util.List; public record ProductDetailResponse( - String id, + ProductId id, String name, String description, int stock, diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java index 1a0fc29..39b8d4a 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java @@ -2,12 +2,14 @@ import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; import lombok.Getter; import java.time.Instant; @Getter public class OrderCreatedEvent extends DomainEvent { + @AggregateId private final OrderId orderId; private final Long userId; private final String currency; diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java index 285182b..089ffca 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java @@ -3,12 +3,14 @@ import com.zenfulcode.commercify.order.domain.model.OrderStatus; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; import lombok.Getter; import java.time.Instant; @Getter public class OrderStatusChangedEvent extends DomainEvent { + @AggregateId private final OrderId orderId; private final OrderStatus oldStatus; private final OrderStatus newStatus; diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java index 6dee077..83f17b2 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -40,37 +40,38 @@ public class Order extends AggregateRoot { @Column(nullable = false) private OrderStatus status; - private String currency; - @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "order_shipping_info_id") private OrderShippingInfo orderShippingInfo; + @Column(name = "currency") + private String currency; + @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "subtotal")), - @AttributeOverride(name = "currency", column = @Column(name = "currency")) + @AttributeOverride(name = "currency", column = @Column(name = "currency", insertable = false, updatable = false)) }) private Money subtotal; @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "shipping_cost")), - @AttributeOverride(name = "currency", column = @Column(name = "currency")) + @AttributeOverride(name = "currency", column = @Column(name = "currency", insertable = false, updatable = false)) }) private Money shippingCost; @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "tax")), - @AttributeOverride(name = "currency", column = @Column(name = "currency")) + @AttributeOverride(name = "currency", column = @Column(name = "currency", insertable = false, updatable = false)) }) private Money tax; @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "total_amount")), - @AttributeOverride(name = "currency", column = @Column(name = "currency")) + @AttributeOverride(name = "currency", column = @Column(name = "currency", insertable = false, updatable = false)) }) private Money totalAmount; diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java index f30d2e6..1879984 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java @@ -10,8 +10,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.Objects; - @Entity @Table(name = "order_lines") @Getter @@ -26,10 +24,13 @@ public class OrderLine { @Column(nullable = false) private Integer quantity; + @Column(name = "currency") + private String currency; + @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "unit_price")), - @AttributeOverride(name = "currency", column = @Column(name = "currency")) + @AttributeOverride(name = "currency", column = @Column(name = "currency", insertable = false, updatable = false)) }) private Money unitPrice; @@ -42,7 +43,6 @@ public class OrderLine { @JoinColumn(name = "order_id") private Order order; - // Factory method public static OrderLine create( ProductId productId, ProductVariant variant, @@ -55,23 +55,11 @@ public static OrderLine create( line.productVariant = variant; line.quantity = quantity; line.unitPrice = unitPrice; + line.currency = unitPrice.getCurrency(); return line; } public Money getTotal() { return unitPrice.multiply(quantity); } - - // Equals and hashCode based on business identity - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof OrderLine that)) return false; - return Objects.equals(id, that.id); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderLineRepository.java index 68db133..680d7ef 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderLineRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderLineRepository.java @@ -5,6 +5,7 @@ import com.zenfulcode.commercify.order.domain.model.OrderStatus; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import java.util.Collection; import java.util.List; @@ -21,7 +22,7 @@ Set findActiveOrdersForProduct( ); Set findActiveOrdersForVariant( - Long variantId, + VariantId variantId, Collection statuses ); @@ -30,6 +31,6 @@ boolean hasActiveOrders( ); boolean hasActiveOrdersForVariant( - ProductId variantId + VariantId variantId ); } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java index 655d828..dc77c27 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java @@ -1,30 +1,48 @@ package com.zenfulcode.commercify.order.domain.valueobject; +import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import lombok.Value; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.Objects; import java.util.UUID; -@Value @Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderId { - String value; + @Column(name = "id") + private String id; - private OrderId(String value) { - this.value = Objects.requireNonNull(value); + private OrderId(String id) { + this.id = Objects.requireNonNull(id); } public static OrderId generate() { return new OrderId(UUID.randomUUID().toString()); } - public static OrderId of(String value) { - return new OrderId(value); + public static OrderId of(String id) { + return new OrderId(id); } - // Required by JPA - protected OrderId() { - this.value = null; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OrderId that = (OrderId) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java index a10e50d..93eb85c 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java @@ -2,6 +2,7 @@ import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import com.zenfulcode.commercify.shared.domain.model.Money; import java.util.ArrayList; @@ -9,7 +10,7 @@ public record OrderLineDetails( ProductId productId, - ProductId variantId, + VariantId variantId, int quantity, Money unitPrice ) { diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java index f5c58e2..3e5c718 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java @@ -1,30 +1,48 @@ package com.zenfulcode.commercify.order.domain.valueobject; +import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import lombok.Value; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.Objects; import java.util.UUID; -@Value @Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderLineId { - String value; + @Column(name = "id") + private String id; - private OrderLineId(String value) { - this.value = Objects.requireNonNull(value); + private OrderLineId(String id) { + this.id = Objects.requireNonNull(id); } public static OrderLineId generate() { return new OrderLineId(UUID.randomUUID().toString()); } - public static OrderLineId of(String value) { - return new OrderLineId(value); + public static OrderLineId of(String id) { + return new OrderLineId(id); } - // Required by JPA - protected OrderLineId() { - this.value = null; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OrderLineId that = (OrderLineId) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java index cb4488c..3229b10 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java @@ -2,10 +2,12 @@ import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; import lombok.Getter; @Getter public class LargeStockIncreaseEvent extends DomainEvent { + @AggregateId private final ProductId productId; private final int quantity; private final String reason; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java index 30dcc18..f8f5ae5 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java @@ -2,10 +2,12 @@ import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; import lombok.Getter; @Getter public class LowStockEvent extends DomainEvent { + @AggregateId private final ProductId productId; private final int stockAmount; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java index aff0257..d68a9dc 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java @@ -3,10 +3,12 @@ import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; import lombok.Getter; @Getter public class ProductCreatedEvent extends DomainEvent { + @AggregateId private final ProductId productId; private final String name; private final Money price; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java index 1e72462..438c1b4 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java @@ -2,10 +2,12 @@ import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; import lombok.Getter; @Getter public class StockCorrectionEvent extends DomainEvent { + @AggregateId private final ProductId productId; private final int quantity; private final String reason; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java index 1540e63..f33186b 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java @@ -3,6 +3,7 @@ import com.zenfulcode.commercify.product.domain.event.ProductCreatedEvent; import com.zenfulcode.commercify.product.domain.exception.InsufficientStockException; import com.zenfulcode.commercify.product.domain.exception.ProductModificationException; +import com.zenfulcode.commercify.product.domain.valueobject.CategoryId; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; import com.zenfulcode.commercify.shared.domain.model.Money; @@ -49,6 +50,10 @@ public class Product extends AggregateRoot { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "product") private Set variants = new HashSet<>(); + @Embedded + @AttributeOverride(name = "id", column = @Column(name = "category_id")) + private CategoryId categoryId; // Add this field + // Factory method for creating new products public static Product create(String name, String description, int stock, Money money) { Product product = new Product(); diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java index e594c64..5337cd2 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java @@ -1,6 +1,6 @@ package com.zenfulcode.commercify.product.domain.model; -import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import com.zenfulcode.commercify.shared.domain.model.Money; import jakarta.persistence.*; import lombok.*; @@ -18,7 +18,7 @@ @AllArgsConstructor public class ProductVariant { @EmbeddedId - private ProductId id; + private VariantId id; @Column(nullable = false, unique = true) private String sku; @@ -45,7 +45,7 @@ public class ProductVariant { // Factory method public static ProductVariant create(String sku, Integer stock, Money price) { ProductVariant variant = new ProductVariant(); - variant.id = ProductId.generate(); + variant.id = VariantId.generate(); variant.sku = Objects.requireNonNull(sku, "SKU is required"); variant.stock = stock; variant.price = price; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java index eae641f..b138e39 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java @@ -1,30 +1,48 @@ package com.zenfulcode.commercify.product.domain.valueobject; +import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import lombok.Value; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.Objects; import java.util.UUID; -@Value @Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class CategoryId { - String value; + @Column(name = "id") + private String id; - private CategoryId(String value) { - this.value = Objects.requireNonNull(value); + private CategoryId(String id) { + this.id = Objects.requireNonNull(id); } public static CategoryId generate() { return new CategoryId(UUID.randomUUID().toString()); } - public static CategoryId of(String value) { - return new CategoryId(value); + public static CategoryId of(String id) { + return new CategoryId(id); } - // Required by JPA - protected CategoryId() { - this.value = null; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CategoryId that = (CategoryId) o; + return Objects.equals(id, that.id); } -} + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java index 24b7bef..ea322ef 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java @@ -1,30 +1,48 @@ package com.zenfulcode.commercify.product.domain.valueobject; +import jakarta.persistence.Column; import jakarta.persistence.Embeddable; -import lombok.Value; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.Objects; import java.util.UUID; -@Value @Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProductId { - String value; + @Column(name = "id") + private String id; - private ProductId(String value) { - this.value = Objects.requireNonNull(value); + private ProductId(String id) { + this.id = Objects.requireNonNull(id); } public static ProductId generate() { return new ProductId(UUID.randomUUID().toString()); } - public static ProductId of(String value) { - return new ProductId(value); + public static ProductId of(String id) { + return new ProductId(id); } - // Required by JPA - protected ProductId() { - this.value = null; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProductId that = (ProductId) o; + return Objects.equals(id, that.id); } -} + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java new file mode 100644 index 0000000..d4a313f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java @@ -0,0 +1,48 @@ +package com.zenfulcode.commercify.product.domain.valueobject; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.UUID; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class VariantId extends ProductId{ + @Column(name = "id") + private String id; + + private VariantId(String id) { + this.id = Objects.requireNonNull(id); + } + + public static VariantId generate() { + return new VariantId(UUID.randomUUID().toString()); + } + + public static VariantId of(String id) { + return new VariantId(id); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VariantId that = (VariantId) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java index 9e3b978..4833f3c 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java @@ -6,6 +6,7 @@ import com.zenfulcode.commercify.order.domain.repository.OrderLineRepository; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -32,22 +33,22 @@ public List findByOrderId(OrderId orderId) { @Override public Set findActiveOrdersForProduct( ProductId productId, Collection statuses) { - return repository.findActiveOrdersForProduct(productId.getValue(), statuses); + return repository.findActiveOrdersForProduct(productId, statuses); } @Override public Set findActiveOrdersForVariant( - Long variantId, Collection statuses) { + VariantId variantId, Collection statuses) { return repository.findActiveOrdersForVariant(variantId, statuses); } @Override public boolean hasActiveOrders(ProductId productId) { - return repository.hasActiveOrders(productId.getValue()); + return repository.hasActiveOrders(productId); } @Override - public boolean hasActiveOrdersForVariant(ProductId variantId) { + public boolean hasActiveOrdersForVariant(VariantId variantId) { return repository.hasActiveOrdersForVariant(variantId); } } diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java index d898ff2..6a612d5 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java @@ -26,32 +26,32 @@ public Product save(Product product) { @Override public Optional findById(ProductId id) { - return repository.findById(id.getValue()); + return repository.findById(id); } @Override public void delete(Product product) { - + repository.delete(product); } @Override public Page findAll(Pageable pageable) { - return null; + return repository.findAll(pageable); } @Override public Page findByActiveTrue(Pageable pageable) { - return null; + return repository.findByActiveTrue(pageable); } @Override public Page findByCategory(CategoryId categoryId, Pageable pageable) { - return repository.findByCategoryId(categoryId.getValue(), pageable); + return repository.findByCategoryId(categoryId, pageable); } @Override public Page findByStockLessThan(int threshold, Pageable pageable) { - return null; + return repository.findByStockLessThan(threshold, pageable); } @Override diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java index 0ce216f..6fa7f50 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java @@ -5,6 +5,7 @@ import com.zenfulcode.commercify.order.domain.model.OrderStatus; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -25,7 +26,7 @@ interface SpringDataJpaOrderLineRepository extends JpaRepository findActiveOrdersForProduct( - @Param("productId") String productId, + @Param("productId") ProductId productId, @Param("statuses") Collection statuses ); @@ -36,7 +37,7 @@ Set findActiveOrdersForProduct( AND ol.order.status IN :statuses """) Set findActiveOrdersForVariant( - @Param("variantId") Long variantId, + @Param("variantId") VariantId variantId, @Param("statuses") Collection statuses ); @@ -45,7 +46,7 @@ SELECT COUNT(ol) > 0 FROM OrderLine ol WHERE ol.productId = :productId """) boolean hasActiveOrders( - @Param("productId") String productId + @Param("productId") ProductId productId ); @Query(""" @@ -54,6 +55,6 @@ SELECT COUNT(ol) > 0 FROM OrderLine ol WHERE v.id = :variantId """) boolean hasActiveOrdersForVariant( - @Param("variantId") ProductId variantId + @Param("variantId") VariantId variantId ); } diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java index f9d1a46..f4a014f 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java @@ -1,18 +1,16 @@ package com.zenfulcode.commercify.product.infrastructure.persistence; import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.valueobject.CategoryId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -interface SpringDataJpaProductRepository extends JpaRepository { +interface SpringDataJpaProductRepository extends JpaRepository { Page findByActiveTrue(Pageable pageable); - - Page findByCategoryId(String categoryId, Pageable pageable); - + Page findByCategoryId(CategoryId categoryId, Pageable pageable); // Update parameter type Page findByStockLessThan(int threshold, Pageable pageable); - - boolean existsBySku(String sku); -} +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/AggregateReference.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/AggregateReference.java new file mode 100644 index 0000000..d7c0850 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/AggregateReference.java @@ -0,0 +1,57 @@ +package com.zenfulcode.commercify.shared.domain.event; + +import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; + +import java.lang.reflect.Field; +import java.util.Arrays; + +public class AggregateReference { + + public static String extractId(DomainEvent event) { + // First try to find a field ending with "Id" + String id = Arrays.stream(event.getClass().getDeclaredFields()) + .filter(field -> field.getName().toLowerCase().endsWith("id")) + .findFirst() + .map(field -> getFieldValue(field, event)) + .map(Object::toString) + .orElse(null); + + if (id == null) { + // Fallback to any field annotated with @AggregateId if exists + id = Arrays.stream(event.getClass().getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(AggregateId.class)) + .findFirst() + .map(field -> getFieldValue(field, event)) + .map(Object::toString) + .orElse(event.getEventId()); // Use event ID as last resort + } + + return id; + } + + public static String extractType(DomainEvent event) { + // Try to extract from class name (e.g., OrderCreatedEvent -> Order) + String className = event.getClass().getSimpleName(); + if (className.endsWith("Event")) { + return className.substring(0, className.length() - "Event".length()); + } + + // Fallback to any field that is an AggregateRoot + return Arrays.stream(event.getClass().getDeclaredFields()) + .filter(field -> AggregateRoot.class.isAssignableFrom(field.getType())) + .findFirst() + .map(Field::getType) + .map(Class::getSimpleName) + .orElse("Unknown"); + } + + private static Object getFieldValue(Field field, Object target) { + try { + field.setAccessible(true); + return field.get(target); + } catch (IllegalAccessException e) { + throw new RuntimeException("Could not access field: " + field.getName(), e); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java index ba960fd..6518ed3 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java @@ -18,4 +18,4 @@ protected DomainEvent() { this.eventType = this.getClass().getSimpleName(); this.version = 1; } -} +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java index 9210ad5..c53f3e7 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java @@ -1,7 +1,7 @@ package com.zenfulcode.commercify.shared.domain.event; -public interface DomainEventHandler { - void handle(T event); +public interface DomainEventHandler { + void handle(DomainEvent event); boolean canHandle(DomainEvent event); } diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java b/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java index f9bba13..fcba37c 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java @@ -4,7 +4,6 @@ import org.springframework.data.annotation.Transient; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public abstract class AggregateRoot { @@ -16,7 +15,7 @@ protected void registerEvent(DomainEvent event) { } public List getDomainEvents() { - return Collections.unmodifiableList(domainEvents); + return new ArrayList<>(domainEvents); } public void clearDomainEvents() { diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java b/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java index a85a418..63257dd 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java @@ -33,10 +33,18 @@ public class StoredEvent { @Column private String aggregateType; - public StoredEvent(String eventId, String eventType, String eventData, Instant occurredOn) { + public StoredEvent( + String eventId, + String eventType, + String eventData, + Instant occurredOn, + String aggregateId, + String aggregateType) { this.eventId = eventId; this.eventType = eventType; this.eventData = eventData; this.occurredOn = occurredOn; + this.aggregateId = aggregateId; + this.aggregateType = aggregateType; } } diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java b/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java index 362e307..877cf8b 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java @@ -5,7 +5,6 @@ import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import com.zenfulcode.commercify.shared.domain.event.DomainEventStore; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -15,9 +14,8 @@ @RequiredArgsConstructor public class DefaultDomainEventPublisher implements DomainEventPublisher { private final ApplicationEventPublisher applicationEventPublisher; - @Qualifier("enhancedEventStore") private final DomainEventStore eventStore; - private final List> eventHandlers; + private final List eventHandlers; @Override public void publish(DomainEvent event) { diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/valueobject/AggregateId.java b/src/main/java/com/zenfulcode/commercify/shared/domain/valueobject/AggregateId.java new file mode 100644 index 0000000..02298a0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/valueobject/AggregateId.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.shared.domain.valueobject; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface AggregateId { +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EnhancedEventStore.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EnhancedEventStore.java deleted file mode 100644 index f3783ad..0000000 --- a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/EnhancedEventStore.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.zenfulcode.commercify.shared.infrastructure.persistence; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.zenfulcode.commercify.shared.domain.event.DomainEvent; -import com.zenfulcode.commercify.shared.domain.event.DomainEventStore; -import com.zenfulcode.commercify.shared.domain.model.StoredEvent; -import com.zenfulcode.commercify.shared.domain.service.EventSerializer; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.List; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class EnhancedEventStore implements DomainEventStore { - private final EventStoreRepository repository; - private final ObjectMapper objectMapper; - private final EventSerializer eventSerializer; - - @Override - public void store(DomainEvent event) { - StoredEvent storedEvent = eventSerializer.serialize(event); - repository.save(storedEvent); - } - - @Override - public List getEvents(String aggregateId, String aggregateType) { - return repository.findByAggregateIdAndAggregateType(aggregateId, aggregateType) - .stream() - .map(eventSerializer::deserialize) - .collect(Collectors.toList()); - } - - public List getEventsSince(Instant since) { - return repository.findEventsSince(since) - .stream() - .map(eventSerializer::deserialize) - .collect(Collectors.toList()); - } - - public List getEventsByType(Class eventType) { - return repository.findByEventType(eventType.getName()) - .stream() - .map(event -> (T) eventSerializer.deserialize(event)) - .collect(Collectors.toList()); - } - - public Page getEventsByAggregateType(String aggregateType, Pageable pageable) { - return repository.findByAggregateType(aggregateType, pageable) - .map(eventSerializer::deserialize); - } - - public boolean hasEventOccurred(String aggregateId, String aggregateType, String eventType) { - return repository.hasEventType(aggregateId, aggregateType, eventType); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java index ed457e5..fbc93ac 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java @@ -1,32 +1,29 @@ package com.zenfulcode.commercify.shared.infrastructure.persistence; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.event.DomainEventStore; -import com.zenfulcode.commercify.shared.domain.exception.EventDeserializationException; -import com.zenfulcode.commercify.shared.domain.exception.EventSerializationException; import com.zenfulcode.commercify.shared.domain.model.StoredEvent; +import com.zenfulcode.commercify.shared.infrastructure.service.EventSerializer; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import java.time.Instant; import java.util.List; import java.util.stream.Collectors; -@Repository +@Service @RequiredArgsConstructor public class JpaDomainEventStore implements DomainEventStore { private final EventStoreRepository repository; private final ObjectMapper objectMapper; + private final EventSerializer eventSerializer; @Override public void store(DomainEvent event) { - StoredEvent storedEvent = new StoredEvent( - event.getEventId(), - event.getEventType(), - serializeEvent(event), - event.getOccurredOn() - ); + StoredEvent storedEvent = eventSerializer.serialize(event); repository.save(storedEvent); } @@ -34,27 +31,30 @@ public void store(DomainEvent event) { public List getEvents(String aggregateId, String aggregateType) { return repository.findByAggregateIdAndAggregateType(aggregateId, aggregateType) .stream() - .map(this::deserializeEvent) + .map(eventSerializer::deserialize) .collect(Collectors.toList()); } - private String serializeEvent(DomainEvent event) { - try { - return objectMapper.writeValueAsString(event); - } catch (JsonProcessingException e) { - throw new EventSerializationException("Failed to serialize event", e); - } + public List getEventsSince(Instant since) { + return repository.findEventsSince(since) + .stream() + .map(eventSerializer::deserialize) + .collect(Collectors.toList()); + } + + public List getEventsByType(Class eventType) { + return repository.findByEventType(eventType.getName()) + .stream() + .map(event -> (T) eventSerializer.deserialize(event)) + .collect(Collectors.toList()); + } + + public Page getEventsByAggregateType(String aggregateType, Pageable pageable) { + return repository.findByAggregateType(aggregateType, pageable) + .map(eventSerializer::deserialize); } - private DomainEvent deserializeEvent(StoredEvent storedEvent) { - try { - Class eventClass = Class.forName(storedEvent.getEventType()); - return (DomainEvent) objectMapper.readValue( - storedEvent.getEventData(), - eventClass - ); - } catch (Exception e) { - throw new EventDeserializationException("Failed to deserialize event", e); - } + public boolean hasEventOccurred(String aggregateId, String aggregateType, String eventType) { + return repository.hasEventType(aggregateId, aggregateType, eventType); } } diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/ProductIdConverter.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/ProductIdConverter.java deleted file mode 100644 index dfe292f..0000000 --- a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/ProductIdConverter.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.zenfulcode.commercify.shared.infrastructure.persistence; - -import com.zenfulcode.commercify.product.domain.valueobject.ProductId; -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Converter; - -@Converter(autoApply = true) -public class ProductIdConverter implements AttributeConverter { - - @Override - public String convertToDatabaseColumn(ProductId productId) { - return productId == null ? null : productId.getValue(); - } - - @Override - public ProductId convertToEntityAttribute(String dbData) { - return dbData == null ? null : ProductId.of(dbData); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventSerializer.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java similarity index 91% rename from src/main/java/com/zenfulcode/commercify/shared/domain/service/EventSerializer.java rename to src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java index 7648318..75a18f5 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventSerializer.java +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java @@ -1,7 +1,8 @@ -package com.zenfulcode.commercify.shared.domain.service; +package com.zenfulcode.commercify.shared.infrastructure.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.zenfulcode.commercify.shared.domain.event.AggregateReference; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.exception.EventDeserializationException; import com.zenfulcode.commercify.shared.domain.exception.EventSerializationException; @@ -47,12 +48,10 @@ public DomainEvent deserialize(StoredEvent storedEvent) { } private String getAggregateId(DomainEvent event) { - // Use reflection or annotation to get aggregate ID return AggregateReference.extractId(event); } private String getAggregateType(DomainEvent event) { - // Use reflection or annotation to get aggregate type return AggregateReference.extractType(event); } -} +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventTypeResolver.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventTypeResolver.java similarity index 50% rename from src/main/java/com/zenfulcode/commercify/shared/domain/service/EventTypeResolver.java rename to src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventTypeResolver.java index c1b91c1..d907a67 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/service/EventTypeResolver.java +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventTypeResolver.java @@ -1,6 +1,7 @@ -package com.zenfulcode.commercify.shared.domain.service; +package com.zenfulcode.commercify.shared.infrastructure.service; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.exception.EventDeserializationException; import org.springframework.stereotype.Component; import java.util.Map; @@ -10,8 +11,14 @@ public class EventTypeResolver { private final Map> eventTypeMap = new ConcurrentHashMap<>(); - public Class resolveEventClass(String eventType) throws ClassNotFoundException { - return eventTypeMap.computeIfAbsent(eventType, this::loadEventClass); + public Class resolveEventClass(String eventType) { + return eventTypeMap.computeIfAbsent(eventType, type -> { + try { + return loadEventClass(type); + } catch (ClassNotFoundException e) { + throw new EventDeserializationException("Failed to load event class: " + type, e); + } + }); } @SuppressWarnings("unchecked") @@ -22,4 +29,16 @@ private Class loadEventClass(String eventType) throws Cla } return (Class) loadedClass; } -} + + public void registerEventType(String eventType, Class eventClass) { + eventTypeMap.put(eventType, eventClass); + } + + public boolean isRegistered(String eventType) { + return eventTypeMap.containsKey(eventType); + } + + public void clearRegistrations() { + eventTypeMap.clear(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a069488..3248f42 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,9 +5,10 @@ spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATA spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=create-drop # Migrations spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml +spring.liquibase.enabled=false #The secret key must be an HMAC hash string of 256 bits; security.jwt.secret-key=${JWT_SECRET_KEY} # 1h in millisecond From 8609b3985f500c2756bb24ba915ff05d9d7bb695 Mon Sep 17 00:00:00 2001 From: GustavH Date: Fri, 3 Jan 2025 16:34:51 +0100 Subject: [PATCH 05/57] adding OrderApplicationService fixes server can now start up --- .../command/CancelOrderCommand.java | 7 + .../command/CreateOrderCommand.java | 18 +++ .../command/UpdateOrderStatusCommand.java | 9 ++ .../application/dto/OrderDetailsDTO.java | 34 +++++ .../order/application/dto/OrderLineDTO.java | 31 +++++ .../application/query/FindAllOrdersQuery.java | 8 ++ .../query/FindOrdersByUserIdQuery.java | 10 ++ .../service/OrderApplicationService.java | 121 ++++++++++++++++++ .../order/domain/event/OrderCreatedEvent.java | 5 +- .../exception/OrderNotFoundException.java | 10 ++ .../commercify/order/domain/model/Order.java | 5 +- .../order/domain/model/OrderShippingInfo.java | 32 +++++ .../domain/repository/OrderRepository.java | 21 +++ .../domain/valueobject/OrderDetails.java | 21 +-- .../order/domain/valueobject/OrderId.java | 2 +- .../order/domain/valueobject/OrderLineId.java | 2 +- .../persistence/JpaOrderLineRepository.java | 2 +- .../persistence/JpaOrderRepository.java | 43 +++++++ .../SpringDataJpaOrderLineRepository.java | 2 +- .../SpringDataJpaOrderRepository.java | 16 +++ .../domain/repository/ProductRepository.java | 8 +- .../domain/valueobject/CategoryId.java | 2 +- .../product/domain/valueobject/ProductId.java | 2 +- .../product/domain/valueobject/VariantId.java | 4 +- .../persistence/JpaProductRepository.java | 17 ++- .../JpaProductVariantRepository.java | 43 ------- .../SpringDataJpaProductRepository.java | 4 +- .../SpringDataJpaVariantRepository.java | 11 +- .../user/domain/valueobject/UserId.java | 48 +++++++ 29 files changed, 455 insertions(+), 83 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/command/CancelOrderCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/command/CreateOrderCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/command/UpdateOrderStatusCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/dto/OrderLineDTO.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/query/FindAllOrdersQuery.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/query/FindOrdersByUserIdQuery.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/exception/OrderNotFoundException.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java rename src/main/java/com/zenfulcode/commercify/{product => order}/infrastructure/persistence/JpaOrderLineRepository.java (96%) create mode 100644 src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java rename src/main/java/com/zenfulcode/commercify/{product => order}/infrastructure/persistence/SpringDataJpaOrderLineRepository.java (96%) create mode 100644 src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductVariantRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserId.java diff --git a/src/main/java/com/zenfulcode/commercify/order/application/command/CancelOrderCommand.java b/src/main/java/com/zenfulcode/commercify/order/application/command/CancelOrderCommand.java new file mode 100644 index 0000000..f4d97ca --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/command/CancelOrderCommand.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.order.application.command; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; + +public record CancelOrderCommand( + OrderId orderId +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/application/command/CreateOrderCommand.java b/src/main/java/com/zenfulcode/commercify/order/application/command/CreateOrderCommand.java new file mode 100644 index 0000000..a83639d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/command/CreateOrderCommand.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.order.application.command; + +import com.zenfulcode.commercify.order.domain.valueobject.Address; +import com.zenfulcode.commercify.order.domain.valueobject.CustomerDetails; +import com.zenfulcode.commercify.order.domain.valueobject.OrderLineDetails; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; + +import java.util.List; + +public record CreateOrderCommand( + UserId customerId, + String currency, + CustomerDetails customerDetails, + Address shippingAddress, + Address billingAddress, + List orderLines +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/command/UpdateOrderStatusCommand.java b/src/main/java/com/zenfulcode/commercify/order/application/command/UpdateOrderStatusCommand.java new file mode 100644 index 0000000..b7c3888 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/command/UpdateOrderStatusCommand.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.order.application.command; + +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; + +public record UpdateOrderStatusCommand( + OrderId orderId, + OrderStatus newStatus +) {} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java new file mode 100644 index 0000000..79c640c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java @@ -0,0 +1,34 @@ +package com.zenfulcode.commercify.order.application.dto; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.Address; +import com.zenfulcode.commercify.order.domain.valueobject.CustomerDetails; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; + +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +public record OrderDetailsDTO(OrderId id, UserId userId, OrderStatus status, String currency, Money totalAmount, + List orderLines, CustomerDetails customerDetails, Address shippingAddress, + Address billingAddress, Instant createdAt) { + public static OrderDetailsDTO fromOrder(Order order) { + return new OrderDetailsDTO( + order.getId(), + order.getUserId(), + order.getStatus(), + order.getCurrency(), + order.getTotalAmount(), + order.getOrderLines().stream() + .map(OrderLineDTO::fromOrderLine) + .collect(Collectors.toList()), + order.getOrderShippingInfo().toCustomerDetails(), + order.getOrderShippingInfo().toShippingAddress(), + order.getOrderShippingInfo().toBillingAddress(), + order.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderLineDTO.java b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderLineDTO.java new file mode 100644 index 0000000..eecd64b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderLineDTO.java @@ -0,0 +1,31 @@ +package com.zenfulcode.commercify.order.application.dto; + +import com.zenfulcode.commercify.order.domain.model.OrderLine; +import com.zenfulcode.commercify.order.domain.valueobject.OrderLineId; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.Builder; + +@Builder +public record OrderLineDTO(OrderLineId id, + ProductId productId, + VariantId variantId, + int quantity, + Money unitPrice, + Money total, + String variantSku) { + public static OrderLineDTO fromOrderLine(OrderLine orderLine) { + return OrderLineDTO.builder() + .id(orderLine.getId()) + .productId(orderLine.getProductId()) + .variantId(orderLine.getProductVariant() != null ? + orderLine.getProductVariant().getId() : null) + .quantity(orderLine.getQuantity()) + .unitPrice(orderLine.getUnitPrice()) + .total(orderLine.getTotal()) + .variantSku(orderLine.getProductVariant() != null ? + orderLine.getProductVariant().getSku() : null) + .build(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/query/FindAllOrdersQuery.java b/src/main/java/com/zenfulcode/commercify/order/application/query/FindAllOrdersQuery.java new file mode 100644 index 0000000..1ea8181 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/query/FindAllOrdersQuery.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.order.application.query; + +import org.springframework.data.domain.PageRequest; + +public record FindAllOrdersQuery( + PageRequest pageRequest +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/query/FindOrdersByUserIdQuery.java b/src/main/java/com/zenfulcode/commercify/order/application/query/FindOrdersByUserIdQuery.java new file mode 100644 index 0000000..b66925c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/query/FindOrdersByUserIdQuery.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.order.application.query; + +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import org.springframework.data.domain.PageRequest; + +public record FindOrdersByUserIdQuery( + UserId userId, + PageRequest pageRequest +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java new file mode 100644 index 0000000..e67e1b7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java @@ -0,0 +1,121 @@ +package com.zenfulcode.commercify.order.application.service; + +import com.zenfulcode.commercify.order.application.command.CancelOrderCommand; +import com.zenfulcode.commercify.order.application.command.CreateOrderCommand; +import com.zenfulcode.commercify.order.application.command.UpdateOrderStatusCommand; +import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; +import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; +import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; +import com.zenfulcode.commercify.order.domain.exception.OrderNotFoundException; +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.repository.OrderRepository; +import com.zenfulcode.commercify.order.domain.service.OrderDomainService; +import com.zenfulcode.commercify.order.domain.valueobject.OrderDetails; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.order.domain.valueobject.OrderLineDetails; +import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.repository.ProductRepository; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class OrderApplicationService { + private final OrderDomainService orderDomainService; + private final OrderRepository orderRepository; + private final ProductRepository productRepository; + private final DomainEventPublisher eventPublisher; + + @Transactional + public OrderId createOrder(CreateOrderCommand command) { + // Get products and variants + List productIds = command.orderLines() + .stream() + .map(OrderLineDetails::productId) + .collect(Collectors.toList()); + + List products = productRepository.findAllById(productIds); + + List variantIds = command.orderLines() + .stream() + .map(OrderLineDetails::variantId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + List variants = productRepository.findVariantsByIds(variantIds); + + // Create order through domain service + Order order = orderDomainService.createOrder( + OrderDetails.builder() + .customerId(command.customerId()) + .currency(command.currency()) + .customerDetails(command.customerDetails()) + .shippingAddress(command.shippingAddress()) + .billingAddress(command.billingAddress()) + .orderLines(command.orderLines()) + .build(), + products, + variants + ); + + // Save and publish events + Order savedOrder = orderRepository.save(order); + eventPublisher.publish(savedOrder.getDomainEvents()); + return savedOrder.getId(); + } + + @Transactional + public void updateOrderStatus(UpdateOrderStatusCommand command) { + Order order = orderRepository.findById(command.orderId()) + .orElseThrow(() -> new OrderNotFoundException(command.orderId())); + + orderDomainService.updateOrderStatus(order, command.newStatus()); + orderRepository.save(order); + eventPublisher.publish(order.getDomainEvents()); + } + + @Transactional + public void cancelOrder(CancelOrderCommand command) { + Order order = orderRepository.findById(command.orderId()) + .orElseThrow(() -> new OrderNotFoundException(command.orderId())); + + orderDomainService.updateOrderStatus(order, OrderStatus.CANCELLED); + orderRepository.save(order); + eventPublisher.publish(order.getDomainEvents()); + } + + @Transactional(readOnly = true) + public Page findOrdersByUserId(FindOrdersByUserIdQuery query) { + return orderRepository.findByUserId(query.userId(), query.pageRequest()); + } + + @Transactional(readOnly = true) + public Page findAllOrders(FindAllOrdersQuery query) { + return orderRepository.findAll(query.pageRequest()); + } + + @Transactional(readOnly = true) + public OrderDetailsDTO getOrderById(OrderId orderId) { + Order order = orderRepository.findById(orderId) + .orElseThrow(() -> new OrderNotFoundException(orderId)); + + return OrderDetailsDTO.fromOrder(order); + } + + @Transactional(readOnly = true) + public boolean isOrderOwnedByUser(OrderId orderId, UserId userId) { + return orderRepository.existsByIdAndUserId(orderId, userId); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java index 39b8d4a..361d9e8 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java @@ -3,6 +3,7 @@ import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; import lombok.Getter; import java.time.Instant; @@ -11,11 +12,11 @@ public class OrderCreatedEvent extends DomainEvent { @AggregateId private final OrderId orderId; - private final Long userId; + private final UserId userId; private final String currency; private final Instant createdAt; - public OrderCreatedEvent(OrderId orderId, Long userId, String currency) { + public OrderCreatedEvent(OrderId orderId, UserId userId, String currency) { this.orderId = orderId; this.userId = userId; this.currency = currency; diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/exception/OrderNotFoundException.java b/src/main/java/com/zenfulcode/commercify/order/domain/exception/OrderNotFoundException.java new file mode 100644 index 0000000..f990f9c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/exception/OrderNotFoundException.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.order.domain.exception; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.shared.domain.exception.DomainException; + +public class OrderNotFoundException extends DomainException { + public OrderNotFoundException(OrderId orderId) { + super("Order not found with ID: " + orderId); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java index 83f17b2..596a3e3 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -7,6 +7,7 @@ import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -27,7 +28,7 @@ public class Order extends AggregateRoot { private OrderId id; @Column(name = "user_id") - private Long userId; + private UserId userId; @OneToMany( mappedBy = "order", @@ -85,7 +86,7 @@ public class Order extends AggregateRoot { // Factory method public static Order create( - Long userId, + UserId userId, String currency, OrderShippingInfo shippingInfo ) { diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java index af0ecc0..b81f7a6 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java @@ -92,6 +92,38 @@ public static OrderShippingInfo create( return info; } + public CustomerDetails toCustomerDetails() { + return new CustomerDetails( + customerFirstName, + customerLastName, + customerEmail, + customerPhone + ); + } + + public Address toShippingAddress() { + return new Address( + shippingStreet, + shippingCity, + shippingState, + shippingZip, + shippingCountry + ); + } + + public Address toBillingAddress() { + if (!hasBillingAddress()) { + return null; + } + return new Address( + billingStreet, + billingCity, + billingState, + billingZip, + billingCountry + ); + } + public boolean hasBillingAddress() { return billingStreet != null && !billingStreet.isBlank(); } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java new file mode 100644 index 0000000..d91af08 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java @@ -0,0 +1,21 @@ +package com.zenfulcode.commercify.order.domain.repository; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.Optional; + +public interface OrderRepository { + Order save(Order order); + + Optional findById(OrderId id); + + Page findByUserId(UserId userId, PageRequest pageRequest); + + Page findAll(PageRequest pageRequest); + + boolean existsByIdAndUserId(OrderId id, UserId userId); +} diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java index 0d68a3c..998a2f5 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java @@ -1,13 +1,15 @@ package com.zenfulcode.commercify.order.domain.valueobject; import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; -import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.Builder; import java.util.ArrayList; import java.util.List; +@Builder public record OrderDetails( - Long customerId, + UserId customerId, String currency, CustomerDetails customerDetails, Address shippingAddress, @@ -19,7 +21,7 @@ public record OrderDetails( } private void validate( - Long customerId, + UserId customerId, String currency, CustomerDetails customerDetails, Address shippingAddress, @@ -27,22 +29,15 @@ private void validate( ) { List violations = new ArrayList<>(); - if (customerId == null) { - violations.add("Customer ID is required"); - } - if (currency == null || currency.isBlank()) { violations.add("Currency is required"); } - if (customerDetails == null) { violations.add("Customer details are required"); } - if (shippingAddress == null) { violations.add("Shipping address is required"); } - if (orderLines == null || orderLines.isEmpty()) { violations.add("Order must contain at least one item"); } @@ -51,10 +46,4 @@ private void validate( throw new OrderValidationException(violations); } } - - public Money calculateSubtotal() { - return orderLines.stream() - .map(OrderLineDetails::calculateTotal) - .reduce(Money.zero(currency), Money::add); - } } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java index dc77c27..16200be 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderId { - @Column(name = "id") + @Column(name = "order_id") private String id; private OrderId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java index 3e5c718..6c4418b 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderLineId { - @Column(name = "id") + @Column(name = "orderline_id") private String id; private OrderLineId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderLineRepository.java similarity index 96% rename from src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java rename to src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderLineRepository.java index 4833f3c..a95e020 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaOrderLineRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderLineRepository.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.product.infrastructure.persistence; +package com.zenfulcode.commercify.order.infrastructure.persistence; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.order.domain.model.OrderLine; diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java new file mode 100644 index 0000000..fa25f06 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java @@ -0,0 +1,43 @@ +package com.zenfulcode.commercify.order.infrastructure.persistence; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.repository.OrderRepository; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class JpaOrderRepository implements OrderRepository { + private final SpringDataJpaOrderRepository repository; + + @Override + public Order save(Order order) { + return repository.save(order); + } + + @Override + public Optional findById(OrderId id) { + return repository.findById(id); + } + + @Override + public Page findByUserId(UserId userId, PageRequest pageRequest) { + return repository.findByUserId(userId, pageRequest); + } + + @Override + public Page findAll(PageRequest pageRequest) { + return repository.findAll(pageRequest); + } + + @Override + public boolean existsByIdAndUserId(OrderId id, UserId userId) { + return repository.existsByIdAndUserId(id, userId); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderLineRepository.java similarity index 96% rename from src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java rename to src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderLineRepository.java index 6fa7f50..9febaba 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaOrderLineRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderLineRepository.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.product.infrastructure.persistence; +package com.zenfulcode.commercify.order.infrastructure.persistence; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.order.domain.model.OrderLine; diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderRepository.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderRepository.java new file mode 100644 index 0000000..93da3ea --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderRepository.java @@ -0,0 +1,16 @@ +package com.zenfulcode.commercify.order.infrastructure.persistence; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +interface SpringDataJpaOrderRepository extends JpaRepository { + Page findByUserId(UserId userId, Pageable pageable); + + boolean existsByIdAndUserId(OrderId id, UserId userId); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java index 23855ca..9d5b3a7 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java @@ -1,11 +1,15 @@ package com.zenfulcode.commercify.product.domain.repository; import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; import com.zenfulcode.commercify.product.domain.valueobject.CategoryId; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.Collection; +import java.util.List; import java.util.Optional; public interface ProductRepository { @@ -23,5 +27,7 @@ public interface ProductRepository { Page findByStockLessThan(int threshold, Pageable pageable); - boolean existsBySku(String sku); + List findAllById(Collection ids); + + List findVariantsByIds(Collection variantIds); } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java index b138e39..4a527a3 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CategoryId { - @Column(name = "id") + @Column(name = "category_id") private String id; private CategoryId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java index ea322ef..9c9fc22 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProductId { - @Column(name = "id") + @Column(name = "product_id") private String id; private ProductId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java index d4a313f..6850963 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java @@ -12,8 +12,8 @@ @Embeddable @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class VariantId extends ProductId{ - @Column(name = "id") +public class VariantId extends ProductId { + @Column(name = "variant_id") private String id; private VariantId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java index 6a612d5..06e2417 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java @@ -2,21 +2,27 @@ import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; import com.zenfulcode.commercify.product.domain.repository.ProductRepository; import com.zenfulcode.commercify.product.domain.valueobject.CategoryId; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import java.util.Collection; +import java.util.List; import java.util.Optional; @Repository public class JpaProductRepository implements ProductRepository { private final SpringDataJpaProductRepository repository; + private final SpringDataJpaVariantRepository variantRepository; - JpaProductRepository(SpringDataJpaProductRepository repository) { + JpaProductRepository(SpringDataJpaProductRepository repository, SpringDataJpaVariantRepository variantRepository) { this.repository = repository; + this.variantRepository = variantRepository; } @Override @@ -55,7 +61,12 @@ public Page findByStockLessThan(int threshold, Pageable pageable) { } @Override - public boolean existsBySku(String sku) { - return false; + public List findAllById(Collection ids) { + return repository.findAllById(ids); + } + + @Override + public List findVariantsByIds(Collection variantIds) { + return variantRepository.findAllById(variantIds); } } diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductVariantRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductVariantRepository.java deleted file mode 100644 index b1a813f..0000000 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductVariantRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.zenfulcode.commercify.product.infrastructure.persistence; - -import com.zenfulcode.commercify.product.domain.model.ProductVariant; -import com.zenfulcode.commercify.product.domain.repository.ProductVariantRepository; -import com.zenfulcode.commercify.product.domain.valueobject.ProductId; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -class JpaProductVariantRepository implements ProductVariantRepository { - private final SpringDataJpaVariantRepository repository; - - JpaProductVariantRepository(SpringDataJpaVariantRepository repository) { - this.repository = repository; - } - - @Override - public ProductVariant save(ProductVariant variant) { - return null; - } - - @Override - public Optional findById(Long id) { - return Optional.empty(); - } - - @Override - public Optional findBySku(String sku) { - return Optional.empty(); - } - - @Override - public void delete(ProductVariant variant) { - - } - - @Override - public List findByProductId(ProductId productId) { - return List.of(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java index f4a014f..c570414 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java @@ -11,6 +11,8 @@ @Repository interface SpringDataJpaProductRepository extends JpaRepository { Page findByActiveTrue(Pageable pageable); - Page findByCategoryId(CategoryId categoryId, Pageable pageable); // Update parameter type + + Page findByCategoryId(CategoryId categoryId, Pageable pageable); + Page findByStockLessThan(int threshold, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaVariantRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaVariantRepository.java index cd8ec20..79231f6 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaVariantRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaVariantRepository.java @@ -1,13 +1,10 @@ package com.zenfulcode.commercify.product.infrastructure.persistence; import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.Optional; - -interface SpringDataJpaVariantRepository extends JpaRepository { - Optional findBySku(String sku); - - List findByProductId(String productId); +@Repository +interface SpringDataJpaVariantRepository extends JpaRepository { } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserId.java b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserId.java new file mode 100644 index 0000000..3960b8f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserId.java @@ -0,0 +1,48 @@ +package com.zenfulcode.commercify.user.domain.valueobject; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.UUID; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserId { + @Column(name = "user_id") + private String id; + + private UserId(String id) { + this.id = Objects.requireNonNull(id); + } + + public static UserId generate() { + return new UserId(UUID.randomUUID().toString()); + } + + public static UserId of(String id) { + return new UserId(id); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserId that = (UserId) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; + } +} \ No newline at end of file From ac142209cf59431de70f58d77966884aee8d97c1 Mon Sep 17 00:00:00 2001 From: GustavH Date: Fri, 3 Jan 2025 17:01:16 +0100 Subject: [PATCH 06/57] preparing database setup --- .../commercify/api/order/OrderController.java | 108 +++++++++++ .../api/order/dto/request/AddressRequest.java | 10 + .../dto/request/CreateOrderLineRequest.java | 9 + .../order/dto/request/CreateOrderRequest.java | 13 ++ .../dto/request/CustomerDetailsRequest.java | 9 + .../dto/request/UpdateOrderStatusRequest.java | 6 + .../order/dto/response/AddressResponse.java | 10 + .../dto/response/CreateOrderResponse.java | 7 + .../dto/response/CustomerDetailsResponse.java | 9 + .../api/order/dto/response/MoneyResponse.java | 7 + .../dto/response/OrderDetailsResponse.java | 17 ++ .../order/dto/response/OrderLineResponse.java | 11 ++ .../dto/response/OrderSummaryResponse.java | 12 ++ .../dto/response/PagedOrderResponse.java | 10 + .../api/order/mapper/OrderDtoMapper.java | 156 ++++++++++++++++ .../api/product/ProductController.java | 2 +- .../{dto => mapper}/ProductDtoMapper.java | 2 +- .../application/dto/OrderDetailsDTO.java | 13 +- .../domain/valueobject/OrderLineDetails.java | 8 +- src/main/resources/application.properties | 4 +- .../db/changelog/db.changelog-master.xml | 11 -- .../migrations/241108185143-changelog.xml | 173 ------------------ .../migrations/241115001827-changelog.xml | 114 ------------ .../migrations/241115135357-changelog.xml | 12 -- .../migrations/241115144659-changelog.xml | 15 -- .../migrations/241121174307-changelog.xml | 50 ----- .../migrations/241127144629-changelog.xml | 49 ----- .../migrations/241201215506-changelog.xml | 12 -- .../migrations/241223151914-changelog.xml | 65 ------- .../migrations/241230175909-changelog.xml | 20 -- .../migrations/241230180613-changelog.xml | 13 -- 31 files changed, 409 insertions(+), 548 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/OrderController.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/request/AddressRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderLineRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/request/CustomerDetailsRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/request/UpdateOrderStatusRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/AddressResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/CreateOrderResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/CustomerDetailsResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/MoneyResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderDetailsResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderSummaryResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/PagedOrderResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java rename src/main/java/com/zenfulcode/commercify/api/product/{dto => mapper}/ProductDtoMapper.java (98%) delete mode 100644 src/main/resources/db/changelog/migrations/241108185143-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241115001827-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241115135357-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241115144659-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241121174307-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241127144629-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241201215506-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241223151914-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241230175909-changelog.xml delete mode 100644 src/main/resources/db/changelog/migrations/241230180613-changelog.xml diff --git a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java new file mode 100644 index 0000000..b02819f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java @@ -0,0 +1,108 @@ +package com.zenfulcode.commercify.api.order; + +import com.zenfulcode.commercify.api.order.dto.request.CreateOrderRequest; +import com.zenfulcode.commercify.api.order.dto.request.UpdateOrderStatusRequest; +import com.zenfulcode.commercify.api.order.dto.response.CreateOrderResponse; +import com.zenfulcode.commercify.api.order.dto.response.OrderDetailsResponse; +import com.zenfulcode.commercify.api.order.dto.response.PagedOrderResponse; +import com.zenfulcode.commercify.api.order.mapper.OrderDtoMapper; +import com.zenfulcode.commercify.order.application.command.CancelOrderCommand; +import com.zenfulcode.commercify.order.application.command.CreateOrderCommand; +import com.zenfulcode.commercify.order.application.command.UpdateOrderStatusCommand; +import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; +import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; +import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; +import com.zenfulcode.commercify.order.application.service.OrderApplicationService; +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/orders") +@RequiredArgsConstructor +public class OrderController { + private final OrderApplicationService orderApplicationService; + private final OrderDtoMapper orderDtoMapper; + + @PostMapping + public ResponseEntity> createOrder( + @RequestBody CreateOrderRequest request) { + CreateOrderCommand command = orderDtoMapper.toCommand(request); + OrderId orderId = orderApplicationService.createOrder(command); + + CreateOrderResponse response = new CreateOrderResponse( + orderId.toString(), + "Order created successfully" + ); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @GetMapping("/{orderId}") + public ResponseEntity> getOrder( + @PathVariable String orderId) { + OrderDetailsDTO order = orderApplicationService.getOrderById(OrderId.of(orderId)); + OrderDetailsResponse response = orderDtoMapper.toResponse(order); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @GetMapping("/user/{userId}") + @PreAuthorize("hasRole('USER') and #userId == authentication.principal.id or hasRole('ADMIN')") + public ResponseEntity> getOrdersByUserId( + @PathVariable String userId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + + FindOrdersByUserIdQuery query = new FindOrdersByUserIdQuery( + UserId.of(userId), + PageRequest.of(page, size) + ); + + Page orders = orderApplicationService.findOrdersByUserId(query); + PagedOrderResponse response = orderDtoMapper.toPagedResponse(orders); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @GetMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getAllOrders( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + + FindAllOrdersQuery query = new FindAllOrdersQuery(PageRequest.of(page, size)); + Page orders = orderApplicationService.findAllOrders(query); + PagedOrderResponse response = orderDtoMapper.toPagedResponse(orders); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PutMapping("/{orderId}/status") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> updateOrderStatus( + @PathVariable String orderId, + @RequestBody UpdateOrderStatusRequest request) { + + UpdateOrderStatusCommand command = new UpdateOrderStatusCommand( + OrderId.of(orderId), + OrderStatus.valueOf(request.status()) + ); + + orderApplicationService.updateOrderStatus(command); + return ResponseEntity.ok(ApiResponse.success("Order status updated successfully")); + } + + @DeleteMapping("/{orderId}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> cancelOrder(@PathVariable String orderId) { + CancelOrderCommand command = new CancelOrderCommand(OrderId.of(orderId)); + orderApplicationService.cancelOrder(command); + return ResponseEntity.ok(ApiResponse.success("Order cancelled successfully")); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/request/AddressRequest.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/AddressRequest.java new file mode 100644 index 0000000..f6ca1c8 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/AddressRequest.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.api.order.dto.request; + +public record AddressRequest( + String street, + String city, + String state, + String zipCode, + String country +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderLineRequest.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderLineRequest.java new file mode 100644 index 0000000..7367a5e --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderLineRequest.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.api.order.dto.request; + + +public record CreateOrderLineRequest( + String productId, + String variantId, + int quantity +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderRequest.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderRequest.java new file mode 100644 index 0000000..23012a7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderRequest.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.api.order.dto.request; + +import java.util.List; + +public record CreateOrderRequest( + String userId, + String currency, + CustomerDetailsRequest customerDetails, + AddressRequest shippingAddress, + AddressRequest billingAddress, + List orderLines +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CustomerDetailsRequest.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CustomerDetailsRequest.java new file mode 100644 index 0000000..2d278ca --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CustomerDetailsRequest.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.api.order.dto.request; + +public record CustomerDetailsRequest( + String firstName, + String lastName, + String email, + String phone +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/request/UpdateOrderStatusRequest.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/UpdateOrderStatusRequest.java new file mode 100644 index 0000000..1d38262 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/UpdateOrderStatusRequest.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.api.order.dto.request; + +public record UpdateOrderStatusRequest( + String status +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/AddressResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/AddressResponse.java new file mode 100644 index 0000000..4a313c9 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/AddressResponse.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.api.order.dto.response; + +public record AddressResponse( + String street, + String city, + String state, + String zipCode, + String country +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/CreateOrderResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/CreateOrderResponse.java new file mode 100644 index 0000000..add682a --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/CreateOrderResponse.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.api.order.dto.response; + +public record CreateOrderResponse( + String id, + String message +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/CustomerDetailsResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/CustomerDetailsResponse.java new file mode 100644 index 0000000..87465f1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/CustomerDetailsResponse.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.api.order.dto.response; + +public record CustomerDetailsResponse( + String firstName, + String lastName, + String email, + String phone +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/MoneyResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/MoneyResponse.java new file mode 100644 index 0000000..1ba0f0f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/MoneyResponse.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.api.order.dto.response; + +public record MoneyResponse( + double amount, + String currency +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderDetailsResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderDetailsResponse.java new file mode 100644 index 0000000..2e524f2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderDetailsResponse.java @@ -0,0 +1,17 @@ +package com.zenfulcode.commercify.api.order.dto.response; + +import java.time.Instant; +import java.util.List; + +public record OrderDetailsResponse( + String id, + String userId, + String status, + String currency, + MoneyResponse totalAmount, + List orderLines, + CustomerDetailsResponse customerDetails, + AddressResponse shippingAddress, + AddressResponse billingAddress, + Instant createdAt +) {} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java new file mode 100644 index 0000000..f22b751 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.api.order.dto.response; + +public record OrderLineResponse( + String id, + String productId, + String variantId, + int quantity, + MoneyResponse unitPrice, + MoneyResponse total +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderSummaryResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderSummaryResponse.java new file mode 100644 index 0000000..c6dd019 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderSummaryResponse.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.api.order.dto.response; + +import java.time.Instant; + +public record OrderSummaryResponse( + String id, + String userId, + String status, + MoneyResponse totalAmount, + Instant createdAt +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/PagedOrderResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/PagedOrderResponse.java new file mode 100644 index 0000000..878b8cf --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/PagedOrderResponse.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.api.order.dto.response; + +import com.zenfulcode.commercify.api.product.dto.response.PageInfo; + +import java.util.List; + +public record PagedOrderResponse( + List items, + PageInfo pageInfo +) {} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java new file mode 100644 index 0000000..1a72c4e --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java @@ -0,0 +1,156 @@ +package com.zenfulcode.commercify.api.order.mapper; + +import com.zenfulcode.commercify.api.order.dto.request.CreateOrderLineRequest; +import com.zenfulcode.commercify.api.order.dto.request.CreateOrderRequest; +import com.zenfulcode.commercify.api.order.dto.response.*; +import com.zenfulcode.commercify.api.product.dto.response.PageInfo; +import com.zenfulcode.commercify.order.application.command.CreateOrderCommand; +import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; +import com.zenfulcode.commercify.order.application.dto.OrderLineDTO; +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.valueobject.Address; +import com.zenfulcode.commercify.order.domain.valueobject.CustomerDetails; +import com.zenfulcode.commercify.order.domain.valueobject.OrderLineDetails; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class OrderDtoMapper { + public CreateOrderCommand toCommand(CreateOrderRequest request) { + CustomerDetails customerDetails = new CustomerDetails( + request.customerDetails().firstName(), + request.customerDetails().lastName(), + request.customerDetails().email(), + request.customerDetails().phone() + ); + + Address shippingAddress = new Address( + request.shippingAddress().street(), + request.shippingAddress().city(), + request.shippingAddress().state(), + request.shippingAddress().zipCode(), + request.shippingAddress().country() + ); + + Address billingAddress = request.billingAddress() != null ? new Address( + request.billingAddress().street(), + request.billingAddress().city(), + request.billingAddress().state(), + request.billingAddress().zipCode(), + request.billingAddress().country() + ) : null; + + List orderLines = request.orderLines().stream() + .map(this::toOrderLineDetails) + .toList(); + + return new CreateOrderCommand( + UserId.of(request.userId()), + request.currency(), + customerDetails, + shippingAddress, + billingAddress, + orderLines + ); + } + + private OrderLineDetails toOrderLineDetails(CreateOrderLineRequest request) { + return new OrderLineDetails( + ProductId.of(request.productId()), + VariantId.of(request.variantId()), + request.quantity() + ); + } + + public OrderDetailsResponse toResponse(OrderDetailsDTO dto) { + return new OrderDetailsResponse( + dto.id().toString(), + dto.userId().toString(), + dto.status().toString(), + dto.currency(), + new MoneyResponse( + dto.totalAmount().getAmount().doubleValue(), + dto.totalAmount().getCurrency() + ), + dto.orderLines().stream() + .map(this::toOrderLineResponse) + .collect(Collectors.toList()), + toCustomerDetailsResponse(dto.customerDetails()), + toAddressResponse(dto.shippingAddress()), + dto.billingAddress() != null ? + toAddressResponse(dto.billingAddress()) : null, + dto.createdAt() + ); + } + + public PagedOrderResponse toPagedResponse(Page orderPage) { + List orders = orderPage.getContent().stream() + .map(this::toSummaryResponse) + .collect(Collectors.toList()); + + return new PagedOrderResponse( + orders, + new PageInfo( + orderPage.getNumber(), + orderPage.getSize(), + orderPage.getTotalElements(), + orderPage.getTotalPages() + ) + ); + } + + private OrderSummaryResponse toSummaryResponse(Order order) { + return new OrderSummaryResponse( + order.getId().toString(), + order.getUserId().toString(), + order.getStatus().toString(), + new MoneyResponse( + order.getTotalAmount().getAmount().doubleValue(), + order.getTotalAmount().getCurrency() + ), + order.getCreatedAt() + ); + } + + private OrderLineResponse toOrderLineResponse(OrderLineDTO line) { + return new OrderLineResponse( + line.id().toString(), + line.productId().toString(), + line.variantId().toString(), + line.quantity(), + new MoneyResponse( + line.unitPrice().getAmount().doubleValue(), + line.unitPrice().getCurrency() + ), + new MoneyResponse( + line.total().getAmount().doubleValue(), + line.total().getCurrency() + ) + ); + } + + private CustomerDetailsResponse toCustomerDetailsResponse(CustomerDetails details) { + return new CustomerDetailsResponse( + details.firstName(), + details.lastName(), + details.email(), + details.phone() + ); + } + + private AddressResponse toAddressResponse(Address address) { + return new AddressResponse( + address.street(), + address.city(), + address.state(), + address.zipCode(), + address.country() + ); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java index 8fe4eb0..8434164 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java @@ -1,6 +1,6 @@ package com.zenfulcode.commercify.api.product; -import com.zenfulcode.commercify.api.product.dto.ProductDtoMapper; +import com.zenfulcode.commercify.api.product.mapper.ProductDtoMapper; import com.zenfulcode.commercify.api.product.dto.request.*; import com.zenfulcode.commercify.api.product.dto.response.CreateProductResponse; import com.zenfulcode.commercify.api.product.dto.response.PagedProductResponse; diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java similarity index 98% rename from src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java rename to src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java index d6b17c2..bd3a182 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/ProductDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.api.product.dto; +package com.zenfulcode.commercify.api.product.mapper; import com.zenfulcode.commercify.api.product.dto.request.*; import com.zenfulcode.commercify.api.product.dto.response.ProductDetailResponse; diff --git a/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java index 79c640c..fa260e0 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java @@ -12,9 +12,16 @@ import java.util.List; import java.util.stream.Collectors; -public record OrderDetailsDTO(OrderId id, UserId userId, OrderStatus status, String currency, Money totalAmount, - List orderLines, CustomerDetails customerDetails, Address shippingAddress, - Address billingAddress, Instant createdAt) { +public record OrderDetailsDTO(OrderId id, + UserId userId, + OrderStatus status, + String currency, + Money totalAmount, + List orderLines, + CustomerDetails customerDetails, + Address shippingAddress, + Address billingAddress, + Instant createdAt) { public static OrderDetailsDTO fromOrder(Order order) { return new OrderDetailsDTO( order.getId(), diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java index 93eb85c..e62e0aa 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineDetails.java @@ -3,7 +3,6 @@ import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.product.domain.valueobject.VariantId; -import com.zenfulcode.commercify.shared.domain.model.Money; import java.util.ArrayList; import java.util.List; @@ -11,8 +10,7 @@ public record OrderLineDetails( ProductId productId, VariantId variantId, - int quantity, - Money unitPrice + int quantity ) { public OrderLineDetails { validate(productId, quantity); @@ -34,10 +32,6 @@ private void validate(ProductId productId, int quantity) { } } - public Money calculateTotal() { - return unitPrice.multiply(quantity); - } - public boolean hasVariant() { return variantId != null; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3248f42..bb8c359 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,10 +5,10 @@ spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATA spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=none # Migrations spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml -spring.liquibase.enabled=false +#spring.liquibase.enabled=false #The secret key must be an HMAC hash string of 256 bits; security.jwt.secret-key=${JWT_SECRET_KEY} # 1h in millisecond diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index cd2ae3e..18b3635 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -3,15 +3,4 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241108185143-changelog.xml b/src/main/resources/db/changelog/migrations/241108185143-changelog.xml deleted file mode 100644 index 4c9bca1..0000000 --- a/src/main/resources/db/changelog/migrations/241108185143-changelog.xml +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241115001827-changelog.xml b/src/main/resources/db/changelog/migrations/241115001827-changelog.xml deleted file mode 100644 index f296de3..0000000 --- a/src/main/resources/db/changelog/migrations/241115001827-changelog.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241115135357-changelog.xml b/src/main/resources/db/changelog/migrations/241115135357-changelog.xml deleted file mode 100644 index 3f94a1f..0000000 --- a/src/main/resources/db/changelog/migrations/241115135357-changelog.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241115144659-changelog.xml b/src/main/resources/db/changelog/migrations/241115144659-changelog.xml deleted file mode 100644 index bf7afd6..0000000 --- a/src/main/resources/db/changelog/migrations/241115144659-changelog.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241121174307-changelog.xml b/src/main/resources/db/changelog/migrations/241121174307-changelog.xml deleted file mode 100644 index 9b08163..0000000 --- a/src/main/resources/db/changelog/migrations/241121174307-changelog.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241127144629-changelog.xml b/src/main/resources/db/changelog/migrations/241127144629-changelog.xml deleted file mode 100644 index 8ffedfa..0000000 --- a/src/main/resources/db/changelog/migrations/241127144629-changelog.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241201215506-changelog.xml b/src/main/resources/db/changelog/migrations/241201215506-changelog.xml deleted file mode 100644 index d814ca7..0000000 --- a/src/main/resources/db/changelog/migrations/241201215506-changelog.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241223151914-changelog.xml b/src/main/resources/db/changelog/migrations/241223151914-changelog.xml deleted file mode 100644 index bdd2e53..0000000 --- a/src/main/resources/db/changelog/migrations/241223151914-changelog.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241230175909-changelog.xml b/src/main/resources/db/changelog/migrations/241230175909-changelog.xml deleted file mode 100644 index 0513578..0000000 --- a/src/main/resources/db/changelog/migrations/241230175909-changelog.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/241230180613-changelog.xml b/src/main/resources/db/changelog/migrations/241230180613-changelog.xml deleted file mode 100644 index d9c5b64..0000000 --- a/src/main/resources/db/changelog/migrations/241230180613-changelog.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - \ No newline at end of file From 77ff1a664c6edb51665e6162f1cc7f6deb68e410 Mon Sep 17 00:00:00 2001 From: GustavH Date: Fri, 3 Jan 2025 22:12:58 +0100 Subject: [PATCH 07/57] Initial database migration --- .../commercify/order/domain/model/Order.java | 1 - .../order/domain/model/OrderShippingInfo.java | 2 +- .../product/domain/model/VariantOption.java | 2 +- .../db/changelog/db.changelog-master.xml | 1 + .../migrations/250103220503-changelog.sql | 139 ++++++++++++++++++ 5 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/db/changelog/migrations/250103220503-changelog.sql diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java index 596a3e3..31acfa0 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -27,7 +27,6 @@ public class Order extends AggregateRoot { @EmbeddedId private OrderId id; - @Column(name = "user_id") private UserId userId; @OneToMany( diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java index b81f7a6..b2ad29a 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java @@ -14,7 +14,7 @@ public class OrderShippingInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Long orderInfoId; @Column(name = "customer_first_name") private String customerFirstName; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java index 488786e..f287ca0 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java @@ -14,7 +14,7 @@ public class VariantOption { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Long optionId; @Column(nullable = false) private String name; diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index 18b3635..27fbfa8 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -3,4 +3,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/250103220503-changelog.sql b/src/main/resources/db/changelog/migrations/250103220503-changelog.sql new file mode 100644 index 0000000..4bc57ae --- /dev/null +++ b/src/main/resources/db/changelog/migrations/250103220503-changelog.sql @@ -0,0 +1,139 @@ +-- liquibase formatted sql + +-- changeset gkhaavik:1735938302939-1 +CREATE TABLE domain_events +( + eventId VARCHAR(255) NOT NULL, + eventType VARCHAR(255) NOT NULL, + eventData TEXT NOT NULL, + occurredOn datetime NOT NULL, + aggregateId VARCHAR(255) NULL, + aggregateType VARCHAR(255) NULL, + CONSTRAINT pk_domain_events PRIMARY KEY (eventId) +); + +-- changeset gkhaavik:1735938302939-2 +CREATE TABLE order_shipping_info +( + id BIGINT AUTO_INCREMENT NOT NULL, + customer_first_name VARCHAR(255) NULL, + customer_last_name VARCHAR(255) NULL, + customer_email VARCHAR(255) NULL, + customer_phone VARCHAR(255) NULL, + shipping_street VARCHAR(255) NOT NULL, + shipping_city VARCHAR(255) NOT NULL, + shipping_state VARCHAR(255) NULL, + shipping_zip VARCHAR(255) NOT NULL, + shipping_country VARCHAR(255) NOT NULL, + billing_street VARCHAR(255) NULL, + billing_city VARCHAR(255) NULL, + billing_state VARCHAR(255) NULL, + billing_zip VARCHAR(255) NULL, + billing_country VARCHAR(255) NULL, + CONSTRAINT pk_order_shipping_info PRIMARY KEY (id) +); + +-- changeset gkhaavik:1735938302939-3 +CREATE TABLE products +( + product_id VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NULL, + stock INT NOT NULL, + image_url VARCHAR(255) NULL, + active BIT(1) NOT NULL, + unit_price DECIMAL(19, 2) NULL, + currency VARCHAR(255) NULL, + category_id VARCHAR(255) NULL, + CONSTRAINT pk_products PRIMARY KEY (product_id) +); + +-- changeset gkhaavik:1735938302939-4 +CREATE TABLE product_variants +( + id VARCHAR(255) NOT NULL, + product_id VARCHAR(255) NOT NULL, + sku VARCHAR(255) NOT NULL, + stock INT NULL, + image_url VARCHAR(255) NULL, + unit_price DECIMAL(19, 2) NULL, + currency VARCHAR(255) NULL, + CONSTRAINT pk_product_variants PRIMARY KEY (id), + CONSTRAINT uc_product_variants_sku UNIQUE (sku) +); + +-- changeset gkhaavik:1735938302939-5 +CREATE TABLE variant_options +( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(255) NOT NULL, + value VARCHAR(255) NOT NULL, + product_variant_id VARCHAR(255) NOT NULL, + CONSTRAINT pk_variant_options PRIMARY KEY (id) +); + +-- changeset gkhaavik:1735938302939-6 +CREATE TABLE orders +( + order_id VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NULL, + status VARCHAR(255) NOT NULL, + currency VARCHAR(255) NULL, + subtotal DECIMAL(19, 2) NULL, + shipping_cost DECIMAL(19, 2) NULL, + tax DECIMAL(19, 2) NULL, + total_amount DECIMAL(19, 2) NULL, + order_shipping_info_id BIGINT NULL, + created_at datetime NOT NULL, + updated_at datetime NULL, + CONSTRAINT pk_orders PRIMARY KEY (order_id) +); + +-- changeset gkhaavik:1735938302939-7 +CREATE TABLE order_lines +( + orderline_id VARCHAR(255) NOT NULL, + order_id VARCHAR(255) NOT NULL, + product_id VARCHAR(255) NOT NULL, + product_variant_id VARCHAR(255) NULL, + quantity INT NOT NULL, + unit_price DECIMAL(19, 2) NULL, + currency VARCHAR(255) NULL, + CONSTRAINT pk_order_lines PRIMARY KEY (orderline_id) +); + +-- changeset gkhaavik:1735938302939-8 +ALTER TABLE product_variants + ADD CONSTRAINT FK_PRODUCT_VARIANTS_ON_PRODUCT + FOREIGN KEY (product_id) + REFERENCES products (product_id); + +-- changeset gkhaavik:1735938302939-9 +ALTER TABLE variant_options + ADD CONSTRAINT FK_VARIANT_OPTIONS_ON_VARIANT + FOREIGN KEY (product_variant_id) + REFERENCES product_variants (id); + +-- changeset gkhaavik:1735938302939-10 +ALTER TABLE orders + ADD CONSTRAINT FK_ORDERS_ON_ORDER_SHIPPING_INFO + FOREIGN KEY (order_shipping_info_id) + REFERENCES order_shipping_info (id); + +-- changeset gkhaavik:1735938302939-11 +ALTER TABLE order_lines + ADD CONSTRAINT FK_ORDER_LINES_ON_ORDER + FOREIGN KEY (order_id) + REFERENCES orders (order_id); + +-- changeset gkhaavik:1735938302939-12 +ALTER TABLE order_lines + ADD CONSTRAINT FK_ORDER_LINES_ON_VARIANT + FOREIGN KEY (product_variant_id) + REFERENCES product_variants (id); + +-- changeset gkhaavik:1735938302939-13 +ALTER TABLE order_lines + ADD CONSTRAINT FK_ORDER_LINES_ON_PRODUCT + FOREIGN KEY (product_id) + REFERENCES products (product_id); \ No newline at end of file From 9550612dfd849256a339846245e63e05a30418fc Mon Sep 17 00:00:00 2001 From: GustavH Date: Fri, 3 Jan 2025 22:44:04 +0100 Subject: [PATCH 08/57] start user module implementation --- .../command/CreateUserCommand.java | 13 ++ .../command/UpdateUserCommand.java | 9 + .../command/UpdateUserStatusCommand.java | 10 + .../user/application/dto/UserUpdateSpec.java | 29 +++ .../service/UserApplicationService.java | 177 ++++++++++++++++++ .../user/domain/event/UserCreatedEvent.java | 21 +++ .../domain/event/UserStatusChangedEvent.java | 25 +++ .../exception/InvalidUserStateException.java | 27 +++ .../exception/UserNotFoundException.java | 20 ++ .../commercify/user/domain/model/User.java | 172 +++++++++++++++++ .../user/domain/model/UserRole.java | 9 + .../user/domain/model/UserStatus.java | 8 + .../domain/repository/UserRepository.java | 25 +++ .../domain/service/UserDomainService.java | 87 +++++++++ .../persistence/JpaUserRepository.java | 53 ++++++ .../SpringDataJpaUserRepository.java | 20 ++ 16 files changed, 705 insertions(+) create mode 100644 src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserStatusCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/application/dto/UserUpdateSpec.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/exception/InvalidUserStateException.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/model/User.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/model/UserRole.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/model/UserStatus.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/repository/UserRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/JpaUserRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/SpringDataJpaUserRepository.java diff --git a/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java b/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java new file mode 100644 index 0000000..d8ea01d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.user.application.command; + +import com.zenfulcode.commercify.user.domain.model.UserRole; + +import java.util.Set; + +public record CreateUserCommand( + String email, + String firstName, + String lastName, + String password, + Set roles +) {} diff --git a/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserCommand.java b/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserCommand.java new file mode 100644 index 0000000..405ff8f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserCommand.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.user.application.command; + +import com.zenfulcode.commercify.user.application.dto.UserUpdateSpec; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; + +public record UpdateUserCommand( + UserId userId, + UserUpdateSpec updateSpec +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserStatusCommand.java b/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserStatusCommand.java new file mode 100644 index 0000000..ab64538 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserStatusCommand.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.user.application.command; + +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; + +public record UpdateUserStatusCommand( + UserId userId, + UserStatus newStatus +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/user/application/dto/UserUpdateSpec.java b/src/main/java/com/zenfulcode/commercify/user/application/dto/UserUpdateSpec.java new file mode 100644 index 0000000..ec89a66 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/application/dto/UserUpdateSpec.java @@ -0,0 +1,29 @@ +package com.zenfulcode.commercify.user.application.dto; + +import com.zenfulcode.commercify.user.domain.model.UserRole; + +import java.util.Set; + +public record UserUpdateSpec( + String firstName, + String lastName, + String email, + String phoneNumber, + Set roles +) { + public boolean hasNameUpdate() { + return firstName != null || lastName != null; + } + + public boolean hasEmailUpdate() { + return email != null; + } + + public boolean hasPhoneUpdate() { + return phoneNumber != null; + } + + public boolean hasRolesUpdate() { + return roles != null; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java new file mode 100644 index 0000000..7117801 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java @@ -0,0 +1,177 @@ +package com.zenfulcode.commercify.user.application.service; + +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.user.application.command.CreateUserCommand; +import com.zenfulcode.commercify.user.application.command.UpdateUserCommand; +import com.zenfulcode.commercify.user.application.command.UpdateUserStatusCommand; +import com.zenfulcode.commercify.user.domain.exception.UserNotFoundException; +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.repository.UserRepository; +import com.zenfulcode.commercify.user.domain.service.UserDomainService; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserApplicationService { + private final UserDomainService userDomainService; + private final UserRepository userRepository; + private final DomainEventPublisher eventPublisher; + private final PasswordEncoder passwordEncoder; + + /** + * Creates a new user + */ + @Transactional + public UserId createUser(CreateUserCommand command) { + // Hash the password + String hashedPassword = passwordEncoder.encode(command.password()); + + // Create the user through domain service + User user = userDomainService.createUser( + command.email(), + command.firstName(), + command.lastName(), + hashedPassword, + command.roles() + ); + + // Save the user + User savedUser = userRepository.save(user); + + // Publish domain events + eventPublisher.publish(user.getDomainEvents()); + + return savedUser.getId(); + } + + /** + * Updates an existing user + */ + @Transactional + public void updateUser(UpdateUserCommand command) { + // Retrieve user + User user = userRepository.findById(command.userId()) + .orElseThrow(() -> new UserNotFoundException(command.userId())); + + // Update through domain service + userDomainService.updateUser(user, command.updateSpec()); + + // Save changes + userRepository.save(user); + + // Publish events + eventPublisher.publish(user.getDomainEvents()); + } + + /** + * Updates user status (activate/deactivate) + */ + @Transactional + public void updateUserStatus(UpdateUserStatusCommand command) { + User user = userRepository.findById(command.userId()) + .orElseThrow(() -> new UserNotFoundException(command.userId())); + + userDomainService.updateUserStatus(user, command.newStatus()); + userRepository.save(user); + eventPublisher.publish(user.getDomainEvents()); + } + + /** + * Activates a user + */ + @Transactional + public void activateUser(UserId userId) { + updateUserStatus(new UpdateUserStatusCommand(userId, UserStatus.ACTIVE)); + } + + /** + * Deactivates a user + */ + @Transactional + public void deactivateUser(UserId userId) { + updateUserStatus(new UpdateUserStatusCommand(userId, UserStatus.INACTIVE)); + } + + /** + * Gets a user by ID + */ + @Transactional(readOnly = true) + public User getUser(UserId userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } + + /** + * Gets a user by email + */ + @Transactional(readOnly = true) + public User getUserByEmail(String email) { + return userRepository.findByEmail(email) + .orElseThrow(() -> new UserNotFoundException("User not found with email: " + email)); + } + + /** + * Lists all users with pagination + */ + @Transactional(readOnly = true) + public Page getAllUsers(Pageable pageable) { + return userRepository.findAll(pageable); + } + + /** + * Lists active users with pagination + */ + @Transactional(readOnly = true) + public Page getActiveUsers(Pageable pageable) { + return userRepository.findByStatus(UserStatus.ACTIVE, pageable); + } + + /** + * Checks if email exists + */ + @Transactional(readOnly = true) + public boolean emailExists(String email) { + return userRepository.existsByEmail(email); + } + + /** + * Changes user password + */ + @Transactional + public void changePassword(UserId userId, String currentPassword, String newPassword) { + User user = getUser(userId); + + // Verify current password + if (!passwordEncoder.matches(currentPassword, user.getPassword())) { + throw new IllegalArgumentException("Current password is incorrect"); + } + + // Hash new password and update + String hashedPassword = passwordEncoder.encode(newPassword); + userDomainService.updatePassword(user, hashedPassword); + + userRepository.save(user); + eventPublisher.publish(user.getDomainEvents()); + } + + /** + * Resets user password (admin function) + */ + @Transactional + public void resetPassword(UserId userId, String newPassword) { + User user = getUser(userId); + + String hashedPassword = passwordEncoder.encode(newPassword); + userDomainService.updatePassword(user, hashedPassword); + + userRepository.save(user); + eventPublisher.publish(user.getDomainEvents()); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java new file mode 100644 index 0000000..4b8858b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java @@ -0,0 +1,21 @@ +package com.zenfulcode.commercify.user.domain.event; + +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.Getter; + +@Getter +public class UserCreatedEvent extends DomainEvent { + @AggregateId + private final UserId userId; + private final String email; + private final UserStatus status; + + public UserCreatedEvent(UserId userId, String email, UserStatus status) { + this.userId = userId; + this.email = email; + this.status = status; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java new file mode 100644 index 0000000..f60488d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java @@ -0,0 +1,25 @@ +package com.zenfulcode.commercify.user.domain.event; + +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.Getter; + +@Getter +public class UserStatusChangedEvent extends DomainEvent { + @AggregateId + private final UserId userId; + private final UserStatus oldStatus; + private final UserStatus newStatus; + + public UserStatusChangedEvent( + UserId userId, + UserStatus oldStatus, + UserStatus newStatus + ) { + this.userId = userId; + this.oldStatus = oldStatus; + this.newStatus = newStatus; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/exception/InvalidUserStateException.java b/src/main/java/com/zenfulcode/commercify/user/domain/exception/InvalidUserStateException.java new file mode 100644 index 0000000..abd1dac --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/exception/InvalidUserStateException.java @@ -0,0 +1,27 @@ +package com.zenfulcode.commercify.user.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.Getter; + +@Getter +public class InvalidUserStateException extends DomainException { + private final UserId userId; + private final UserStatus currentStatus; + private final UserStatus targetStatus; + + public InvalidUserStateException( + UserId userId, + UserStatus currentStatus, + UserStatus targetStatus, + String message + ) { + super(String.format("Invalid user status transition from %s to %s for user %s: %s", + currentStatus, targetStatus, userId, message)); + + this.userId = userId; + this.currentStatus = currentStatus; + this.targetStatus = targetStatus; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java new file mode 100644 index 0000000..c32d23d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java @@ -0,0 +1,20 @@ +package com.zenfulcode.commercify.user.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.Getter; + +@Getter +public class UserNotFoundException extends DomainException { + private final Object identifier; + + public UserNotFoundException(UserId userId) { + super("User not found with ID: " + userId); + this.identifier = userId; + } + + public UserNotFoundException(String message) { + super(message); + this.identifier = null; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java new file mode 100644 index 0000000..c4610bc --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -0,0 +1,172 @@ +package com.zenfulcode.commercify.user.domain.model; + +import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; +import com.zenfulcode.commercify.user.domain.event.UserCreatedEvent; +import com.zenfulcode.commercify.user.domain.event.UserStatusChangedEvent; +import com.zenfulcode.commercify.user.domain.exception.InvalidUserStateException; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.Instant; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +@Entity +@Table(name = "users") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends AggregateRoot { + @EmbeddedId + private UserId id; + + @Column(nullable = false, unique = true) + private String email; + + @Column(nullable = false) + private String firstName; + + @Column(nullable = false) + private String lastName; + + @Column(nullable = false) + private String password; + + private String phoneNumber; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private UserStatus status; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "user_roles", + joinColumns = @JoinColumn(name = "user_id") + ) + @Enumerated(EnumType.STRING) + @Column(name = "role") + private Set roles = new HashSet<>(); + + @CreationTimestamp + @Column(nullable = false) + private Instant createdAt; + + @UpdateTimestamp + @Column(nullable = false) + private Instant updatedAt; + + private Instant lastLoginAt; + + // Factory method + public static User create( + String email, + String firstName, + String lastName, + String password, + Set roles + ) { + User user = new User(); + user.id = UserId.generate(); + user.email = Objects.requireNonNull(email, "Email is required").toLowerCase(); + user.firstName = Objects.requireNonNull(firstName, "First name is required"); + user.lastName = Objects.requireNonNull(lastName, "Last name is required"); + user.password = Objects.requireNonNull(password, "Password is required"); + user.status = UserStatus.PENDING; + user.roles = new HashSet<>(roles != null ? roles : Set.of(UserRole.USER)); + + // Register domain event + user.registerEvent(new UserCreatedEvent( + user.getId(), + user.getEmail(), + user.getStatus() + )); + + return user; + } + + // Domain methods + public void updateProfile(String firstName, String lastName, String phoneNumber) { + if (firstName != null) { + this.firstName = firstName; + } + if (lastName != null) { + this.lastName = lastName; + } + if (phoneNumber != null) { + this.phoneNumber = phoneNumber; + } + } + + public void updateEmail(String newEmail) { + this.email = Objects.requireNonNull(newEmail, "Email is required").toLowerCase(); + } + + public void updatePassword(String newPassword) { + this.password = Objects.requireNonNull(newPassword, "Password is required"); + } + + public void updateStatus(UserStatus newStatus) { + if (!canTransitionTo(newStatus)) { + throw new InvalidUserStateException( + id, + status, + newStatus, + "Invalid status transition" + ); + } + + UserStatus oldStatus = this.status; + this.status = newStatus; + + registerEvent(new UserStatusChangedEvent( + this.id, + oldStatus, + newStatus + )); + } + + public void updateRoles(Set newRoles) { + if (newRoles == null || newRoles.isEmpty()) { + throw new IllegalArgumentException("User must have at least one role"); + } + this.roles = new HashSet<>(newRoles); + } + + public void recordLogin() { + this.lastLoginAt = Instant.now(); + } + + public boolean hasRole(UserRole role) { + return roles.contains(role); + } + + public boolean isActive() { + return status == UserStatus.ACTIVE; + } + + private boolean canTransitionTo(UserStatus newStatus) { + return switch (status) { + case PENDING -> Set.of(UserStatus.ACTIVE, UserStatus.INACTIVE).contains(newStatus); + case ACTIVE -> Set.of(UserStatus.INACTIVE, UserStatus.SUSPENDED).contains(newStatus); + case INACTIVE -> Set.of(UserStatus.ACTIVE).contains(newStatus); + case SUSPENDED -> Set.of(UserStatus.ACTIVE, UserStatus.INACTIVE).contains(newStatus); + }; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User user)) return false; + return Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/UserRole.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/UserRole.java new file mode 100644 index 0000000..7dbd5dd --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/UserRole.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.user.domain.model; + +public enum UserRole { + GUEST, // Guest user + USER, // Standard user + ADMIN, // Administrator + MANAGER, // Store manager + SUPPORT // Customer support +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/UserStatus.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/UserStatus.java new file mode 100644 index 0000000..db2143f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/UserStatus.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.user.domain.model; + +public enum UserStatus { + PENDING, // Initial status after registration + ACTIVE, // Normal active user + INACTIVE, // User deactivated (by self or admin) + SUSPENDED // User suspended (by admin) +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/repository/UserRepository.java b/src/main/java/com/zenfulcode/commercify/user/domain/repository/UserRepository.java new file mode 100644 index 0000000..93275b9 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/repository/UserRepository.java @@ -0,0 +1,25 @@ +package com.zenfulcode.commercify.user.domain.repository; + +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface UserRepository { + User save(User user); + + Optional findById(UserId id); + + Optional findByEmail(String email); + + boolean existsByEmail(String email); + + Page findAll(Pageable pageable); + + Page findByStatus(UserStatus status, Pageable pageable); + + void delete(User user); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java new file mode 100644 index 0000000..89f96f2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java @@ -0,0 +1,87 @@ +package com.zenfulcode.commercify.user.domain.service; + +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.user.application.dto.UserUpdateSpec; +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserRole; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class UserDomainService { + private final UserRepository userRepository; + private final DomainEventPublisher domainEventPublisher; + + /** + * Creates a new user with domain logic and validation + */ + public User createUser( + String email, + String firstName, + String lastName, + String hashedPassword, + Set roles + ) { + // Validate that email is unique + if (userRepository.existsByEmail(email)) { + throw new IllegalArgumentException("Email already exists: " + email); + } + + return User.create( + email, + firstName, + lastName, + hashedPassword, + roles + ); + } + + /** + * Updates user details with domain validation + */ + public void updateUser(User user, UserUpdateSpec updateSpec) { + // Validate email uniqueness if email is being changed + if (updateSpec.email() != null && !updateSpec.email().equals(user.getEmail())) { + if (userRepository.existsByEmail(updateSpec.email())) { + throw new IllegalArgumentException("Email already exists: " + updateSpec.email()); + } + } + + // Update user details + user.update(updateSpec); + } + + /** + * Updates user status with domain logic + */ + public void updateUserStatus(User user, UserStatus newStatus) { + // Update user status + user.updateStatus(newStatus); + } + + /** + * Updates user password with domain logic + */ + public void updatePassword(User user, String newHashedPassword) { + // Validate password complexity (example validation) + validatePasswordComplexity(newHashedPassword); + + // Update password + user.updatePassword(newHashedPassword); + } + + /** + * Validate password complexity (example implementation) + */ + private void validatePasswordComplexity(String hashedPassword) { + // Example basic validation - can be expanded + if (hashedPassword == null || hashedPassword.length() < 8) { + throw new IllegalArgumentException("Password is too short"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/JpaUserRepository.java b/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/JpaUserRepository.java new file mode 100644 index 0000000..044841d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/JpaUserRepository.java @@ -0,0 +1,53 @@ +package com.zenfulcode.commercify.user.infrastructure.persistence; + +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.repository.UserRepository; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class JpaUserRepository implements UserRepository { + private final SpringDataJpaUserRepository repository; + + @Override + public User save(User user) { + return repository.save(user); + } + + @Override + public Optional findById(UserId id) { + return repository.findById(id); + } + + @Override + public Optional findByEmail(String email) { + return repository.findByEmailIgnoreCase(email); + } + + @Override + public boolean existsByEmail(String email) { + return repository.existsByEmailIgnoreCase(email); + } + + @Override + public Page findAll(Pageable pageable) { + return repository.findAll(pageable); + } + + @Override + public Page findByStatus(UserStatus status, Pageable pageable) { + return repository.findByStatus(status, pageable); + } + + @Override + public void delete(User user) { + repository.delete(user); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/SpringDataJpaUserRepository.java b/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/SpringDataJpaUserRepository.java new file mode 100644 index 0000000..f05764a --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/SpringDataJpaUserRepository.java @@ -0,0 +1,20 @@ +package com.zenfulcode.commercify.user.infrastructure.persistence; + +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +interface SpringDataJpaUserRepository extends JpaRepository { + Optional findByEmailIgnoreCase(String email); + + boolean existsByEmailIgnoreCase(String email); + + Page findByStatus(UserStatus status, Pageable pageable); +} \ No newline at end of file From 2de2cc7820c0960e858bfc09de251e0d564522a2 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 4 Jan 2025 12:25:12 +0100 Subject: [PATCH 09/57] adding UsreDomainService and cleaning up OrderDomainService implementing OrderStateFlow usage inside OrderValidationService --- .../commercify/order/domain/model/Order.java | 28 +-- .../domain/repository/OrderRepository.java | 4 +- .../domain/service/OrderDomainService.java | 11 -- .../service/OrderValidationService.java | 8 +- .../persistence/JpaOrderRepository.java | 5 + .../product/domain/model/ProductVariant.java | 12 +- .../exception/UserValidationException.java | 11 ++ .../service/DefaultUserValidationPolicy.java | 25 +++ .../domain/service/UserDomainService.java | 179 +++++++++++++++--- .../domain/service/UserValidationPolicy.java | 8 + 10 files changed, 217 insertions(+), 74 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/exception/UserValidationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/service/DefaultUserValidationPolicy.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationPolicy.java diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java index 31acfa0..9238d68 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -2,8 +2,8 @@ import com.zenfulcode.commercify.order.domain.event.OrderCreatedEvent; import com.zenfulcode.commercify.order.domain.event.OrderStatusChangedEvent; -import com.zenfulcode.commercify.order.domain.exception.InvalidOrderStateTransitionException; import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; +import com.zenfulcode.commercify.order.domain.service.OrderStateFlow; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; import com.zenfulcode.commercify.shared.domain.model.Money; @@ -120,15 +120,6 @@ public void removeOrderLine(OrderLine orderLine) { } public void updateStatus(OrderStatus newStatus) { - if (!canTransitionTo(newStatus)) { - throw new InvalidOrderStateTransitionException( - id, - status, - newStatus, - "Invalid order status transition" - ); - } - OrderStatus oldStatus = this.status; this.status = newStatus; @@ -169,23 +160,18 @@ private void validateSameCurrency(Money money) { } } - private boolean canTransitionTo(OrderStatus newStatus) { - return switch (status) { - case PENDING -> Set.of(OrderStatus.CONFIRMED, OrderStatus.CANCELLED).contains(newStatus); - case CONFIRMED -> Set.of(OrderStatus.SHIPPED, OrderStatus.CANCELLED).contains(newStatus); - case SHIPPED -> Set.of(OrderStatus.COMPLETED, OrderStatus.RETURNED).contains(newStatus); - default -> false; // Terminal states - }; - } - private void recalculateTotal() { this.totalAmount = orderLines.stream() .map(OrderLine::getTotal) .reduce(Money.zero(currency), Money::add); } - public boolean canBeCancelled() { - return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED; + public boolean isInTerminalState(OrderStateFlow stateFlow) { + return stateFlow.isTerminalState(status); + } + + public Set getValidTransitions(OrderStateFlow stateFlow) { + return stateFlow.getValidTransitions(status); } public boolean isCompleted() { diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java index d91af08..9e593b1 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java @@ -12,10 +12,12 @@ public interface OrderRepository { Order save(Order order); Optional findById(OrderId id); - + Page findByUserId(UserId userId, PageRequest pageRequest); Page findAll(PageRequest pageRequest); boolean existsByIdAndUserId(OrderId id, UserId userId); + + boolean existsByUserId(UserId userId); } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index ab2516f..114225d 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -1,6 +1,5 @@ package com.zenfulcode.commercify.order.domain.service; -import com.zenfulcode.commercify.order.domain.exception.OrderValidationException; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.order.domain.model.OrderLine; import com.zenfulcode.commercify.order.domain.model.OrderShippingInfo; @@ -22,7 +21,6 @@ @Service @RequiredArgsConstructor public class OrderDomainService { - private final OrderStateFlow orderStateFlow; private final OrderPricingStrategy pricingStrategy; private final OrderValidationService validationService; @@ -111,13 +109,4 @@ private Money calculateLinePrice(Product product, ProductVariant variant, int qu product.getPrice(); return unitPrice.multiply(quantity); } - - private void validateOrderState(Order order) { - if (order.getOrderLines().isEmpty()) { - throw new OrderValidationException("Order must contain at least one item"); - } - if (order.getTotalAmount().isLessThanOrEqual(Money.zero(order.getCurrency()))) { - throw new OrderValidationException("Order total must be greater than zero"); - } - } } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java index fb1e9d8..f32568f 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java @@ -20,7 +20,7 @@ @Service @RequiredArgsConstructor public class OrderValidationService { - private final OrderStateFlow orderStateFlow; + private final OrderStateFlow stateFlow; public void validateCreateOrder(Order order) { List violations = new ArrayList<>(); @@ -56,7 +56,7 @@ private void validateOrderLines(Set orderLines, List violatio } public void validateStatusTransition(Order order, OrderStatus newStatus) { - if (!orderStateFlow.canTransition(order.getStatus(), newStatus)) { + if (!stateFlow.canTransition(order.getStatus(), newStatus)) { throw new InvalidOrderStateTransitionException( order.getId(), order.getStatus(), @@ -67,9 +67,9 @@ public void validateStatusTransition(Order order, OrderStatus newStatus) { } public void validateOrderCancellation(Order order) { - if (!order.canBeCancelled()) { + if (order.isInTerminalState(stateFlow)) { throw new OrderValidationException( - "Cannot cancel order in status: " + order.getStatus() + "Cannot cancel order in terminal status: " + order.getStatus() ); } } diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java index fa25f06..f15dedd 100644 --- a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java @@ -40,4 +40,9 @@ public Page findAll(PageRequest pageRequest) { public boolean existsByIdAndUserId(OrderId id, UserId userId) { return repository.existsByIdAndUserId(id, userId); } + + @Override + public boolean existsByUserId(UserId userId) { + return repository.findByUserId(userId, PageRequest.of(0, 1)).hasContent(); + } } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java index 5337cd2..32d8636 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java @@ -4,7 +4,6 @@ import com.zenfulcode.commercify.shared.domain.model.Money; import jakarta.persistence.*; import lombok.*; -import org.apache.commons.lang3.NotImplementedException; import java.util.HashSet; import java.util.Objects; @@ -89,11 +88,14 @@ public Money getEffectivePrice() { return price != null ? price : product.getPrice(); } + public void updatePrice(Money newPrice) { + this.price = Objects.requireNonNull(newPrice, "Price cannot be null"); + } + @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof ProductVariant)) return false; - ProductVariant that = (ProductVariant) o; + if (!(o instanceof ProductVariant that)) return false; return Objects.equals(sku, that.sku); } @@ -102,7 +104,5 @@ public int hashCode() { return Objects.hash(sku); } - public void updatePrice(Money newPrice) { - throw new NotImplementedException("update price has not been implemented"); - } + } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserValidationException.java b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserValidationException.java new file mode 100644 index 0000000..d2d79c6 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserValidationException.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.user.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainValidationException; + +import java.util.List; + +public class UserValidationException extends DomainValidationException { + public UserValidationException(List violations) { + super("User validation failed", violations); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/DefaultUserValidationPolicy.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/DefaultUserValidationPolicy.java new file mode 100644 index 0000000..6df6cac --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/DefaultUserValidationPolicy.java @@ -0,0 +1,25 @@ +package com.zenfulcode.commercify.user.domain.service; + +import com.zenfulcode.commercify.order.domain.repository.OrderRepository; +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DefaultUserValidationPolicy implements UserValidationPolicy { + private final OrderRepository orderRepository; + + @Override + public boolean canTransitionToStatus(User user, UserStatus newStatus) { + if (newStatus == UserStatus.INACTIVE || newStatus == UserStatus.SUSPENDED) { + // Check for active orders before allowing deactivation + // Only allow transition if user has no active orders + boolean hasActiveOrders = orderRepository.existsByUserId(user.getId()); + return !hasActiveOrders; + } + // Allow all other transitions + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java index 89f96f2..48794bb 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java @@ -1,7 +1,8 @@ package com.zenfulcode.commercify.user.domain.service; -import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import com.zenfulcode.commercify.user.application.dto.UserUpdateSpec; +import com.zenfulcode.commercify.user.domain.exception.InvalidUserStateException; +import com.zenfulcode.commercify.user.domain.exception.UserValidationException; import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.model.UserRole; import com.zenfulcode.commercify.user.domain.model.UserStatus; @@ -9,16 +10,26 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; import java.util.Set; +import java.util.regex.Pattern; @Service @RequiredArgsConstructor public class UserDomainService { + private static final Pattern EMAIL_PATTERN = Pattern.compile( + "^[A-Za-z0-9+_.-]+@(.+)$" + ); + private static final Pattern PHONE_PATTERN = Pattern.compile( + "^\\+?[1-9]\\d{1,14}$" + ); + private final UserRepository userRepository; - private final DomainEventPublisher domainEventPublisher; + private final UserValidationPolicy validationPolicy; /** - * Creates a new user with domain logic and validation + * Creates a new user with validation */ public User createUser( String email, @@ -27,10 +38,8 @@ public User createUser( String hashedPassword, Set roles ) { - // Validate that email is unique - if (userRepository.existsByEmail(email)) { - throw new IllegalArgumentException("Email already exists: " + email); - } + validateNewUser(email, firstName, lastName, hashedPassword); + validateRoles(roles); return User.create( email, @@ -42,46 +51,154 @@ public User createUser( } /** - * Updates user details with domain validation + * Updates user information */ public void updateUser(User user, UserUpdateSpec updateSpec) { - // Validate email uniqueness if email is being changed - if (updateSpec.email() != null && !updateSpec.email().equals(user.getEmail())) { - if (userRepository.existsByEmail(updateSpec.email())) { - throw new IllegalArgumentException("Email already exists: " + updateSpec.email()); + List violations = new ArrayList<>(); + + // Validate and apply name updates + if (updateSpec.hasNameUpdate()) { + if (updateSpec.firstName() != null) { + validateFirstName(updateSpec.firstName(), violations); + } + if (updateSpec.lastName() != null) { + validateLastName(updateSpec.lastName(), violations); } } - // Update user details - user.update(updateSpec); + // Validate and apply email update + if (updateSpec.hasEmailUpdate()) { + validateEmail(updateSpec.email(), violations); + validateEmailUniqueness(updateSpec.email(), user); + } + + // Validate and apply phone update + if (updateSpec.hasPhoneUpdate() && updateSpec.phoneNumber() != null) { + validatePhoneNumber(updateSpec.phoneNumber(), violations); + } + + // Validate and apply role updates + if (updateSpec.hasRolesUpdate()) { + validateRoles(updateSpec.roles()); + } + + if (!violations.isEmpty()) { + throw new UserValidationException(violations); + } + + // Apply updates + user.updateProfile( + updateSpec.firstName(), + updateSpec.lastName(), + updateSpec.phoneNumber() + ); + + if (updateSpec.hasEmailUpdate()) { + user.updateEmail(updateSpec.email()); + } + + if (updateSpec.hasRolesUpdate()) { + user.updateRoles(updateSpec.roles()); + } } /** - * Updates user status with domain logic + * Updates user's password */ - public void updateUserStatus(User user, UserStatus newStatus) { - // Update user status - user.updateStatus(newStatus); + public void updatePassword(User user, String hashedPassword) { + validatePassword(hashedPassword); + user.updatePassword(hashedPassword); } /** - * Updates user password with domain logic + * Updates user's status with validation */ - public void updatePassword(User user, String newHashedPassword) { - // Validate password complexity (example validation) - validatePasswordComplexity(newHashedPassword); + public void updateUserStatus(User user, UserStatus newStatus) { + validateStatusTransition(user, newStatus); + user.updateStatus(newStatus); + } + + private void validateNewUser(String email, String firstName, String lastName, String password) { + List violations = new ArrayList<>(); - // Update password - user.updatePassword(newHashedPassword); + validateEmail(email, violations); + validateFirstName(firstName, violations); + validateLastName(lastName, violations); + validatePassword(password); + validateEmailUniqueness(email, null); + + if (!violations.isEmpty()) { + throw new UserValidationException(violations); + } } - /** - * Validate password complexity (example implementation) - */ - private void validatePasswordComplexity(String hashedPassword) { - // Example basic validation - can be expanded - if (hashedPassword == null || hashedPassword.length() < 8) { - throw new IllegalArgumentException("Password is too short"); + private void validateEmail(String email, List violations) { + if (email == null || email.isBlank()) { + violations.add("Email is required"); + return; + } + + if (!EMAIL_PATTERN.matcher(email).matches()) { + violations.add("Invalid email format"); + } + } + + private void validateEmailUniqueness(String email, User existingUser) { + if (userRepository.findByEmail(email) + .map(user -> !user.equals(existingUser)) + .orElse(false)) { + throw new UserValidationException(List.of("Email already exists")); + } + } + + private void validateFirstName(String firstName, List violations) { + if (firstName == null || firstName.isBlank()) { + violations.add("First name is required"); + } else if (firstName.length() < 2 || firstName.length() > 50) { + violations.add("First name must be between 2 and 50 characters"); + } + } + + private void validateLastName(String lastName, List violations) { + if (lastName == null || lastName.isBlank()) { + violations.add("Last name is required"); + } else if (lastName.length() < 2 || lastName.length() > 50) { + violations.add("Last name must be between 2 and 50 characters"); + } + } + + private void validatePhoneNumber(String phoneNumber, List violations) { + if (phoneNumber != null && !phoneNumber.isBlank() && + !PHONE_PATTERN.matcher(phoneNumber).matches()) { + violations.add("Invalid phone number format"); + } + } + + private void validatePassword(String hashedPassword) { + if (hashedPassword == null || hashedPassword.isBlank()) { + throw new UserValidationException(List.of("Password is required")); + } + } + + private void validateRoles(Set roles) { + if (roles == null || roles.isEmpty()) { + throw new UserValidationException(List.of("User must have at least one role")); + } + + // Additional role validation logic can be added here + // For example, ensuring admin role assignments follow specific rules + } + + private void validateStatusTransition(User user, UserStatus newStatus) { + // Additional status transition validation logic can be added here + // For example, checking if user has pending orders before deactivation + if (validationPolicy.canTransitionToStatus(user, newStatus)) { + throw new InvalidUserStateException( + user.getId(), + user.getStatus(), + newStatus, + "Status transition not allowed by policy" + ); } } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationPolicy.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationPolicy.java new file mode 100644 index 0000000..e24b778 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationPolicy.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.user.domain.service; + +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; + +public interface UserValidationPolicy { + boolean canTransitionToStatus(User user, UserStatus newStatus); +} From 7e396678943b47e216827d3d1ae9f610d247c1f5 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 4 Jan 2025 13:17:19 +0100 Subject: [PATCH 10/57] UserDomainService implementating UserStateflow and UserValidationService usage --- .../commercify/config/SecurityConfig.java | 14 + .../event/ProductPriceUpdatedEvent.java | 20 ++ .../product/domain/model/Product.java | 14 + .../domain/service/ProductDomainService.java | 71 ++++- .../shared/domain/model/AggregateRoot.java | 2 +- .../command/CreateUserCommand.java | 6 +- .../command/UpdateUserCommand.java | 7 +- .../service/UserApplicationService.java | 18 +- .../domain/event/UserStatusChangedEvent.java | 18 +- .../InvalidUserStateTransitionException.java | 27 ++ .../exception/UserValidationException.java | 6 +- .../commercify/user/domain/model/User.java | 53 ++-- .../user/domain/model/UserStatus.java | 8 +- .../service/DefaultUserValidationPolicy.java | 25 -- .../domain/service/UserDomainService.java | 246 +++++++----------- .../user/domain/service/UserStateFlow.java | 49 ++++ .../domain/service/UserValidationPolicy.java | 8 - .../domain/service/UserValidationService.java | 120 +++++++++ .../valueobject/UserDeletionValidation.java | 8 + .../domain/valueobject/UserSpecification.java | 30 +++ .../valueobject}/UserUpdateSpec.java | 2 +- 21 files changed, 522 insertions(+), 230 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/exception/InvalidUserStateTransitionException.java delete mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/service/DefaultUserValidationPolicy.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/service/UserStateFlow.java delete mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationPolicy.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserDeletionValidation.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserSpecification.java rename src/main/java/com/zenfulcode/commercify/user/{application/dto => domain/valueobject}/UserUpdateSpec.java (90%) diff --git a/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java new file mode 100644 index 0000000..4c5960f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class SecurityConfig { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java new file mode 100644 index 0000000..8ca62ae --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java @@ -0,0 +1,20 @@ +package com.zenfulcode.commercify.product.domain.event; + + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import lombok.Getter; + +@Getter +public class ProductPriceUpdatedEvent extends DomainEvent { + @AggregateId + private final ProductId productId; + private final Money newPrice; + + public ProductPriceUpdatedEvent(ProductId productId, Money newPrice) { + this.productId = productId; + this.newPrice = newPrice; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java index f33186b..5d785a2 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java @@ -1,6 +1,7 @@ package com.zenfulcode.commercify.product.domain.model; import com.zenfulcode.commercify.product.domain.event.ProductCreatedEvent; +import com.zenfulcode.commercify.product.domain.event.ProductPriceUpdatedEvent; import com.zenfulcode.commercify.product.domain.exception.InsufficientStockException; import com.zenfulcode.commercify.product.domain.exception.ProductModificationException; import com.zenfulcode.commercify.product.domain.valueobject.CategoryId; @@ -20,6 +21,7 @@ @Entity @Table(name = "products") @Getter +@Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Product extends AggregateRoot { @@ -101,6 +103,11 @@ public void removeStock(int quantity) { public void updatePrice(Money newPrice) { this.price = Objects.requireNonNull(newPrice, "Price cannot be null"); + + registerEvent(new ProductPriceUpdatedEvent( + id, + newPrice + )); } public void activate() { @@ -139,6 +146,13 @@ public Money getEffectivePrice(ProductVariant variant) { variant.getPrice() : this.price; } + public void setName(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Product name cannot be empty"); + } + this.name = name; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java index 92ed4fe..8b2a69b 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java @@ -1,6 +1,8 @@ package com.zenfulcode.commercify.product.domain.service; import com.zenfulcode.commercify.order.domain.repository.OrderLineRepository; +import com.zenfulcode.commercify.product.domain.exception.InvalidPriceException; +import com.zenfulcode.commercify.product.domain.exception.ProductValidationException; import com.zenfulcode.commercify.product.domain.exception.VariantNotFoundException; import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.model.ProductVariant; @@ -11,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; @Service @RequiredArgsConstructor @@ -118,8 +121,72 @@ private void validatePriceUpdates(List updates) { }); } - public void updateProduct(Product product, ProductUpdateSpec productUpdateSpec) { - // TODO: Implement product update logic + public void updateProduct(Product product, ProductUpdateSpec updateSpec) { + // Validate the product is not null + Objects.requireNonNull(product, "Product cannot be null"); + Objects.requireNonNull(updateSpec, "Update specification cannot be null"); + + List violations = new ArrayList<>(); + + // Update name if specified + if (updateSpec.hasNameUpdate()) { + if (updateSpec.name() == null || updateSpec.name().isBlank()) { + violations.add("Product name cannot be empty"); + } else { + product.setName(updateSpec.name()); + } + } + + // Update description if specified + if (updateSpec.hasDescriptionUpdate()) { + product.setDescription(updateSpec.description()); + } + + // Update stock if specified + if (updateSpec.hasStockUpdate()) { + if (updateSpec.stock() < 0) { + violations.add("Stock cannot be negative"); + } else { + InventoryAdjustment adjustment = new InventoryAdjustment( + InventoryAdjustmentType.STOCK_CORRECTION, + updateSpec.stock(), + "Stock updated through product update" + ); + this.adjustInventory(product, adjustment); + } + } + + // Update price if specified + if (updateSpec.hasPriceUpdate()) { + if (updateSpec.price() == null || updateSpec.price().isNegative()) { + violations.add("Price must be non-negative"); + } else { + try { + pricingPolicy.applyDefaultPricing(product); + product.updatePrice(updateSpec.price()); + } catch (InvalidPriceException e) { + violations.add(e.getMessage()); + } + } + } + + // Update active status if specified + if (updateSpec.hasActiveUpdate()) { + if (updateSpec.active()) { + product.activate(); + } else { + product.deactivate(); + } + } + + // If there are any violations, throw an exception + if (!violations.isEmpty()) { + throw new ProductValidationException(violations); + } + + if (updateSpec.hasStockUpdate()) { + inventoryPolicy.initializeInventory(product); + } } } diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java b/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java index fcba37c..793732a 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/model/AggregateRoot.java @@ -10,7 +10,7 @@ public abstract class AggregateRoot { @Transient private final List domainEvents = new ArrayList<>(); - protected void registerEvent(DomainEvent event) { + public void registerEvent(DomainEvent event) { domainEvents.add(event); } diff --git a/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java b/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java index d8ea01d..e2f94b9 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java +++ b/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java @@ -9,5 +9,7 @@ public record CreateUserCommand( String firstName, String lastName, String password, - Set roles -) {} + Set roles, + String phoneNumber +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserCommand.java b/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserCommand.java index 405ff8f..0120ce5 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserCommand.java +++ b/src/main/java/com/zenfulcode/commercify/user/application/command/UpdateUserCommand.java @@ -1,9 +1,10 @@ package com.zenfulcode.commercify.user.application.command; -import com.zenfulcode.commercify.user.application.dto.UserUpdateSpec; import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import com.zenfulcode.commercify.user.domain.valueobject.UserSpecification; public record UpdateUserCommand( UserId userId, - UserUpdateSpec updateSpec -) {} \ No newline at end of file + UserSpecification userSpec +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java index 7117801..d36ae67 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java @@ -10,6 +10,7 @@ import com.zenfulcode.commercify.user.domain.repository.UserRepository; import com.zenfulcode.commercify.user.domain.service.UserDomainService; import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import com.zenfulcode.commercify.user.domain.valueobject.UserSpecification; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -33,15 +34,18 @@ public UserId createUser(CreateUserCommand command) { // Hash the password String hashedPassword = passwordEncoder.encode(command.password()); - // Create the user through domain service - User user = userDomainService.createUser( - command.email(), + UserSpecification userSpecification = new UserSpecification( command.firstName(), command.lastName(), + command.email(), hashedPassword, + command.phoneNumber(), command.roles() ); + // Create the user through domain service + User user = userDomainService.createUser(userSpecification); + // Save the user User savedUser = userRepository.save(user); @@ -61,7 +65,7 @@ public void updateUser(UpdateUserCommand command) { .orElseThrow(() -> new UserNotFoundException(command.userId())); // Update through domain service - userDomainService.updateUser(user, command.updateSpec()); + userDomainService.updateUserInfo(user, command.userSpec()); // Save changes userRepository.save(user); @@ -96,7 +100,7 @@ public void activateUser(UserId userId) { */ @Transactional public void deactivateUser(UserId userId) { - updateUserStatus(new UpdateUserStatusCommand(userId, UserStatus.INACTIVE)); + updateUserStatus(new UpdateUserStatusCommand(userId, UserStatus.DEACTIVATED)); } /** @@ -155,7 +159,7 @@ public void changePassword(UserId userId, String currentPassword, String newPass // Hash new password and update String hashedPassword = passwordEncoder.encode(newPassword); - userDomainService.updatePassword(user, hashedPassword); + userDomainService.changePassword(user, hashedPassword); userRepository.save(user); eventPublisher.publish(user.getDomainEvents()); @@ -169,7 +173,7 @@ public void resetPassword(UserId userId, String newPassword) { User user = getUser(userId); String hashedPassword = passwordEncoder.encode(newPassword); - userDomainService.updatePassword(user, hashedPassword); + userDomainService.changePassword(user, hashedPassword); userRepository.save(user); eventPublisher.publish(user.getDomainEvents()); diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java index f60488d..03c7018 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java @@ -6,12 +6,15 @@ import com.zenfulcode.commercify.user.domain.valueobject.UserId; import lombok.Getter; +import java.time.Instant; + @Getter public class UserStatusChangedEvent extends DomainEvent { @AggregateId private final UserId userId; private final UserStatus oldStatus; private final UserStatus newStatus; + private final Instant changedAt; public UserStatusChangedEvent( UserId userId, @@ -21,5 +24,18 @@ public UserStatusChangedEvent( this.userId = userId; this.oldStatus = oldStatus; this.newStatus = newStatus; + this.changedAt = Instant.now(); + } + + public boolean isDeactivationTransition() { + return newStatus == UserStatus.DEACTIVATED; + } + + public boolean isSuspensionTransition() { + return newStatus == UserStatus.SUSPENDED; + } + + public boolean isActivationTransition() { + return newStatus == UserStatus.ACTIVE; } -} +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/exception/InvalidUserStateTransitionException.java b/src/main/java/com/zenfulcode/commercify/user/domain/exception/InvalidUserStateTransitionException.java new file mode 100644 index 0000000..5145172 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/exception/InvalidUserStateTransitionException.java @@ -0,0 +1,27 @@ +package com.zenfulcode.commercify.user.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.Getter; + +@Getter +public class InvalidUserStateTransitionException extends DomainException { + private final UserId userId; + private final UserStatus currentStatus; + private final UserStatus targetStatus; + + public InvalidUserStateTransitionException( + UserId userId, + UserStatus currentStatus, + UserStatus targetStatus, + String message + ) { + super(String.format("Invalid user status transition from %s to %s for user %s: %s", + currentStatus, targetStatus, userId, message)); + + this.userId = userId; + this.currentStatus = currentStatus; + this.targetStatus = targetStatus; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserValidationException.java b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserValidationException.java index d2d79c6..452b3a7 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserValidationException.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserValidationException.java @@ -5,7 +5,11 @@ import java.util.List; public class UserValidationException extends DomainValidationException { + public UserValidationException(String message) { + super(message, List.of(message)); + } + public UserValidationException(List violations) { - super("User validation failed", violations); + super("User validation failed: " + String.join(", ", violations), violations); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java index c4610bc..1ab4e01 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -1,19 +1,22 @@ package com.zenfulcode.commercify.user.domain.model; +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; import com.zenfulcode.commercify.user.domain.event.UserCreatedEvent; import com.zenfulcode.commercify.user.domain.event.UserStatusChangedEvent; -import com.zenfulcode.commercify.user.domain.exception.InvalidUserStateException; import com.zenfulcode.commercify.user.domain.valueobject.UserId; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import java.time.Instant; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; @@ -52,6 +55,10 @@ public class User extends AggregateRoot { @Column(name = "role") private Set roles = new HashSet<>(); + @Setter + @OneToMany(mappedBy = "userId", orphanRemoval = true) + private Set orders = new LinkedHashSet<>(); + @CreationTimestamp @Column(nullable = false) private Instant createdAt; @@ -64,11 +71,12 @@ public class User extends AggregateRoot { // Factory method public static User create( - String email, String firstName, String lastName, + String email, String password, - Set roles + Set roles, + String phoneNumber ) { User user = new User(); user.id = UserId.generate(); @@ -78,6 +86,7 @@ public static User create( user.password = Objects.requireNonNull(password, "Password is required"); user.status = UserStatus.PENDING; user.roles = new HashSet<>(roles != null ? roles : Set.of(UserRole.USER)); + user.phoneNumber = phoneNumber; // Register domain event user.registerEvent(new UserCreatedEvent( @@ -90,36 +99,28 @@ public static User create( } // Domain methods - public void updateProfile(String firstName, String lastName, String phoneNumber) { + public void updateProfile(String firstName, String lastName) { if (firstName != null) { this.firstName = firstName; } if (lastName != null) { this.lastName = lastName; } - if (phoneNumber != null) { - this.phoneNumber = phoneNumber; - } } public void updateEmail(String newEmail) { this.email = Objects.requireNonNull(newEmail, "Email is required").toLowerCase(); } + public void updatePhone(String phone) { + this.phoneNumber = phone; + } + public void updatePassword(String newPassword) { this.password = Objects.requireNonNull(newPassword, "Password is required"); } public void updateStatus(UserStatus newStatus) { - if (!canTransitionTo(newStatus)) { - throw new InvalidUserStateException( - id, - status, - newStatus, - "Invalid status transition" - ); - } - UserStatus oldStatus = this.status; this.status = newStatus; @@ -149,13 +150,18 @@ public boolean isActive() { return status == UserStatus.ACTIVE; } - private boolean canTransitionTo(UserStatus newStatus) { - return switch (status) { - case PENDING -> Set.of(UserStatus.ACTIVE, UserStatus.INACTIVE).contains(newStatus); - case ACTIVE -> Set.of(UserStatus.INACTIVE, UserStatus.SUSPENDED).contains(newStatus); - case INACTIVE -> Set.of(UserStatus.ACTIVE).contains(newStatus); - case SUSPENDED -> Set.of(UserStatus.ACTIVE, UserStatus.INACTIVE).contains(newStatus); - }; + public boolean hasOutstandingPayments() { + return false; + } + + public boolean hasActiveOrders() { + return orders.stream() + .anyMatch(order -> { + OrderStatus status = order.getStatus(); + return status == OrderStatus.PENDING || + status == OrderStatus.CONFIRMED || + status == OrderStatus.SHIPPED; + }); } @Override @@ -169,4 +175,5 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(id); } + } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/UserStatus.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/UserStatus.java index db2143f..24151c1 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/UserStatus.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/UserStatus.java @@ -1,8 +1,8 @@ package com.zenfulcode.commercify.user.domain.model; public enum UserStatus { - PENDING, // Initial status after registration - ACTIVE, // Normal active user - INACTIVE, // User deactivated (by self or admin) - SUSPENDED // User suspended (by admin) + PENDING, // Initial state after registration + ACTIVE, // User is active and can use the system + SUSPENDED, // User is temporarily suspended + DEACTIVATED // User account is deactivated (terminal state) } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/DefaultUserValidationPolicy.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/DefaultUserValidationPolicy.java deleted file mode 100644 index 6df6cac..0000000 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/DefaultUserValidationPolicy.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.zenfulcode.commercify.user.domain.service; - -import com.zenfulcode.commercify.order.domain.repository.OrderRepository; -import com.zenfulcode.commercify.user.domain.model.User; -import com.zenfulcode.commercify.user.domain.model.UserStatus; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class DefaultUserValidationPolicy implements UserValidationPolicy { - private final OrderRepository orderRepository; - - @Override - public boolean canTransitionToStatus(User user, UserStatus newStatus) { - if (newStatus == UserStatus.INACTIVE || newStatus == UserStatus.SUSPENDED) { - // Check for active orders before allowing deactivation - // Only allow transition if user has no active orders - boolean hasActiveOrders = orderRepository.existsByUserId(user.getId()); - return !hasActiveOrders; - } - // Allow all other transitions - return true; - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java index 48794bb..a0bf78f 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java @@ -1,204 +1,146 @@ package com.zenfulcode.commercify.user.domain.service; -import com.zenfulcode.commercify.user.application.dto.UserUpdateSpec; -import com.zenfulcode.commercify.user.domain.exception.InvalidUserStateException; -import com.zenfulcode.commercify.user.domain.exception.UserValidationException; +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.user.domain.event.UserCreatedEvent; +import com.zenfulcode.commercify.user.domain.event.UserStatusChangedEvent; import com.zenfulcode.commercify.user.domain.model.User; -import com.zenfulcode.commercify.user.domain.model.UserRole; import com.zenfulcode.commercify.user.domain.model.UserStatus; -import com.zenfulcode.commercify.user.domain.repository.UserRepository; +import com.zenfulcode.commercify.user.domain.valueobject.UserDeletionValidation; +import com.zenfulcode.commercify.user.domain.valueobject.UserSpecification; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; @Service @RequiredArgsConstructor public class UserDomainService { - private static final Pattern EMAIL_PATTERN = Pattern.compile( - "^[A-Za-z0-9+_.-]+@(.+)$" - ); - private static final Pattern PHONE_PATTERN = Pattern.compile( - "^\\+?[1-9]\\d{1,14}$" - ); - - private final UserRepository userRepository; - private final UserValidationPolicy validationPolicy; + private final UserStateFlow userStateFlow; + private final UserValidationService validationService; + private final DomainEventPublisher eventPublisher; + private final PasswordEncoder passwordEncoder; /** - * Creates a new user with validation + * Creates a new user with validation and enrichment */ - public User createUser( - String email, - String firstName, - String lastName, - String hashedPassword, - Set roles - ) { - validateNewUser(email, firstName, lastName, hashedPassword); - validateRoles(roles); - - return User.create( - email, - firstName, - lastName, - hashedPassword, - roles + public User createUser(UserSpecification spec) { + // Create user with encrypted password + User user = User.create( + spec.firstName(), + spec.lastName(), + spec.email(), + passwordEncoder.encode(spec.password()), + spec.roles(), + spec.phone() ); + + // Validate user + validationService.validateCreateUser(user); + + // Register creation event + user.registerEvent(new UserCreatedEvent( + user.getId(), + user.getEmail(), + user.getStatus() + )); + + return user; } /** - * Updates user information + * Updates user status with validation */ - public void updateUser(User user, UserUpdateSpec updateSpec) { - List violations = new ArrayList<>(); - - // Validate and apply name updates - if (updateSpec.hasNameUpdate()) { - if (updateSpec.firstName() != null) { - validateFirstName(updateSpec.firstName(), violations); - } - if (updateSpec.lastName() != null) { - validateLastName(updateSpec.lastName(), violations); - } - } + public void updateUserStatus(User user, UserStatus newStatus) { + // Validate status transition + validationService.validateStatusTransition(user, newStatus); - // Validate and apply email update - if (updateSpec.hasEmailUpdate()) { - validateEmail(updateSpec.email(), violations); - validateEmailUniqueness(updateSpec.email(), user); + // If transitioning to deactivated state, validate deactivation + if (newStatus == UserStatus.DEACTIVATED) { + validationService.validateDeactivation(user); } - // Validate and apply phone update - if (updateSpec.hasPhoneUpdate() && updateSpec.phoneNumber() != null) { - validatePhoneNumber(updateSpec.phoneNumber(), violations); - } + UserStatus oldStatus = user.getStatus(); + user.updateStatus(newStatus); - // Validate and apply role updates - if (updateSpec.hasRolesUpdate()) { - validateRoles(updateSpec.roles()); - } + // Register status change event + user.registerEvent(new UserStatusChangedEvent( + user.getId(), + oldStatus, + newStatus + )); + } - if (!violations.isEmpty()) { - throw new UserValidationException(violations); + /** + * Updates user information with validation + */ + public void updateUserInfo(User user, UserSpecification updateSpec) { + // Update basic information if provided + if (updateSpec.hasBasicInfoUpdate()) { + user.updateProfile(updateSpec.firstName(), updateSpec.lastName()); } - // Apply updates - user.updateProfile( - updateSpec.firstName(), - updateSpec.lastName(), - updateSpec.phoneNumber() - ); - - if (updateSpec.hasEmailUpdate()) { + if (updateSpec.hasContactInfoUpdate()) { user.updateEmail(updateSpec.email()); } - if (updateSpec.hasRolesUpdate()) { - user.updateRoles(updateSpec.roles()); + if (updateSpec.phone() != null) { + user.updatePhone(updateSpec.phone()); } - } - /** - * Updates user's password - */ - public void updatePassword(User user, String hashedPassword) { - validatePassword(hashedPassword); - user.updatePassword(hashedPassword); + // Validate updated user + validationService.validateUpdateUser(user); } /** - * Updates user's status with validation + * Changes user password with validation */ - public void updateUserStatus(User user, UserStatus newStatus) { - validateStatusTransition(user, newStatus); - user.updateStatus(newStatus); - } - - private void validateNewUser(String email, String firstName, String lastName, String password) { - List violations = new ArrayList<>(); - - validateEmail(email, violations); - validateFirstName(firstName, violations); - validateLastName(lastName, violations); - validatePassword(password); - validateEmailUniqueness(email, null); - - if (!violations.isEmpty()) { - throw new UserValidationException(violations); - } - } - - private void validateEmail(String email, List violations) { - if (email == null || email.isBlank()) { - violations.add("Email is required"); - return; - } + public void changePassword(User user, String newPassword) { + // Validate new password + validationService.validatePasswordChange(newPassword); - if (!EMAIL_PATTERN.matcher(email).matches()) { - violations.add("Invalid email format"); - } + // Update password + user.updatePassword(passwordEncoder.encode(newPassword)); } - private void validateEmailUniqueness(String email, User existingUser) { - if (userRepository.findByEmail(email) - .map(user -> !user.equals(existingUser)) - .orElse(false)) { - throw new UserValidationException(List.of("Email already exists")); - } - } + /** + * Validates if a user can be deleted + */ + public UserDeletionValidation validateUserDeletion(User user) { + List issues = new ArrayList<>(); - private void validateFirstName(String firstName, List violations) { - if (firstName == null || firstName.isBlank()) { - violations.add("First name is required"); - } else if (firstName.length() < 2 || firstName.length() > 50) { - violations.add("First name must be between 2 and 50 characters"); + try { + validationService.validateAccountDeletion(user); + } catch (Exception e) { + issues.add(e.getMessage()); } - } - private void validateLastName(String lastName, List violations) { - if (lastName == null || lastName.isBlank()) { - violations.add("Last name is required"); - } else if (lastName.length() < 2 || lastName.length() > 50) { - violations.add("Last name must be between 2 and 50 characters"); - } + return new UserDeletionValidation(issues.isEmpty(), issues); } - private void validatePhoneNumber(String phoneNumber, List violations) { - if (phoneNumber != null && !phoneNumber.isBlank() && - !PHONE_PATTERN.matcher(phoneNumber).matches()) { - violations.add("Invalid phone number format"); - } - } - - private void validatePassword(String hashedPassword) { - if (hashedPassword == null || hashedPassword.isBlank()) { - throw new UserValidationException(List.of("Password is required")); - } + /** + * Deactivates a user account with validation + */ + public void deactivateUser(User user) { + validationService.validateDeactivation(user); + updateUserStatus(user, UserStatus.DEACTIVATED); } - private void validateRoles(Set roles) { - if (roles == null || roles.isEmpty()) { - throw new UserValidationException(List.of("User must have at least one role")); - } - - // Additional role validation logic can be added here - // For example, ensuring admin role assignments follow specific rules + /** + * Suspends a user account with validation + */ + public void suspendUser(User user) { + updateUserStatus(user, UserStatus.SUSPENDED); } - private void validateStatusTransition(User user, UserStatus newStatus) { - // Additional status transition validation logic can be added here - // For example, checking if user has pending orders before deactivation - if (validationPolicy.canTransitionToStatus(user, newStatus)) { - throw new InvalidUserStateException( - user.getId(), - user.getStatus(), - newStatus, - "Status transition not allowed by policy" - ); + /** + * Reactivates a suspended user account with validation + */ + public void reactivateUser(User user) { + if (user.getStatus() != UserStatus.SUSPENDED) { + throw new IllegalStateException("Can only reactivate suspended users"); } + updateUserStatus(user, UserStatus.ACTIVE); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserStateFlow.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserStateFlow.java new file mode 100644 index 0000000..fd3b7e1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserStateFlow.java @@ -0,0 +1,49 @@ +package com.zenfulcode.commercify.user.domain.service; + +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.Set; + +@Component +public class UserStateFlow { + private final EnumMap> validTransitions; + + public UserStateFlow() { + validTransitions = new EnumMap<>(UserStatus.class); + + // Initial state -> Active or Suspended + validTransitions.put(UserStatus.PENDING, Set.of( + UserStatus.ACTIVE, + UserStatus.SUSPENDED + )); + + // Active -> Suspended or Deactivated + validTransitions.put(UserStatus.ACTIVE, Set.of( + UserStatus.SUSPENDED, + UserStatus.DEACTIVATED + )); + + // Suspended -> Active or Deactivated + validTransitions.put(UserStatus.SUSPENDED, Set.of( + UserStatus.ACTIVE, + UserStatus.DEACTIVATED + )); + + // Terminal state + validTransitions.put(UserStatus.DEACTIVATED, Set.of()); + } + + public boolean canTransition(UserStatus currentState, UserStatus newState) { + return validTransitions.get(currentState).contains(newState); + } + + public Set getValidTransitions(UserStatus currentState) { + return validTransitions.get(currentState); + } + + public boolean isTerminalState(UserStatus state) { + return validTransitions.get(state).isEmpty(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationPolicy.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationPolicy.java deleted file mode 100644 index e24b778..0000000 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationPolicy.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.zenfulcode.commercify.user.domain.service; - -import com.zenfulcode.commercify.user.domain.model.User; -import com.zenfulcode.commercify.user.domain.model.UserStatus; - -public interface UserValidationPolicy { - boolean canTransitionToStatus(User user, UserStatus newStatus); -} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationService.java new file mode 100644 index 0000000..6925cd2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserValidationService.java @@ -0,0 +1,120 @@ +package com.zenfulcode.commercify.user.domain.service; + +import com.zenfulcode.commercify.user.domain.exception.InvalidUserStateTransitionException; +import com.zenfulcode.commercify.user.domain.exception.UserValidationException; +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +@Service +@RequiredArgsConstructor +public class UserValidationService { + private final UserStateFlow userStateFlow; + private static final Pattern EMAIL_PATTERN = Pattern.compile( + "^[A-Za-z0-9+_.-]+@(.+)$" + ); + private static final Pattern PASSWORD_PATTERN = Pattern.compile( + "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$" + ); + private static final Pattern PHONE_PATTERN = Pattern.compile( + "^\\+?[1-9]\\d{1,14}$" + ); + + public void validateCreateUser(User user) { + List violations = new ArrayList<>(); + + validateBasicUserInfo(user, violations); + validateContactInfo(user, violations); + validatePassword(user.getPassword(), violations); + + if (!violations.isEmpty()) { + throw new UserValidationException(violations); + } + } + + public void validateUpdateUser(User user) { + List violations = new ArrayList<>(); + + validateBasicUserInfo(user, violations); + validateContactInfo(user, violations); + + if (!violations.isEmpty()) { + throw new UserValidationException(violations); + } + } + + public void validateStatusTransition(User user, UserStatus newStatus) { + if (!userStateFlow.canTransition(user.getStatus(), newStatus)) { + throw new InvalidUserStateTransitionException( + user.getId(), + user.getStatus(), + newStatus, + "Invalid status transition" + ); + } + } + + public void validatePasswordChange(String newPassword) { + List violations = new ArrayList<>(); + validatePassword(newPassword, violations); + + if (!violations.isEmpty()) { + throw new UserValidationException(violations); + } + } + + private void validateBasicUserInfo(User user, List violations) { + if (user.getFirstName() == null || user.getFirstName().isBlank()) { + violations.add("First name is required"); + } + if (user.getLastName() == null || user.getLastName().isBlank()) { + violations.add("Last name is required"); + } + } + + private void validateContactInfo(User user, List violations) { + if (user.getEmail() == null || !EMAIL_PATTERN.matcher(user.getEmail()).matches()) { + violations.add("Valid email address is required"); + } + if (user.getPhoneNumber() != null && !PHONE_PATTERN.matcher(user.getPhoneNumber()).matches()) { + violations.add("Invalid phone number format"); + } + } + + private void validatePassword(String password, List violations) { + if (password == null || !PASSWORD_PATTERN.matcher(password).matches()) { + violations.add("Password must be at least 8 characters long and contain at least one " + + "uppercase letter, one lowercase letter, one number, and one special character"); + } + } + + public void validateDeactivation(User user) { + if (user.getStatus() == UserStatus.DEACTIVATED) { + throw new UserValidationException("User is already deactivated"); + } + // Add additional deactivation validation rules here + } + + public void validateAccountDeletion(User user) { + List violations = new ArrayList<>(); + + // Check for active orders + if (user.hasActiveOrders()) { + violations.add("Cannot delete user with active orders"); + } + + // Check for outstanding payments + if (user.hasOutstandingPayments()) { + violations.add("Cannot delete user with outstanding payments"); + } + + if (!violations.isEmpty()) { + throw new UserValidationException(violations); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserDeletionValidation.java b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserDeletionValidation.java new file mode 100644 index 0000000..08fa5e2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserDeletionValidation.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.user.domain.valueobject; + +import java.util.List; + +public record UserDeletionValidation( + boolean canDelete, + List issues +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserSpecification.java b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserSpecification.java new file mode 100644 index 0000000..d63a75c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserSpecification.java @@ -0,0 +1,30 @@ +package com.zenfulcode.commercify.user.domain.valueobject; + +import com.zenfulcode.commercify.user.domain.model.UserRole; + +import java.util.Set; + +public record UserSpecification( + String firstName, + String lastName, + String email, + String password, + String phone, + Set roles +) { + public boolean hasBasicInfoUpdate() { + return firstName != null || lastName != null; + } + + public boolean hasContactInfoUpdate() { + return email != null || phone != null; + } + + public boolean hasCredentialsUpdate() { + return password != null; + } + + public boolean hasRoleUpdate() { + return roles != null; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/application/dto/UserUpdateSpec.java b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserUpdateSpec.java similarity index 90% rename from src/main/java/com/zenfulcode/commercify/user/application/dto/UserUpdateSpec.java rename to src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserUpdateSpec.java index ec89a66..a586207 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/dto/UserUpdateSpec.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserUpdateSpec.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.user.application.dto; +package com.zenfulcode.commercify.user.domain.valueobject; import com.zenfulcode.commercify.user.domain.model.UserRole; From 6bb5b14653ed42ddc2c6407c1b273a5135992cad Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 4 Jan 2025 14:05:57 +0100 Subject: [PATCH 11/57] Adding database migration for user tables adding OneToMany relation between user and order --- .../commercify/order/domain/model/Order.java | 7 ++++ .../commercify/user/domain/model/User.java | 2 +- .../db/changelog/db.changelog-master.xml | 1 + .../migrations/250103220503-changelog.sql | 8 ++-- .../migrations/250104134452-changelog.sql | 37 +++++++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 src/main/resources/db/changelog/migrations/250104134452-changelog.sql diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java index 9238d68..254cb6a 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -7,11 +7,13 @@ import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.valueobject.UserId; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -75,6 +77,11 @@ public class Order extends AggregateRoot { }) private Money totalAmount; + @Setter + @ManyToOne(optional = false) + @JoinColumn(name = "user_id", insertable = false, updatable = false) + private User user; + @CreationTimestamp @Column(name = "created_at", nullable = false) private Instant createdAt; diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java index 1ab4e01..6b9f33c 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -56,7 +56,7 @@ public class User extends AggregateRoot { private Set roles = new HashSet<>(); @Setter - @OneToMany(mappedBy = "userId", orphanRemoval = true) + @OneToMany(mappedBy = "user", orphanRemoval = true) private Set orders = new LinkedHashSet<>(); @CreationTimestamp diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index 27fbfa8..99c8378 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -4,4 +4,5 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/250103220503-changelog.sql b/src/main/resources/db/changelog/migrations/250103220503-changelog.sql index 4bc57ae..e739c94 100644 --- a/src/main/resources/db/changelog/migrations/250103220503-changelog.sql +++ b/src/main/resources/db/changelog/migrations/250103220503-changelog.sql @@ -51,14 +51,14 @@ CREATE TABLE products -- changeset gkhaavik:1735938302939-4 CREATE TABLE product_variants ( - id VARCHAR(255) NOT NULL, + variant_id VARCHAR(255) NOT NULL, product_id VARCHAR(255) NOT NULL, sku VARCHAR(255) NOT NULL, stock INT NULL, image_url VARCHAR(255) NULL, unit_price DECIMAL(19, 2) NULL, currency VARCHAR(255) NULL, - CONSTRAINT pk_product_variants PRIMARY KEY (id), + CONSTRAINT pk_product_variants PRIMARY KEY (variant_id), CONSTRAINT uc_product_variants_sku UNIQUE (sku) ); @@ -112,7 +112,7 @@ ALTER TABLE product_variants ALTER TABLE variant_options ADD CONSTRAINT FK_VARIANT_OPTIONS_ON_VARIANT FOREIGN KEY (product_variant_id) - REFERENCES product_variants (id); + REFERENCES product_variants (variant_id); -- changeset gkhaavik:1735938302939-10 ALTER TABLE orders @@ -130,7 +130,7 @@ ALTER TABLE order_lines ALTER TABLE order_lines ADD CONSTRAINT FK_ORDER_LINES_ON_VARIANT FOREIGN KEY (product_variant_id) - REFERENCES product_variants (id); + REFERENCES product_variants (variant_id); -- changeset gkhaavik:1735938302939-13 ALTER TABLE order_lines diff --git a/src/main/resources/db/changelog/migrations/250104134452-changelog.sql b/src/main/resources/db/changelog/migrations/250104134452-changelog.sql new file mode 100644 index 0000000..4d5d29e --- /dev/null +++ b/src/main/resources/db/changelog/migrations/250104134452-changelog.sql @@ -0,0 +1,37 @@ +-- liquibase formatted sql + +-- changeset gkhaavik:1735994692204-5 +CREATE TABLE user_roles +( + user_id VARCHAR(255) NOT NULL, + `role` VARCHAR(255) NULL +); + +-- changeset gkhaavik:1735994692204-6 +CREATE TABLE users +( + email VARCHAR(255) NOT NULL, + firstName VARCHAR(255) NOT NULL, + lastName VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + phoneNumber VARCHAR(255) NULL, + status VARCHAR(255) NOT NULL, + createdAt datetime NOT NULL, + updatedAt datetime NOT NULL, + lastLoginAt datetime NULL, + user_id VARCHAR(255) NOT NULL, + CONSTRAINT pk_users PRIMARY KEY (user_id) +); + +-- changeset gkhaavik:1735994692204-7 +ALTER TABLE users + ADD CONSTRAINT uc_users_email UNIQUE (email); + +-- changeset gkhaavik:1735994692204-9 +ALTER TABLE orders + ADD CONSTRAINT FK_ORDERS_ON_USER FOREIGN KEY (user_id) REFERENCES users (user_id); + +-- changeset gkhaavik:1735994692204-10 +ALTER TABLE user_roles + ADD CONSTRAINT fk_user_roles_on_user FOREIGN KEY (user_id) REFERENCES users (user_id); + From 3baa76d544d6a48044055f3507849c0d0ad9e70c Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 4 Jan 2025 16:27:24 +0100 Subject: [PATCH 12/57] adding user authentication --- .../commercify/api/auth/AuthController.java | 72 ++++++++++++ .../api/auth/dto/request/LoginRequest.java | 7 ++ .../auth/dto/request/RefreshTokenRequest.java | 6 + .../api/auth/dto/request/RegisterRequest.java | 10 ++ .../api/auth/dto/response/AuthResponse.java | 33 ++++++ .../AuthenticationApplicationService.java | 80 +++++++++++++ .../service/AuthenticationResult.java | 10 ++ .../domain/event/UserAuthenticatedEvent.java | 18 +++ .../InvalidCredentialsException.java | 9 ++ .../auth/domain/model/AuthenticatedUser.java | 109 ++++++++++++++++++ .../auth/domain/model/UserRole.java | 8 ++ .../service/AuthenticationDomainService.java | 11 ++ .../infrastructure/config/SecurityConfig.java | 67 +++++++++++ .../security/JwtAuthenticationFilter.java | 55 +++++++++ .../infrastructure/security/TokenService.java | 63 ++++++++++ .../DefaultAuthenticationDomainService.java | 43 +++++++ .../commercify/config/SecurityConfig.java | 14 --- .../exception/UserAccountLockedException.java | 7 ++ .../service/UserApplicationService.java | 33 ++++++ .../exception/UserAlreadyExistsException.java | 9 ++ .../commercify/user/domain/model/User.java | 7 ++ .../domain/service/UserDomainService.java | 8 ++ .../domain/valueobject/UserSpecification.java | 4 + .../resources/application-docker.properties | 6 +- src/main/resources/application.properties | 8 +- 25 files changed, 679 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/auth/dto/request/RefreshTokenRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/auth/dto/request/RegisterRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/auth/dto/response/AuthResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationResult.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/domain/exception/InvalidCredentialsException.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/domain/model/UserRole.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/domain/service/AuthenticationDomainService.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/TokenService.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/infrastructure/service/DefaultAuthenticationDomainService.java delete mode 100644 src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/UserAccountLockedException.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/domain/exception/UserAlreadyExistsException.java diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java new file mode 100644 index 0000000..fa809f4 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java @@ -0,0 +1,72 @@ +package com.zenfulcode.commercify.api.auth; + +import com.zenfulcode.commercify.api.auth.dto.request.LoginRequest; +import com.zenfulcode.commercify.api.auth.dto.request.RefreshTokenRequest; +import com.zenfulcode.commercify.api.auth.dto.request.RegisterRequest; +import com.zenfulcode.commercify.api.auth.dto.response.AuthResponse; +import com.zenfulcode.commercify.auth.application.service.AuthenticationApplicationService; +import com.zenfulcode.commercify.auth.application.service.AuthenticationResult; +import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import com.zenfulcode.commercify.user.application.command.CreateUserCommand; +import com.zenfulcode.commercify.user.application.service.UserApplicationService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Set; + +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthenticationApplicationService authService; + private final UserApplicationService userService; + + @PostMapping("/login") + public ResponseEntity> login( + @RequestBody LoginRequest request) { + + AuthenticationResult result = authService.authenticate( + request.username(), + request.password() + ); + + AuthResponse response = AuthResponse.from(result); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PostMapping("/register") + public ResponseEntity> register( + @RequestBody RegisterRequest request) { + + userService.registerUser( + request.firstName(), + request.lastName(), + request.email(), + request.password(), + request.phone() + ); + + // Authenticate the newly registered user + AuthenticationResult result = authService.authenticate( + request.email(), + request.password() + ); + + AuthResponse response = AuthResponse.from(result); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PostMapping("/refresh") + public ResponseEntity> refreshToken( + @RequestBody RefreshTokenRequest request) { + + AuthenticationResult result = authService.refreshToken(request.refreshToken()); + AuthResponse response = AuthResponse.from(result); + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java new file mode 100644 index 0000000..b8993ef --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.api.auth.dto.request; + +public record LoginRequest( + String username, + String password +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/RefreshTokenRequest.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/RefreshTokenRequest.java new file mode 100644 index 0000000..419f2cf --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/RefreshTokenRequest.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.api.auth.dto.request; + +public record RefreshTokenRequest( + String refreshToken +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/RegisterRequest.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/RegisterRequest.java new file mode 100644 index 0000000..e839a74 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/RegisterRequest.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.api.auth.dto.request; + +public record RegisterRequest( + String firstName, + String lastName, + String email, + String password, + String phone +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/AuthResponse.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/AuthResponse.java new file mode 100644 index 0000000..2bd36d5 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/AuthResponse.java @@ -0,0 +1,33 @@ +package com.zenfulcode.commercify.api.auth.dto.response; + +import com.zenfulcode.commercify.auth.application.service.AuthenticationResult; +import com.zenfulcode.commercify.auth.domain.model.UserRole; + +import java.util.Set; +import java.util.stream.Collectors; + +public record AuthResponse( + String accessToken, + String refreshToken, + String tokenType, + String userId, + String username, + String email, + Set roles +) { + public static AuthResponse from(AuthenticationResult result) { + Set roles = result.user().getRoles().stream() + .map(UserRole::name) + .collect(Collectors.toSet()); + + return new AuthResponse( + result.accessToken(), + result.refreshToken(), + "Bearer", + result.user().getUserId(), + result.user().getUsername(), + result.user().getEmail(), + roles + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java new file mode 100644 index 0000000..b2bf37f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java @@ -0,0 +1,80 @@ +package com.zenfulcode.commercify.auth.application.service; + +import com.zenfulcode.commercify.auth.domain.event.UserAuthenticatedEvent; +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; +import com.zenfulcode.commercify.auth.domain.model.UserRole; +import com.zenfulcode.commercify.auth.domain.service.AuthenticationDomainService; +import com.zenfulcode.commercify.auth.infrastructure.security.TokenService; +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.repository.UserRepository; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class AuthenticationApplicationService { + private final AuthenticationManager authenticationManager; + private final AuthenticationDomainService authenticationDomainService; + private final UserRepository userRepository; + private final TokenService tokenService; + private final DomainEventPublisher eventPublisher; + + @Transactional + public AuthenticationResult authenticate(String email, String password) { + // Authenticate using Spring Security + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(email, password) + ); + + AuthenticatedUser authenticatedUser = (AuthenticatedUser) authentication.getPrincipal(); + + // Generate tokens + String accessToken = tokenService.generateAccessToken(authenticatedUser); + String refreshToken = tokenService.generateRefreshToken(authenticatedUser); + + // Publish domain event + User user = userRepository.findByEmail(email) + .orElseThrow(); // User must exist at this point + eventPublisher.publish(new UserAuthenticatedEvent(user.getId(), email)); + + return new AuthenticationResult(accessToken, refreshToken, authenticatedUser); + } + + @Transactional(readOnly = true) + public AuthenticatedUser validateAccessToken(String token) { + String userId = tokenService.validateTokenAndGetUserId(token); + User user = userRepository.findById(UserId.of(userId)) + .orElseThrow(); + return authenticationDomainService.createAuthenticatedUser(user); + } + + @Transactional + public AuthenticationResult refreshToken(String refreshToken) { + String userId = tokenService.validateTokenAndGetUserId(refreshToken); + User user = userRepository.findById(UserId.of(userId)) + .orElseThrow(); + + AuthenticatedUser authenticatedUser = authenticationDomainService.createAuthenticatedUser(user); + + String newAccessToken = tokenService.generateAccessToken(authenticatedUser); + String newRefreshToken = tokenService.generateRefreshToken(authenticatedUser); + + return new AuthenticationResult(newAccessToken, newRefreshToken, authenticatedUser); + } + + public Set mapRoles(Collection roles) { + return roles.stream() + .map(role -> UserRole.valueOf("ROLE_" + role.name())) + .collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationResult.java b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationResult.java new file mode 100644 index 0000000..d7dc84f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationResult.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.auth.application.service; + +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; + +public record AuthenticationResult( + String accessToken, + String refreshToken, + AuthenticatedUser user +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java b/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java new file mode 100644 index 0000000..3d8ffff --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.auth.domain.event; + +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; +import lombok.Getter; + +@Getter +public class UserAuthenticatedEvent extends DomainEvent { + @AggregateId + private final UserId userId; + private final String username; + + public UserAuthenticatedEvent(UserId userId, String username) { + this.userId = userId; + this.username = username; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/exception/InvalidCredentialsException.java b/src/main/java/com/zenfulcode/commercify/auth/domain/exception/InvalidCredentialsException.java new file mode 100644 index 0000000..d8b6cf7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/exception/InvalidCredentialsException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.auth.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; + +public class InvalidCredentialsException extends DomainException { + public InvalidCredentialsException() { + super("Invalid username or password"); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java new file mode 100644 index 0000000..b0f4c08 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java @@ -0,0 +1,109 @@ +package com.zenfulcode.commercify.auth.domain.model; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AuthenticatedUser implements UserDetails { + private String userId; + private String username; + private String email; + private String password; + private Set roles; + private boolean enabled; + private boolean accountNonExpired; + private boolean accountNonLocked; + private boolean credentialsNonExpired; + + private AuthenticatedUser( + String userId, + String username, + String email, + String password, + Set roles, + boolean enabled, + boolean accountNonExpired, + boolean accountNonLocked, + boolean credentialsNonExpired + ) { + this.userId = userId; + this.username = username; + this.email = email; + this.password = password; + this.roles = roles; + this.enabled = enabled; + this.accountNonExpired = accountNonExpired; + this.accountNonLocked = accountNonLocked; + this.credentialsNonExpired = credentialsNonExpired; + } + + public static AuthenticatedUser create( + String userId, + String username, + String email, + String password, + Set roles, + boolean enabled, + boolean accountNonExpired, + boolean accountNonLocked, + boolean credentialsNonExpired + ) { + return new AuthenticatedUser( + userId, + username, + email, + password, + roles, + enabled, + accountNonExpired, + accountNonLocked, + credentialsNonExpired + ); + } + + @Override + public Collection getAuthorities() { + return roles.stream() + .map(role -> new SimpleGrantedAuthority(role.name())) + .collect(Collectors.toList()); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return accountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return accountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return credentialsNonExpired; + } + + @Override + public boolean isEnabled() { + return enabled; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/model/UserRole.java b/src/main/java/com/zenfulcode/commercify/auth/domain/model/UserRole.java new file mode 100644 index 0000000..b4ec167 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/model/UserRole.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.auth.domain.model; + +public enum UserRole { + ROLE_GUEST, + ROLE_USER, + ROLE_ADMIN, + ROLE_MANAGER +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/service/AuthenticationDomainService.java b/src/main/java/com/zenfulcode/commercify/auth/domain/service/AuthenticationDomainService.java new file mode 100644 index 0000000..cd8baa7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/service/AuthenticationDomainService.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.auth.domain.service; + +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; + +public interface AuthenticationDomainService { + AuthenticatedUser createAuthenticatedUser(User user); + + void validateUserStatus(UserStatus status); +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java new file mode 100644 index 0000000..3b6b0de --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -0,0 +1,67 @@ +package com.zenfulcode.commercify.auth.infrastructure.config; + +import com.zenfulcode.commercify.auth.infrastructure.security.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthFilter; + private final UserDetailsService userDetailsService; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers("/api/v1/products").permitAll() + .requestMatchers("/api/v1/products/{id}").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) + throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..c7246b4 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java @@ -0,0 +1,55 @@ +package com.zenfulcode.commercify.auth.infrastructure.security; + +import com.zenfulcode.commercify.auth.application.service.AuthenticationApplicationService; +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final AuthenticationApplicationService authService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + try { + String token = extractJwtToken(request); + + if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) { + AuthenticatedUser user = authService.validateAccessToken(token); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Could not set user authentication in security context", e); + } + + filterChain.doFilter(request, response); + } + + private String extractJwtToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/TokenService.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/TokenService.java new file mode 100644 index 0000000..ecb3fa1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/TokenService.java @@ -0,0 +1,63 @@ +package com.zenfulcode.commercify.auth.infrastructure.security; + +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Service +public class TokenService { + + @Value("${security.jwt.secret}") + private String jwtSecret; + + @Value("${security.jwt.access-token-expiration}") + private long accessTokenExpiration; + + @Value("${security.jwt.refresh-token-expiration}") + private long refreshTokenExpiration; + + public String generateAccessToken(AuthenticatedUser user) { + return buildToken(user, accessTokenExpiration); + } + + public String generateRefreshToken(AuthenticatedUser user) { + return buildToken(user, refreshTokenExpiration); + } + + public String validateTokenAndGetUserId(String token) { + Claims claims = Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + + return claims.getSubject(); + } + + private String buildToken(AuthenticatedUser user, long expiration) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + + return Jwts.builder() + .subject(user.getUserId()) + .claim("username", user.getUsername()) + .claim("email", user.getEmail()) + .claim("roles", user.getRoles()) + .issuedAt(now) + .expiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + private SecretKey getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/service/DefaultAuthenticationDomainService.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/service/DefaultAuthenticationDomainService.java new file mode 100644 index 0000000..908b8e7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/service/DefaultAuthenticationDomainService.java @@ -0,0 +1,43 @@ +package com.zenfulcode.commercify.auth.infrastructure.service; + +import com.zenfulcode.commercify.auth.application.service.AuthenticationApplicationService; +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; +import com.zenfulcode.commercify.auth.domain.service.AuthenticationDomainService; +import com.zenfulcode.commercify.shared.domain.exception.UserAccountLockedException; +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DefaultAuthenticationDomainService implements AuthenticationDomainService { + private final AuthenticationApplicationService authenticationApplicationService; + + @Override + public AuthenticatedUser createAuthenticatedUser(User user) { + validateUserStatus(user.getStatus()); + + return AuthenticatedUser.create( + user.getId().toString(), + user.getFullName(), + user.getEmail(), + user.getPassword(), + authenticationApplicationService.mapRoles(user.getRoles()), + user.getStatus() == UserStatus.ACTIVE, + user.getStatus() != UserStatus.DEACTIVATED, + user.getStatus() != UserStatus.SUSPENDED, + true + ); + } + + @Override + public void validateUserStatus(UserStatus status) { + if (status == UserStatus.SUSPENDED) { + throw new UserAccountLockedException("Account is suspended"); + } + if (status == UserStatus.DEACTIVATED) { + throw new UserAccountLockedException("Account is deactivated"); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java deleted file mode 100644 index 4c5960f..0000000 --- a/src/main/java/com/zenfulcode/commercify/config/SecurityConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.zenfulcode.commercify.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -@Configuration -public class SecurityConfig { - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } -} diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/UserAccountLockedException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/UserAccountLockedException.java new file mode 100644 index 0000000..e9e69bf --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/UserAccountLockedException.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +public class UserAccountLockedException extends DomainException { + public UserAccountLockedException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java index d36ae67..f04f839 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java @@ -6,6 +6,7 @@ import com.zenfulcode.commercify.user.application.command.UpdateUserStatusCommand; import com.zenfulcode.commercify.user.domain.exception.UserNotFoundException; import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserRole; import com.zenfulcode.commercify.user.domain.model.UserStatus; import com.zenfulcode.commercify.user.domain.repository.UserRepository; import com.zenfulcode.commercify.user.domain.service.UserDomainService; @@ -18,6 +19,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Set; + @Service @RequiredArgsConstructor public class UserApplicationService { @@ -40,6 +43,7 @@ public UserId createUser(CreateUserCommand command) { command.email(), hashedPassword, command.phoneNumber(), + UserStatus.PENDING, command.roles() ); @@ -55,6 +59,35 @@ public UserId createUser(CreateUserCommand command) { return savedUser.getId(); } + @Transactional + public void registerUser( + String firstName, + String lastName, + String email, + String password, + String phone + ) { + // Create user specification + UserSpecification spec = UserSpecification.builder() + .firstName(firstName) + .lastName(lastName) + .email(email) + .password(passwordEncoder.encode(password)) + .phone(phone) + .status(UserStatus.ACTIVE) + .roles(Set.of(UserRole.USER)) + .build(); + + // Create user through domain service + User user = userDomainService.createUser(spec); + + // Save user + userRepository.save(user); + + // Publish domain event + eventPublisher.publish(user.getDomainEvents()); + } + /** * Updates an existing user */ diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserAlreadyExistsException.java b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserAlreadyExistsException.java new file mode 100644 index 0000000..23d8c7b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserAlreadyExistsException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.user.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; + +public class UserAlreadyExistsException extends DomainException { + public UserAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java index 6b9f33c..3a08eee 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -164,6 +164,10 @@ public boolean hasActiveOrders() { }); } + public String getUsername() { + return firstName + lastName; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -176,4 +180,7 @@ public int hashCode() { return Objects.hash(id); } + public String getFullName() { + return firstName + lastName; + } } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java index a0bf78f..dbf80b9 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java @@ -3,8 +3,10 @@ import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import com.zenfulcode.commercify.user.domain.event.UserCreatedEvent; import com.zenfulcode.commercify.user.domain.event.UserStatusChangedEvent; +import com.zenfulcode.commercify.user.domain.exception.UserAlreadyExistsException; import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.repository.UserRepository; import com.zenfulcode.commercify.user.domain.valueobject.UserDeletionValidation; import com.zenfulcode.commercify.user.domain.valueobject.UserSpecification; import lombok.RequiredArgsConstructor; @@ -21,11 +23,17 @@ public class UserDomainService { private final UserValidationService validationService; private final DomainEventPublisher eventPublisher; private final PasswordEncoder passwordEncoder; + private final UserRepository userRepository; /** * Creates a new user with validation and enrichment */ public User createUser(UserSpecification spec) { + // Check if username or email already exists + if (userRepository.existsByEmail(spec.email())) { + throw new UserAlreadyExistsException("Email already exists"); + } + // Create user with encrypted password User user = User.create( spec.firstName(), diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserSpecification.java b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserSpecification.java index d63a75c..941931b 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserSpecification.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserSpecification.java @@ -1,15 +1,19 @@ package com.zenfulcode.commercify.user.domain.valueobject; import com.zenfulcode.commercify.user.domain.model.UserRole; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import lombok.Builder; import java.util.Set; +@Builder public record UserSpecification( String firstName, String lastName, String email, String password, String phone, + UserStatus status, Set roles ) { public boolean hasBasicInfoUpdate() { diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index 4c73da1..91f4c48 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -12,8 +12,10 @@ spring.jpa.open-in-view=false # Liquibase spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml # Other configurations remain the same -security.jwt.expiration-time=3600000 -security.jwt.secret-key=${JWT_SECRET_KEY} +security.jwt.secret=${JWT_SECRET_KEY} +# 1h in millisecond +security.jwt.access-token-expiration=3600000 +security.jwt.refresh-token-expiration=86400000 logging.level.org.springframework=INFO stripe.secret-test-key=${STRIPE_SECRET_TEST_KEY} stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bb8c359..2171206 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,13 +10,17 @@ spring.jpa.hibernate.ddl-auto=none spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml #spring.liquibase.enabled=false #The secret key must be an HMAC hash string of 256 bits; -security.jwt.secret-key=${JWT_SECRET_KEY} +security.jwt.secret=${JWT_SECRET_KEY} # 1h in millisecond -security.jwt.expiration-time=3600000 +security.jwt.access-token-expiration=3600000 +security.jwt.refresh-token-expiration=86400000 +# Stripe Configuration stripe.secret-test-key=${STRIPE_SECRET_TEST_KEY:undefined} stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET:undefined} +# Admin Configuration admin.email=admin@commercify.app admin.password=commercifyadmin123! +# MobilePay Configuration mobilepay.client-id=${MOBILEPAY_CLIENT_ID} mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID} mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} From d70af6092fdbc545539ccb4bb2ed5df0b340c269 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 4 Jan 2025 16:28:08 +0100 Subject: [PATCH 13/57] removing unused imports --- .../com/zenfulcode/commercify/api/auth/AuthController.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java index fa809f4..4473b3f 100644 --- a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java +++ b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java @@ -7,7 +7,6 @@ import com.zenfulcode.commercify.auth.application.service.AuthenticationApplicationService; import com.zenfulcode.commercify.auth.application.service.AuthenticationResult; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; -import com.zenfulcode.commercify.user.application.command.CreateUserCommand; import com.zenfulcode.commercify.user.application.service.UserApplicationService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -16,8 +15,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Set; - @RestController @RequestMapping("/api/v1/auth") @RequiredArgsConstructor From 764cd9cf611ac6bd71e2d2c18c19ee1fc6ed7784 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 4 Jan 2025 16:29:12 +0100 Subject: [PATCH 14/57] adding non null annotation to JwtAuthenticationFiltern --- .../infrastructure/security/JwtAuthenticationFilter.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java index c7246b4..b7a4a61 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java @@ -6,6 +6,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -22,9 +23,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain ) throws ServletException, IOException { try { String token = extractJwtToken(request); From bf7f3378e7698ebebf065f369e17453b7ba69748 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 4 Jan 2025 18:00:37 +0100 Subject: [PATCH 15/57] fixing user authentication couldn't start application --- .../AuthenticationApplicationService.java | 11 ------- .../auth/domain/model/AuthenticatedUser.java | 2 ++ .../infrastructure/config/SecurityConfig.java | 17 +++++++--- .../infrastructure/mapper/UserRoleMapper.java | 17 ++++++++++ .../security/CustomUserDetailsService.java | 31 +++++++++++++++++++ .../DefaultAuthenticationDomainService.java | 6 ++-- 6 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/auth/infrastructure/mapper/UserRoleMapper.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/CustomUserDetailsService.java diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java index b2bf37f..3bbebe3 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java @@ -2,7 +2,6 @@ import com.zenfulcode.commercify.auth.domain.event.UserAuthenticatedEvent; import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; -import com.zenfulcode.commercify.auth.domain.model.UserRole; import com.zenfulcode.commercify.auth.domain.service.AuthenticationDomainService; import com.zenfulcode.commercify.auth.infrastructure.security.TokenService; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; @@ -16,10 +15,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class AuthenticationApplicationService { @@ -71,10 +66,4 @@ public AuthenticationResult refreshToken(String refreshToken) { return new AuthenticationResult(newAccessToken, newRefreshToken, authenticatedUser); } - - public Set mapRoles(Collection roles) { - return roles.stream() - .map(role -> UserRole.valueOf("ROLE_" + role.name())) - .collect(Collectors.toSet()); - } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java index b0f4c08..beda051 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java @@ -1,6 +1,7 @@ package com.zenfulcode.commercify.auth.domain.model; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; @@ -11,6 +12,7 @@ import java.util.Set; import java.util.stream.Collectors; +@Builder @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class AuthenticatedUser implements UserDetails { diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index 3b6b0de..9d000d8 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -1,5 +1,6 @@ package com.zenfulcode.commercify.auth.infrastructure.config; +import com.zenfulcode.commercify.auth.application.service.AuthenticationApplicationService; import com.zenfulcode.commercify.auth.infrastructure.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -24,13 +25,18 @@ @EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { - private final JwtAuthenticationFilter jwtAuthFilter; private final UserDetailsService userDetailsService; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) + public JwtAuthenticationFilter jwtAuthenticationFilter( + AuthenticationApplicationService authService) { + return new JwtAuthenticationFilter(authService); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, + JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/auth/**").permitAll() .requestMatchers("/api/v1/products").permitAll() @@ -41,7 +47,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authenticationProvider(authenticationProvider()) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/mapper/UserRoleMapper.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/mapper/UserRoleMapper.java new file mode 100644 index 0000000..ab5745d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/mapper/UserRoleMapper.java @@ -0,0 +1,17 @@ +package com.zenfulcode.commercify.auth.infrastructure.mapper; + +import com.zenfulcode.commercify.auth.domain.model.UserRole; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class UserRoleMapper { + public Set mapRoles(Collection roles) { + return roles.stream() + .map(role -> UserRole.valueOf("ROLE_" + role.name())) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/CustomUserDetailsService.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/CustomUserDetailsService.java new file mode 100644 index 0000000..aca7a06 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/CustomUserDetailsService.java @@ -0,0 +1,31 @@ +package com.zenfulcode.commercify.auth.infrastructure.security; + +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; +import com.zenfulcode.commercify.auth.infrastructure.mapper.UserRoleMapper; +import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + private final UserRoleMapper userRoleMapper; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + username)); + + return AuthenticatedUser.builder() + .userId(user.getId().toString()) + .email(user.getEmail()) + .password(user.getPassword()) + .roles(userRoleMapper.mapRoles(user.getRoles())) + .build(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/service/DefaultAuthenticationDomainService.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/service/DefaultAuthenticationDomainService.java index 908b8e7..3218012 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/service/DefaultAuthenticationDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/service/DefaultAuthenticationDomainService.java @@ -1,8 +1,8 @@ package com.zenfulcode.commercify.auth.infrastructure.service; -import com.zenfulcode.commercify.auth.application.service.AuthenticationApplicationService; import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; import com.zenfulcode.commercify.auth.domain.service.AuthenticationDomainService; +import com.zenfulcode.commercify.auth.infrastructure.mapper.UserRoleMapper; import com.zenfulcode.commercify.shared.domain.exception.UserAccountLockedException; import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.model.UserStatus; @@ -12,7 +12,7 @@ @Service @RequiredArgsConstructor public class DefaultAuthenticationDomainService implements AuthenticationDomainService { - private final AuthenticationApplicationService authenticationApplicationService; + private final UserRoleMapper roleMapper; @Override public AuthenticatedUser createAuthenticatedUser(User user) { @@ -23,7 +23,7 @@ public AuthenticatedUser createAuthenticatedUser(User user) { user.getFullName(), user.getEmail(), user.getPassword(), - authenticationApplicationService.mapRoles(user.getRoles()), + roleMapper.mapRoles(user.getRoles()), user.getStatus() == UserStatus.ACTIVE, user.getStatus() != UserStatus.DEACTIVATED, user.getStatus() != UserStatus.SUSPENDED, From 56a0471d948abfe34003141352426ca490f3e6a5 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 12 Jan 2025 20:32:29 +0100 Subject: [PATCH 16/57] adding admin user creation --- .../config/AdminUserLoader.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java diff --git a/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java b/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java new file mode 100644 index 0000000..a36c020 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java @@ -0,0 +1,45 @@ +package com.zenfulcode.commercify.user.infrastructure.config; + +import com.zenfulcode.commercify.user.application.command.CreateUserCommand; +import com.zenfulcode.commercify.user.application.service.UserApplicationService; +import com.zenfulcode.commercify.user.domain.exception.UserAlreadyExistsException; +import com.zenfulcode.commercify.user.domain.model.UserRole; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.util.Set; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class AdminUserLoader { + private final UserApplicationService userApplicationService; + + @Value("${admin.email}") + private String adminEmail; + + @Value("${admin.password}") + private String adminPassword; + + @PostConstruct + public void createAdminUser() { + try { + CreateUserCommand command = new CreateUserCommand( + adminEmail, + "User", + "Admin", + adminPassword, + Set.of(UserRole.ADMIN), + null + ); + + userApplicationService.createUser(command); + log.info("Admin user created"); + } catch (UserAlreadyExistsException e) { + log.warn("Admin user already exists with email: {}", adminEmail); + } + } +} From e3ade2f6ef17f41ce95bdbf8cd123e7bec03e66d Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 12 Jan 2025 21:35:56 +0100 Subject: [PATCH 17/57] updating user status and improving database migrations --- .../api/order/mapper/OrderDtoMapper.java | 2 +- .../api/product/mapper/ProductDtoMapper.java | 4 +- .../product/mapper/ProductResponseMapper.java | 2 +- .../application/dto/OrderDetailsDTO.java | 2 +- .../order/application/dto/OrderLineDTO.java | 2 +- .../commercify/order/domain/model/Order.java | 40 ++--- .../order/domain/model/OrderLine.java | 40 ++--- .../order/domain/model/OrderShippingInfo.java | 12 +- .../domain/service/OrderDomainService.java | 12 +- .../service/OrderValidationService.java | 4 +- .../order/domain/valueobject/OrderId.java | 2 - .../order/domain/valueobject/OrderLineId.java | 2 +- .../SpringDataJpaOrderLineRepository.java | 4 +- .../product/domain/model/Product.java | 53 +++--- .../product/domain/model/ProductVariant.java | 40 ++--- .../product/domain/model/VariantOption.java | 23 +-- .../domain/service/ProductDomainService.java | 2 +- .../domain/service/ProductFactory.java | 7 +- .../domain/valueobject/CategoryId.java | 2 +- .../product/domain/valueobject/ProductId.java | 2 - .../product/domain/valueobject/VariantId.java | 2 - .../shared/domain/model/StoredEvent.java | 26 +-- .../commercify/user/domain/model/User.java | 50 +++--- .../user/domain/valueobject/UserId.java | 2 - .../config/AdminUserLoader.java | 13 +- src/main/resources/application.properties | 2 +- .../db/changelog/db.changelog-master.xml | 3 +- .../migrations/250103220503-changelog.sql | 139 --------------- .../migrations/250104134452-changelog.sql | 37 ---- .../migrations/250112212603-changelog.sql | 166 ++++++++++++++++++ 30 files changed, 338 insertions(+), 359 deletions(-) delete mode 100644 src/main/resources/db/changelog/migrations/250103220503-changelog.sql delete mode 100644 src/main/resources/db/changelog/migrations/250104134452-changelog.sql create mode 100644 src/main/resources/db/changelog/migrations/250112212603-changelog.sql diff --git a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java index 1a72c4e..38d636f 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java @@ -108,7 +108,7 @@ public PagedOrderResponse toPagedResponse(Page orderPage) { private OrderSummaryResponse toSummaryResponse(Order order) { return new OrderSummaryResponse( order.getId().toString(), - order.getUserId().toString(), + order.getUser().getId().toString(), order.getStatus().toString(), new MoneyResponse( order.getTotalAmount().getAmount().doubleValue(), diff --git a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java index bd3a182..0779997 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java @@ -95,7 +95,7 @@ private List mapVariants(Set vari .map(variant -> new ProductVariantSummaryResponse( variant.getId().toString(), variant.getSku(), - variant.getOptions().stream() + variant.getVariantOptions().stream() .map(opt -> new ProductVariantSummaryResponse.VariantOptionResponse( opt.getName(), opt.getValue() @@ -121,7 +121,7 @@ public ProductDetailResponse toDetailResponse(Product product) { product.getStock(), toPriceResponse(product.getPrice()), product.isActive(), - mapVariants(product.getVariants()) + mapVariants(product.getProductVariants()) ); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java index d65aef0..3f32c49 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java @@ -62,7 +62,7 @@ private ProductVariantSummaryResponse toVariantResponse(ProductVariant variant) return new ProductVariantSummaryResponse( variant.getId().toString(), variant.getSku(), - toVariantOptionResponses(variant.getOptions()), + toVariantOptionResponses(variant.getVariantOptions()), toVariantPriceResponse(variant), variant.getStock() != null ? variant.getStock() : variant.getProduct().getStock() ); diff --git a/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java index fa260e0..212e367 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderDetailsDTO.java @@ -25,7 +25,7 @@ public record OrderDetailsDTO(OrderId id, public static OrderDetailsDTO fromOrder(Order order) { return new OrderDetailsDTO( order.getId(), - order.getUserId(), + order.getUser().getId(), order.getStatus(), order.getCurrency(), order.getTotalAmount(), diff --git a/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderLineDTO.java b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderLineDTO.java index eecd64b..0d958be 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderLineDTO.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/dto/OrderLineDTO.java @@ -18,7 +18,7 @@ public record OrderLineDTO(OrderLineId id, public static OrderLineDTO fromOrderLine(OrderLine orderLine) { return OrderLineDTO.builder() .id(orderLine.getId()) - .productId(orderLine.getProductId()) + .productId(orderLine.getProduct().getId()) .variantId(orderLine.getProductVariant() != null ? orderLine.getProductVariant().getId() : null) .quantity(orderLine.getQuantity()) diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java index 254cb6a..af6f5a6 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -10,45 +10,37 @@ import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.valueobject.UserId; import jakarta.persistence.*; -import lombok.AccessLevel; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import java.time.Instant; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; +@Getter +@Setter @Entity @Table(name = "orders") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Order extends AggregateRoot { @EmbeddedId private OrderId id; - private UserId userId; - - @OneToMany( - mappedBy = "order", - cascade = CascadeType.ALL, - orphanRemoval = true - ) - private Set orderLines = new HashSet<>(); + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(name = "status", nullable = false) private OrderStatus status; - @ManyToOne(cascade = CascadeType.ALL) - @JoinColumn(name = "order_shipping_info_id") - private OrderShippingInfo orderShippingInfo; - @Column(name = "currency") private String currency; + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) + private Set orderLines = new LinkedHashSet<>(); + @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "subtotal")), @@ -77,17 +69,14 @@ public class Order extends AggregateRoot { }) private Money totalAmount; - @Setter - @ManyToOne(optional = false) - @JoinColumn(name = "user_id", insertable = false, updatable = false) - private User user; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_shipping_info_id") + private OrderShippingInfo orderShippingInfo; @CreationTimestamp - @Column(name = "created_at", nullable = false) private Instant createdAt; @UpdateTimestamp - @Column(name = "updated_at") private Instant updatedAt; // Factory method @@ -98,7 +87,6 @@ public static Order create( ) { Order order = new Order(); order.id = OrderId.generate(); - order.userId = userId; order.currency = currency; order.status = OrderStatus.PENDING; order.orderShippingInfo = shippingInfo; @@ -107,7 +95,7 @@ public static Order create( // Register domain event order.registerEvent(new OrderCreatedEvent( order.getId(), - order.getUserId(), + userId, order.getCurrency() )); diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java index 1879984..8d5fada 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java @@ -1,31 +1,35 @@ package com.zenfulcode.commercify.order.domain.model; import com.zenfulcode.commercify.order.domain.valueobject.OrderLineId; +import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.model.ProductVariant; -import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.model.Money; import jakarta.persistence.*; -import lombok.AccessLevel; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +@Getter +@Setter @Entity @Table(name = "order_lines") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderLine { @EmbeddedId private OrderLineId id; - @Column(name = "product_id", nullable = false) - private ProductId productId; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "order_id", nullable = false) + private Order order; - @Column(nullable = false) - private Integer quantity; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "product_id", nullable = false) + private Product product; - @Column(name = "currency") - private String currency; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_variant_id") + private ProductVariant productVariant; + + @Column(name = "quantity", nullable = false) + private Integer quantity; @Embedded @AttributeOverrides({ @@ -34,32 +38,20 @@ public class OrderLine { }) private Money unitPrice; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_variant_id") - private ProductVariant productVariant; - - @Setter - @ManyToOne(optional = false) - @JoinColumn(name = "order_id") - private Order order; - public static OrderLine create( - ProductId productId, ProductVariant variant, Integer quantity, Money unitPrice ) { OrderLine line = new OrderLine(); line.id = OrderLineId.generate(); - line.productId = productId; line.productVariant = variant; line.quantity = quantity; line.unitPrice = unitPrice; - line.currency = unitPrice.getCurrency(); return line; } public Money getTotal() { return unitPrice.multiply(quantity); } -} +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java index b2ad29a..1453411 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java @@ -3,18 +3,18 @@ import com.zenfulcode.commercify.order.domain.valueobject.Address; import com.zenfulcode.commercify.order.domain.valueobject.CustomerDetails; import jakarta.persistence.*; -import lombok.AccessLevel; import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.Setter; +@Getter +@Setter @Entity @Table(name = "order_shipping_info") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderShippingInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long orderInfoId; + @Column(name = "id", nullable = false) + private Long id; @Column(name = "customer_first_name") private String customerFirstName; @@ -127,4 +127,4 @@ public Address toBillingAddress() { public boolean hasBillingAddress() { return billingStreet != null && !billingStreet.isBlank(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index 114225d..a5ec04d 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -10,6 +10,8 @@ import com.zenfulcode.commercify.product.domain.model.ProductVariant; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.user.application.service.UserApplicationService; +import com.zenfulcode.commercify.user.domain.model.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,6 +25,7 @@ public class OrderDomainService { private final OrderPricingStrategy pricingStrategy; private final OrderValidationService validationService; + private final UserApplicationService userApplicationService; public Order createOrder(OrderDetails orderDetails, List products, List variants) { // Create order with shipping info @@ -32,12 +35,16 @@ public Order createOrder(OrderDetails orderDetails, List products, List orderDetails.billingAddress() ); + User customer = userApplicationService.getUser(orderDetails.customerId()); + Order order = Order.create( - orderDetails.customerId(), + customer.getId(), orderDetails.currency(), shippingInfo ); + order.setUser(customer); + // Map products and variants for lookup Map productMap = products.stream() .collect(Collectors.toMap(Product::getId, Function.identity())); @@ -59,12 +66,13 @@ public Order createOrder(OrderDetails orderDetails, List products, List Money linePrice = calculateLinePrice(product, variant, lineDetails.quantity()); OrderLine line = OrderLine.create( - product.getId(), variant, lineDetails.quantity(), linePrice ); + line.setProduct(product); + order.addOrderLine(line); } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java index f32568f..fe740ca 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java @@ -47,10 +47,10 @@ public void validateCreateOrder(Order order) { private void validateOrderLines(Set orderLines, List violations) { for (OrderLine line : orderLines) { if (line.getQuantity() <= 0) { - violations.add("Quantity must be greater than 0 for product: " + line.getProductId()); + violations.add("Quantity must be greater than 0 for product: " + line.getProduct().getId()); } if (!line.getUnitPrice().isPositive()) { - violations.add("Unit price must be greater than 0 for product: " + line.getProductId()); + violations.add("Unit price must be greater than 0 for product: " + line.getProduct().getId()); } } } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java index 16200be..3c1f65b 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderId.java @@ -1,6 +1,5 @@ package com.zenfulcode.commercify.order.domain.valueobject; -import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; @@ -13,7 +12,6 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderId { - @Column(name = "order_id") private String id; private OrderId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java index 6c4418b..3e5c718 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderLineId.java @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class OrderLineId { - @Column(name = "orderline_id") + @Column(name = "id") private String id; private OrderLineId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderLineRepository.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderLineRepository.java index 9febaba..a10aedc 100644 --- a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderLineRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderLineRepository.java @@ -22,7 +22,7 @@ interface SpringDataJpaOrderLineRepository extends JpaRepository findActiveOrdersForProduct( @@ -43,7 +43,7 @@ Set findActiveOrdersForVariant( @Query(""" SELECT COUNT(ol) > 0 FROM OrderLine ol - WHERE ol.productId = :productId + WHERE ol.product.id = :productId """) boolean hasActiveOrders( @Param("productId") ProductId productId diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java index 5d785a2..3639d0c 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java @@ -9,37 +9,40 @@ import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; import com.zenfulcode.commercify.shared.domain.model.Money; import jakarta.persistence.*; -import lombok.*; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.NotImplementedException; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; -import java.util.HashSet; +import java.time.Instant; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Optional; import java.util.Set; -@Builder -@Entity -@Table(name = "products") @Getter @Setter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor +@Entity +@Table(name = "products") public class Product extends AggregateRoot { @EmbeddedId private ProductId id; - @Column(nullable = false) + @Column(name = "name", nullable = false) private String name; + @Column(name = "description") private String description; - @Column(nullable = false) - private int stock; + @Column(name = "stock", nullable = false) + private Integer stock; + @Column(name = "image_url") private String imageUrl; - @Column(nullable = false) - private boolean active; + @Column(name = "active", nullable = false) + private Boolean active = false; @Embedded @AttributeOverrides({ @@ -48,15 +51,19 @@ public class Product extends AggregateRoot { }) private Money price; - @Builder.Default - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "product") - private Set variants = new HashSet<>(); - @Embedded @AttributeOverride(name = "id", column = @Column(name = "category_id")) - private CategoryId categoryId; // Add this field + private CategoryId categoryId; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "product") + private Set productVariants = new LinkedHashSet<>(); + + @CreationTimestamp + private Instant createdAt; + + @UpdateTimestamp + private Instant updatedAt; - // Factory method for creating new products public static Product create(String name, String description, int stock, Money money) { Product product = new Product(); product.id = ProductId.generate(); @@ -124,7 +131,7 @@ public boolean hasEnoughStock(int quantity) { public void addVariant(ProductVariant variant) { Objects.requireNonNull(variant, "Variant cannot be null"); - variants.add(variant); + productVariants.add(variant); variant.setProduct(this); } @@ -132,12 +139,12 @@ public void removeVariant(ProductVariant variant) { if (variant.hasActiveOrders()) { throw new ProductModificationException("Cannot remove variant with active orders"); } - variants.remove(variant); + productVariants.remove(variant); variant.setProduct(null); } public boolean hasVariant(String sku) { - return variants.stream() + return productVariants.stream() .anyMatch(variant -> variant.getSku().equals(sku)); } @@ -168,4 +175,8 @@ public int hashCode() { public Optional findVariantBySku(String sku) { throw new NotImplementedException("find variant by sku has not been implemented"); } + + public boolean isActive() { + return active; + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java index 32d8636..24004ec 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java @@ -3,27 +3,34 @@ import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import com.zenfulcode.commercify.shared.domain.model.Money; import jakarta.persistence.*; -import lombok.*; +import lombok.Getter; +import lombok.Setter; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; -@Builder -@Entity -@Table(name = "product_variants") @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor +@Setter +@Entity +@Table(name = "product_variants", uniqueConstraints = { + @UniqueConstraint(name = "uc_product_variants_sku", columnNames = {"sku"}) +}) public class ProductVariant { @EmbeddedId private VariantId id; - @Column(nullable = false, unique = true) + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "product_id", nullable = false) + private Product product; + + @Column(name = "sku", nullable = false) private String sku; + @Column(name = "stock") private Integer stock; + @Column(name = "image_url") private String imageUrl; @Embedded @@ -33,21 +40,16 @@ public class ProductVariant { }) private Money price; - @Setter - @ManyToOne(fetch = FetchType.LAZY) - private Product product; - - @Builder.Default - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - private Set options = new HashSet<>(); + @OneToMany(mappedBy = "productVariant") + private Set variantOptions = new LinkedHashSet<>(); - // Factory method - public static ProductVariant create(String sku, Integer stock, Money price) { + public static ProductVariant create(String sku, Integer stock, Money price, String imageUrl) { ProductVariant variant = new ProductVariant(); variant.id = VariantId.generate(); variant.sku = Objects.requireNonNull(sku, "SKU is required"); variant.stock = stock; variant.price = price; + variant.imageUrl = imageUrl; return variant; } @@ -60,7 +62,7 @@ public void updateStock(Integer stock) { } public void addOption(String name, String value) { - options.add(VariantOption.create(name, value, this)); + variantOptions.add(VariantOption.create(name, value, this)); } public boolean hasActiveOrders() { @@ -103,6 +105,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(sku); } - - } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java index f287ca0..d0501ed 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/VariantOption.java @@ -1,35 +1,36 @@ package com.zenfulcode.commercify.product.domain.model; import jakarta.persistence.*; -import lombok.AccessLevel; import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.Setter; import java.util.Objects; +@Getter +@Setter @Entity @Table(name = "variant_options") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) public class VariantOption { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long optionId; + @Column(name = "id", nullable = false) + private Long id; - @Column(nullable = false) + @Column(name = "name", nullable = false) private String name; - @Column(nullable = false) + @Column(name = "value", nullable = false) private String value; - @ManyToOne(fetch = FetchType.LAZY) - private ProductVariant variant; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "product_variant_id", nullable = false) + private ProductVariant productVariant; public static VariantOption create(String name, String value, ProductVariant variant) { VariantOption option = new VariantOption(); option.name = Objects.requireNonNull(name, "Option name is required"); option.value = Objects.requireNonNull(value, "Option value is required"); - option.variant = Objects.requireNonNull(variant, "Variant is required"); + option.productVariant = Objects.requireNonNull(variant, "Variant is required"); return option; } @@ -39,7 +40,7 @@ public boolean equals(Object o) { if (!(o instanceof VariantOption that)) return false; return Objects.equals(name, that.name) && Objects.equals(value, that.value) && - Objects.equals(variant, that.variant); + Objects.equals(productVariant, that.productVariant); } @Override diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java index 8b2a69b..99f2a87 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java @@ -49,7 +49,7 @@ public ProductDeletionValidation validateProductDeletion(Product product) { } // Check variants - for (ProductVariant variant : product.getVariants()) { + for (ProductVariant variant : product.getProductVariants()) { if (orderLineRepository.hasActiveOrdersForVariant(variant.getId())) { issues.add("Variant " + variant.getSku() + " has active orders"); } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java index dc267dd..16f97d1 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java @@ -45,12 +45,7 @@ public void createVariants(Product product, List specs) { String sku = skuGenerator.generateSku(product, spec); Money variantPrice = pricingPolicy.calculateVariantPrice(product, spec); - ProductVariant variant = ProductVariant.builder() - .sku(sku) - .stock(spec.stock()) - .price(variantPrice) - .imageUrl(spec.imageUrl()) - .build(); + ProductVariant variant = ProductVariant.create(sku, spec.stock(), variantPrice, spec.imageUrl()); // Add variant options spec.options().forEach(option -> diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java index 4a527a3..b138e39 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/CategoryId.java @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CategoryId { - @Column(name = "category_id") + @Column(name = "id") private String id; private CategoryId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java index 9c9fc22..5b3badd 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductId.java @@ -1,6 +1,5 @@ package com.zenfulcode.commercify.product.domain.valueobject; -import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; @@ -13,7 +12,6 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProductId { - @Column(name = "product_id") private String id; private ProductId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java index 6850963..e2142fd 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java @@ -1,6 +1,5 @@ package com.zenfulcode.commercify.product.domain.valueobject; -import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; @@ -13,7 +12,6 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class VariantId extends ProductId { - @Column(name = "variant_id") private String id; private VariantId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java b/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java index 63257dd..0691a5b 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/model/StoredEvent.java @@ -1,36 +1,36 @@ package com.zenfulcode.commercify.shared.domain.model; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.AccessLevel; +import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import java.time.Instant; +@Getter +@Setter @Entity @Table(name = "domain_events") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor public class StoredEvent { @Id + @Column(name = "event_id", nullable = false) private String eventId; - @Column(nullable = false) + @Column(name = "event_type", nullable = false) private String eventType; - @Column(nullable = false, columnDefinition = "TEXT") + @Lob + @Column(name = "event_data", nullable = false) private String eventData; - @Column(nullable = false) + @Column(name = "occurred_on", nullable = false) private Instant occurredOn; - @Column + @Column(name = "aggregate_id") private String aggregateId; - @Column + @Column(name = "aggregate_type") private String aggregateType; public StoredEvent( @@ -47,4 +47,4 @@ public StoredEvent( this.aggregateId = aggregateId; this.aggregateType = aggregateType; } -} +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java index 3a08eee..0f38c6f 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -7,9 +7,7 @@ import com.zenfulcode.commercify.user.domain.event.UserStatusChangedEvent; import com.zenfulcode.commercify.user.domain.valueobject.UserId; import jakarta.persistence.*; -import lombok.AccessLevel; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -20,56 +18,56 @@ import java.util.Objects; import java.util.Set; -@Entity -@Table(name = "users") @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Setter +@Entity +@Table(name = "users", uniqueConstraints = { + @UniqueConstraint(name = "uc_users_email", columnNames = {"email"}) +}) public class User extends AggregateRoot { @EmbeddedId private UserId id; - @Column(nullable = false, unique = true) + @Column(name = "email", nullable = false, unique = true) private String email; - @Column(nullable = false) + @Column(name = "first_name", nullable = false) private String firstName; - @Column(nullable = false) + @Column(name = "last_name", nullable = false) private String lastName; - @Column(nullable = false) + @Column(name = "password", nullable = false) private String password; + @Column(name = "phone_number") private String phoneNumber; @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(name = "status", nullable = false) private UserStatus status; - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable( - name = "user_roles", - joinColumns = @JoinColumn(name = "user_id") - ) - @Enumerated(EnumType.STRING) - @Column(name = "role") - private Set roles = new HashSet<>(); - - @Setter - @OneToMany(mappedBy = "user", orphanRemoval = true) - private Set orders = new LinkedHashSet<>(); - @CreationTimestamp - @Column(nullable = false) private Instant createdAt; @UpdateTimestamp - @Column(nullable = false) private Instant updatedAt; + @Column(name = "last_login_at") private Instant lastLoginAt; - // Factory method + @OneToMany(mappedBy = "user") + private Set orders = new LinkedHashSet<>(); + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "user_roles", + joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id") + ) + @Enumerated(EnumType.STRING) + @Column(name = "role") + private Set roles = new HashSet<>(); + public static User create( String firstName, String lastName, diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserId.java b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserId.java index 3960b8f..1364bdf 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserId.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/valueobject/UserId.java @@ -1,6 +1,5 @@ package com.zenfulcode.commercify.user.domain.valueobject; -import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; @@ -13,7 +12,6 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class UserId { - @Column(name = "user_id") private String id; private UserId(String id) { diff --git a/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java b/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java index a36c020..d9ff033 100644 --- a/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java +++ b/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java @@ -1,9 +1,12 @@ package com.zenfulcode.commercify.user.infrastructure.config; +import com.zenfulcode.commercify.shared.domain.exception.DomainException; import com.zenfulcode.commercify.user.application.command.CreateUserCommand; +import com.zenfulcode.commercify.user.application.command.UpdateUserStatusCommand; import com.zenfulcode.commercify.user.application.service.UserApplicationService; -import com.zenfulcode.commercify.user.domain.exception.UserAlreadyExistsException; import com.zenfulcode.commercify.user.domain.model.UserRole; +import com.zenfulcode.commercify.user.domain.model.UserStatus; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,10 +39,12 @@ public void createAdminUser() { null ); - userApplicationService.createUser(command); + UserId adminId = userApplicationService.createUser(command); + userApplicationService.updateUserStatus(new UpdateUserStatusCommand(adminId, UserStatus.ACTIVE)); + log.info("Admin user created"); - } catch (UserAlreadyExistsException e) { - log.warn("Admin user already exists with email: {}", adminEmail); + } catch (DomainException e) { + log.warn("Failed to create admin user: {}", e.getMessage()); } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2171206..68e05c9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ spring.application.name=commercify server.port=6091 # Database configuration -spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATABASE_NAME:commercify_ddd_db}?createDatabaseIfNotExist=true +spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATABASE_NAME:commercifydb}?createDatabaseIfNotExist=true spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index 99c8378..eb8ae1a 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -3,6 +3,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - - + \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/250103220503-changelog.sql b/src/main/resources/db/changelog/migrations/250103220503-changelog.sql deleted file mode 100644 index e739c94..0000000 --- a/src/main/resources/db/changelog/migrations/250103220503-changelog.sql +++ /dev/null @@ -1,139 +0,0 @@ --- liquibase formatted sql - --- changeset gkhaavik:1735938302939-1 -CREATE TABLE domain_events -( - eventId VARCHAR(255) NOT NULL, - eventType VARCHAR(255) NOT NULL, - eventData TEXT NOT NULL, - occurredOn datetime NOT NULL, - aggregateId VARCHAR(255) NULL, - aggregateType VARCHAR(255) NULL, - CONSTRAINT pk_domain_events PRIMARY KEY (eventId) -); - --- changeset gkhaavik:1735938302939-2 -CREATE TABLE order_shipping_info -( - id BIGINT AUTO_INCREMENT NOT NULL, - customer_first_name VARCHAR(255) NULL, - customer_last_name VARCHAR(255) NULL, - customer_email VARCHAR(255) NULL, - customer_phone VARCHAR(255) NULL, - shipping_street VARCHAR(255) NOT NULL, - shipping_city VARCHAR(255) NOT NULL, - shipping_state VARCHAR(255) NULL, - shipping_zip VARCHAR(255) NOT NULL, - shipping_country VARCHAR(255) NOT NULL, - billing_street VARCHAR(255) NULL, - billing_city VARCHAR(255) NULL, - billing_state VARCHAR(255) NULL, - billing_zip VARCHAR(255) NULL, - billing_country VARCHAR(255) NULL, - CONSTRAINT pk_order_shipping_info PRIMARY KEY (id) -); - --- changeset gkhaavik:1735938302939-3 -CREATE TABLE products -( - product_id VARCHAR(255) NOT NULL, - name VARCHAR(255) NOT NULL, - description VARCHAR(255) NULL, - stock INT NOT NULL, - image_url VARCHAR(255) NULL, - active BIT(1) NOT NULL, - unit_price DECIMAL(19, 2) NULL, - currency VARCHAR(255) NULL, - category_id VARCHAR(255) NULL, - CONSTRAINT pk_products PRIMARY KEY (product_id) -); - --- changeset gkhaavik:1735938302939-4 -CREATE TABLE product_variants -( - variant_id VARCHAR(255) NOT NULL, - product_id VARCHAR(255) NOT NULL, - sku VARCHAR(255) NOT NULL, - stock INT NULL, - image_url VARCHAR(255) NULL, - unit_price DECIMAL(19, 2) NULL, - currency VARCHAR(255) NULL, - CONSTRAINT pk_product_variants PRIMARY KEY (variant_id), - CONSTRAINT uc_product_variants_sku UNIQUE (sku) -); - --- changeset gkhaavik:1735938302939-5 -CREATE TABLE variant_options -( - id BIGINT AUTO_INCREMENT NOT NULL, - name VARCHAR(255) NOT NULL, - value VARCHAR(255) NOT NULL, - product_variant_id VARCHAR(255) NOT NULL, - CONSTRAINT pk_variant_options PRIMARY KEY (id) -); - --- changeset gkhaavik:1735938302939-6 -CREATE TABLE orders -( - order_id VARCHAR(255) NOT NULL, - user_id VARCHAR(255) NULL, - status VARCHAR(255) NOT NULL, - currency VARCHAR(255) NULL, - subtotal DECIMAL(19, 2) NULL, - shipping_cost DECIMAL(19, 2) NULL, - tax DECIMAL(19, 2) NULL, - total_amount DECIMAL(19, 2) NULL, - order_shipping_info_id BIGINT NULL, - created_at datetime NOT NULL, - updated_at datetime NULL, - CONSTRAINT pk_orders PRIMARY KEY (order_id) -); - --- changeset gkhaavik:1735938302939-7 -CREATE TABLE order_lines -( - orderline_id VARCHAR(255) NOT NULL, - order_id VARCHAR(255) NOT NULL, - product_id VARCHAR(255) NOT NULL, - product_variant_id VARCHAR(255) NULL, - quantity INT NOT NULL, - unit_price DECIMAL(19, 2) NULL, - currency VARCHAR(255) NULL, - CONSTRAINT pk_order_lines PRIMARY KEY (orderline_id) -); - --- changeset gkhaavik:1735938302939-8 -ALTER TABLE product_variants - ADD CONSTRAINT FK_PRODUCT_VARIANTS_ON_PRODUCT - FOREIGN KEY (product_id) - REFERENCES products (product_id); - --- changeset gkhaavik:1735938302939-9 -ALTER TABLE variant_options - ADD CONSTRAINT FK_VARIANT_OPTIONS_ON_VARIANT - FOREIGN KEY (product_variant_id) - REFERENCES product_variants (variant_id); - --- changeset gkhaavik:1735938302939-10 -ALTER TABLE orders - ADD CONSTRAINT FK_ORDERS_ON_ORDER_SHIPPING_INFO - FOREIGN KEY (order_shipping_info_id) - REFERENCES order_shipping_info (id); - --- changeset gkhaavik:1735938302939-11 -ALTER TABLE order_lines - ADD CONSTRAINT FK_ORDER_LINES_ON_ORDER - FOREIGN KEY (order_id) - REFERENCES orders (order_id); - --- changeset gkhaavik:1735938302939-12 -ALTER TABLE order_lines - ADD CONSTRAINT FK_ORDER_LINES_ON_VARIANT - FOREIGN KEY (product_variant_id) - REFERENCES product_variants (variant_id); - --- changeset gkhaavik:1735938302939-13 -ALTER TABLE order_lines - ADD CONSTRAINT FK_ORDER_LINES_ON_PRODUCT - FOREIGN KEY (product_id) - REFERENCES products (product_id); \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/250104134452-changelog.sql b/src/main/resources/db/changelog/migrations/250104134452-changelog.sql deleted file mode 100644 index 4d5d29e..0000000 --- a/src/main/resources/db/changelog/migrations/250104134452-changelog.sql +++ /dev/null @@ -1,37 +0,0 @@ --- liquibase formatted sql - --- changeset gkhaavik:1735994692204-5 -CREATE TABLE user_roles -( - user_id VARCHAR(255) NOT NULL, - `role` VARCHAR(255) NULL -); - --- changeset gkhaavik:1735994692204-6 -CREATE TABLE users -( - email VARCHAR(255) NOT NULL, - firstName VARCHAR(255) NOT NULL, - lastName VARCHAR(255) NOT NULL, - password VARCHAR(255) NOT NULL, - phoneNumber VARCHAR(255) NULL, - status VARCHAR(255) NOT NULL, - createdAt datetime NOT NULL, - updatedAt datetime NOT NULL, - lastLoginAt datetime NULL, - user_id VARCHAR(255) NOT NULL, - CONSTRAINT pk_users PRIMARY KEY (user_id) -); - --- changeset gkhaavik:1735994692204-7 -ALTER TABLE users - ADD CONSTRAINT uc_users_email UNIQUE (email); - --- changeset gkhaavik:1735994692204-9 -ALTER TABLE orders - ADD CONSTRAINT FK_ORDERS_ON_USER FOREIGN KEY (user_id) REFERENCES users (user_id); - --- changeset gkhaavik:1735994692204-10 -ALTER TABLE user_roles - ADD CONSTRAINT fk_user_roles_on_user FOREIGN KEY (user_id) REFERENCES users (user_id); - diff --git a/src/main/resources/db/changelog/migrations/250112212603-changelog.sql b/src/main/resources/db/changelog/migrations/250112212603-changelog.sql new file mode 100644 index 0000000..7c4cfa1 --- /dev/null +++ b/src/main/resources/db/changelog/migrations/250112212603-changelog.sql @@ -0,0 +1,166 @@ +-- liquibase formatted sql + +-- changeset gkhaavik:1736713563747-1 +CREATE TABLE domain_events +( + event_id VARCHAR(255) NOT NULL, + event_type VARCHAR(255) NOT NULL, + event_data LONGTEXT NOT NULL, + occurred_on datetime NOT NULL, + aggregate_id VARCHAR(255) NULL, + aggregate_type VARCHAR(255) NULL, + CONSTRAINT pk_domain_events PRIMARY KEY (event_id) +); + +-- changeset gkhaavik:1736713563747-2 +CREATE TABLE order_lines +( + quantity INT NOT NULL, + id VARCHAR(255) NOT NULL, + unit_price DECIMAL NULL, + currency VARCHAR(255) NULL, + order_id VARCHAR(255) NOT NULL, + product_id VARCHAR(255) NOT NULL, + product_variant_id VARCHAR(255) NULL, + CONSTRAINT pk_order_lines PRIMARY KEY (id) +); + +-- changeset gkhaavik:1736713563747-3 +CREATE TABLE order_shipping_info +( + id BIGINT AUTO_INCREMENT NOT NULL, + customer_first_name VARCHAR(255) NULL, + customer_last_name VARCHAR(255) NULL, + customer_email VARCHAR(255) NULL, + customer_phone VARCHAR(255) NULL, + shipping_street VARCHAR(255) NOT NULL, + shipping_city VARCHAR(255) NOT NULL, + shipping_state VARCHAR(255) NULL, + shipping_zip VARCHAR(255) NOT NULL, + shipping_country VARCHAR(255) NOT NULL, + billing_street VARCHAR(255) NULL, + billing_city VARCHAR(255) NULL, + billing_state VARCHAR(255) NULL, + billing_zip VARCHAR(255) NULL, + billing_country VARCHAR(255) NULL, + CONSTRAINT pk_order_shipping_info PRIMARY KEY (id) +); + +-- changeset gkhaavik:1736713563747-4 +CREATE TABLE orders +( + status VARCHAR(255) NOT NULL, + currency VARCHAR(255) NULL, + order_shipping_info_id BIGINT NULL, + created_at datetime NOT NULL, + updated_at datetime NULL, + id VARCHAR(255) NOT NULL, + subtotal DECIMAL NULL, + shipping_cost DECIMAL NULL, + tax DECIMAL NULL, + total_amount DECIMAL NULL, + user_id VARCHAR(255) NULL, + CONSTRAINT pk_orders PRIMARY KEY (id) +); + +-- changeset gkhaavik:1736713563747-5 +CREATE TABLE product_variants +( + sku VARCHAR(255) NOT NULL, + stock INT NULL, + image_url VARCHAR(255) NULL, + id VARCHAR(255) NOT NULL, + unit_price DECIMAL NULL, + currency VARCHAR(255) NULL, + product_id VARCHAR(255) NOT NULL, + CONSTRAINT pk_product_variants PRIMARY KEY (id) +); + +-- changeset gkhaavik:1736713563747-6 +CREATE TABLE products +( + name VARCHAR(255) NOT NULL, + `description` VARCHAR(255) NULL, + stock INT NOT NULL, + image_url VARCHAR(255) NULL, + active BIT(1) NOT NULL, + id VARCHAR(255) NOT NULL, + unit_price DECIMAL NULL, + currency VARCHAR(255) NULL, + category_id VARCHAR(255) NULL, + CONSTRAINT pk_products PRIMARY KEY (id) +); + +-- changeset gkhaavik:1736713563747-7 +CREATE TABLE user_roles +( + user_id VARCHAR(255) NOT NULL, + `role` VARCHAR(255) NULL +); + +-- changeset gkhaavik:1736713563747-8 +CREATE TABLE users +( + email VARCHAR(255) NOT NULL, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + phone_number VARCHAR(255) NULL, + status VARCHAR(255) NOT NULL, + created_at datetime NOT NULL, + updated_at datetime NOT NULL, + last_login_at datetime NULL, + id VARCHAR(255) NOT NULL, + CONSTRAINT pk_users PRIMARY KEY (id) +); + +-- changeset gkhaavik:1736713563747-9 +CREATE TABLE variant_options +( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(255) NOT NULL, + value VARCHAR(255) NOT NULL, + product_variant_id VARCHAR(255) NOT NULL, + CONSTRAINT pk_variant_options PRIMARY KEY (id) +); + +-- changeset gkhaavik:1736713563747-10 +ALTER TABLE product_variants + ADD CONSTRAINT uc_product_variants_sku UNIQUE (sku); + +-- changeset gkhaavik:1736713563747-11 +ALTER TABLE users + ADD CONSTRAINT uc_users_email UNIQUE (email); + +-- changeset gkhaavik:1736713563747-12 +ALTER TABLE orders + ADD CONSTRAINT FK_ORDERS_ON_ORDER_SHIPPING_INFO FOREIGN KEY (order_shipping_info_id) REFERENCES order_shipping_info (id); + +-- changeset gkhaavik:1736713563747-13 +ALTER TABLE orders + ADD CONSTRAINT FK_ORDERS_ON_USER FOREIGN KEY (user_id) REFERENCES users (id); + +-- changeset gkhaavik:1736713563747-14 +ALTER TABLE order_lines + ADD CONSTRAINT FK_ORDER_LINES_ON_ORDER FOREIGN KEY (order_id) REFERENCES orders (id); + +-- changeset gkhaavik:1736713563747-15 +ALTER TABLE order_lines + ADD CONSTRAINT FK_ORDER_LINES_ON_PRODUCT FOREIGN KEY (product_id) REFERENCES products (id); + +-- changeset gkhaavik:1736713563747-16 +ALTER TABLE order_lines + ADD CONSTRAINT FK_ORDER_LINES_ON_PRODUCT_VARIANT FOREIGN KEY (product_variant_id) REFERENCES product_variants (id); + +-- changeset gkhaavik:1736713563747-17 +ALTER TABLE product_variants + ADD CONSTRAINT FK_PRODUCT_VARIANTS_ON_PRODUCT FOREIGN KEY (product_id) REFERENCES products (id); + +-- changeset gkhaavik:1736713563747-18 +ALTER TABLE variant_options + ADD CONSTRAINT FK_VARIANT_OPTIONS_ON_PRODUCT_VARIANT FOREIGN KEY (product_variant_id) REFERENCES product_variants (id); + +-- changeset gkhaavik:1736713563747-19 +ALTER TABLE user_roles + ADD CONSTRAINT fk_user_roles_on_user FOREIGN KEY (user_id) REFERENCES users (id); + From 3cf25d982b886e1b008f857bf144009122e6a46a Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 12 Jan 2025 23:48:20 +0100 Subject: [PATCH 18/57] Fixing authentication and cleanup --- .../commercify/api/auth/AuthController.java | 10 ++++----- .../api/auth/dto/request/LoginRequest.java | 2 +- .../commercify/api/order/OrderController.java | 2 +- .../api/product/ProductController.java | 4 ++-- .../AuthenticationApplicationService.java | 5 ++--- .../auth/domain/model/AuthenticatedUser.java | 8 +++---- .../infrastructure/config/SecurityConfig.java | 12 +++++----- .../security/JwtAuthenticationFilter.java | 3 +-- .../exception/GlobalExceptionHandler.java | 2 ++ .../domain/service/UserDomainService.java | 22 +++++-------------- .../config/AdminUserLoader.java | 3 +++ 11 files changed, 32 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java index 4473b3f..987d4a0 100644 --- a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java +++ b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java @@ -16,19 +16,17 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/auth") +@RequestMapping("/api/v2/auth") @RequiredArgsConstructor public class AuthController { - private final AuthenticationApplicationService authService; private final UserApplicationService userService; - @PostMapping("/login") + @PostMapping("/signin") public ResponseEntity> login( @RequestBody LoginRequest request) { - AuthenticationResult result = authService.authenticate( - request.username(), + request.email(), request.password() ); @@ -36,7 +34,7 @@ public ResponseEntity> login( return ResponseEntity.ok(ApiResponse.success(response)); } - @PostMapping("/register") + @PostMapping("/signup") public ResponseEntity> register( @RequestBody RegisterRequest request) { diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java index b8993ef..150d713 100644 --- a/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java @@ -1,7 +1,7 @@ package com.zenfulcode.commercify.api.auth.dto.request; public record LoginRequest( - String username, + String email, String password ) { } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java index b02819f..854cfbf 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java @@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/orders") +@RequestMapping("/api/v2/orders") @RequiredArgsConstructor public class OrderController { private final OrderApplicationService orderApplicationService; diff --git a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java index 8434164..5b94515 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java @@ -1,11 +1,11 @@ package com.zenfulcode.commercify.api.product; -import com.zenfulcode.commercify.api.product.mapper.ProductDtoMapper; import com.zenfulcode.commercify.api.product.dto.request.*; import com.zenfulcode.commercify.api.product.dto.response.CreateProductResponse; import com.zenfulcode.commercify.api.product.dto.response.PagedProductResponse; import com.zenfulcode.commercify.api.product.dto.response.ProductDetailResponse; import com.zenfulcode.commercify.api.product.dto.response.UpdateProductResponse; +import com.zenfulcode.commercify.api.product.mapper.ProductDtoMapper; import com.zenfulcode.commercify.api.product.mapper.ProductResponseMapper; import com.zenfulcode.commercify.product.application.command.*; import com.zenfulcode.commercify.product.application.query.ProductQuery; @@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/products") +@RequestMapping("/api/v2/products") @RequiredArgsConstructor public class ProductController { private final ProductApplicationService productApplicationService; diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java index 3bbebe3..3d0aad0 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java @@ -8,7 +8,7 @@ import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.repository.UserRepository; import com.zenfulcode.commercify.user.domain.valueobject.UserId; -import lombok.RequiredArgsConstructor; +import lombok.AllArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -16,7 +16,7 @@ import org.springframework.transaction.annotation.Transactional; @Service -@RequiredArgsConstructor +@AllArgsConstructor public class AuthenticationApplicationService { private final AuthenticationManager authenticationManager; private final AuthenticationDomainService authenticationDomainService; @@ -26,7 +26,6 @@ public class AuthenticationApplicationService { @Transactional public AuthenticationResult authenticate(String email, String password) { - // Authenticate using Spring Security Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(email, password) ); diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java index beda051..1d56589 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java @@ -91,21 +91,21 @@ public String getUsername() { @Override public boolean isAccountNonExpired() { - return accountNonExpired; + return true; } @Override public boolean isAccountNonLocked() { - return accountNonLocked; + return true; } @Override public boolean isCredentialsNonExpired() { - return credentialsNonExpired; + return true; } @Override public boolean isEnabled() { - return enabled; + return true; } } diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index 9d000d8..b56f1fb 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -37,10 +37,12 @@ public JwtAuthenticationFilter jwtAuthenticationFilter( public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { http.csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/**").permitAll() - .requestMatchers("/api/v1/products").permitAll() - .requestMatchers("/api/v1/products/{id}").permitAll() + .authorizeHttpRequests(req -> req + .requestMatchers( + "/api/v2/auth/**", + "/api/v2/products/active", + "/api/v2/products/{id}", + "/api/v2/payments/mobilepay/callback").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session @@ -71,4 +73,4 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java index b7a4a61..12b5cbc 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java @@ -33,8 +33,7 @@ protected void doFilterInternal( if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) { AuthenticatedUser user = authService.validateAccessToken(token); - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); diff --git a/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java b/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java index 4ce1d45..3ee44d5 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java @@ -21,6 +21,7 @@ public ResponseEntity> handleDomainException(DomainException e "DOMAIN_ERROR", 400 ); + return ResponseEntity.badRequest().body(response); } @@ -48,6 +49,7 @@ public ResponseEntity> handleNotFoundException( "NOT_FOUND", 404 ); + return ResponseEntity.status(404).body(response); } } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java index dbf80b9..aa3dc93 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java @@ -1,8 +1,6 @@ package com.zenfulcode.commercify.user.domain.service; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; -import com.zenfulcode.commercify.user.domain.event.UserCreatedEvent; -import com.zenfulcode.commercify.user.domain.event.UserStatusChangedEvent; import com.zenfulcode.commercify.user.domain.exception.UserAlreadyExistsException; import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.model.UserStatus; @@ -19,7 +17,6 @@ @Service @RequiredArgsConstructor public class UserDomainService { - private final UserStateFlow userStateFlow; private final UserValidationService validationService; private final DomainEventPublisher eventPublisher; private final PasswordEncoder passwordEncoder; @@ -39,7 +36,7 @@ public User createUser(UserSpecification spec) { spec.firstName(), spec.lastName(), spec.email(), - passwordEncoder.encode(spec.password()), + spec.password(), spec.roles(), spec.phone() ); @@ -47,12 +44,7 @@ public User createUser(UserSpecification spec) { // Validate user validationService.validateCreateUser(user); - // Register creation event - user.registerEvent(new UserCreatedEvent( - user.getId(), - user.getEmail(), - user.getStatus() - )); + eventPublisher.publish(user.getDomainEvents()); return user; } @@ -69,15 +61,9 @@ public void updateUserStatus(User user, UserStatus newStatus) { validationService.validateDeactivation(user); } - UserStatus oldStatus = user.getStatus(); user.updateStatus(newStatus); - // Register status change event - user.registerEvent(new UserStatusChangedEvent( - user.getId(), - oldStatus, - newStatus - )); + eventPublisher.publish(user.getDomainEvents()); } /** @@ -110,6 +96,8 @@ public void changePassword(User user, String newPassword) { // Update password user.updatePassword(passwordEncoder.encode(newPassword)); + + eventPublisher.publish(user.getDomainEvents()); } /** diff --git a/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java b/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java index d9ff033..0fac8b3 100644 --- a/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java +++ b/src/main/java/com/zenfulcode/commercify/user/infrastructure/config/AdminUserLoader.java @@ -4,6 +4,7 @@ import com.zenfulcode.commercify.user.application.command.CreateUserCommand; import com.zenfulcode.commercify.user.application.command.UpdateUserStatusCommand; import com.zenfulcode.commercify.user.application.service.UserApplicationService; +import com.zenfulcode.commercify.user.domain.exception.UserAlreadyExistsException; import com.zenfulcode.commercify.user.domain.model.UserRole; import com.zenfulcode.commercify.user.domain.model.UserStatus; import com.zenfulcode.commercify.user.domain.valueobject.UserId; @@ -43,6 +44,8 @@ public void createAdminUser() { userApplicationService.updateUserStatus(new UpdateUserStatusCommand(adminId, UserStatus.ACTIVE)); log.info("Admin user created"); + } catch (UserAlreadyExistsException e) { + log.warn("Admin user already exists"); } catch (DomainException e) { log.warn("Failed to create admin user: {}", e.getMessage()); } From 6d6a483f51fd7eaf6b05d039fc99eed2e6dd019d Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 13 Jan 2025 00:38:33 +0100 Subject: [PATCH 19/57] fixing order creation and database error --- .../commercify/order/domain/model/Order.java | 5 +- .../order/domain/model/OrderLine.java | 7 +-- .../domain/service/OrderDomainService.java | 12 +---- .../domain/valueobject/OrderDetails.java | 4 ++ .../product/domain/model/Product.java | 2 + .../product/domain/valueobject/VariantId.java | 1 + .../commercify/user/domain/model/User.java | 2 + .../db/changelog/db.changelog-master.xml | 2 +- ...angelog.sql => 250112235819-changelog.sql} | 46 ++++++++++--------- 9 files changed, 44 insertions(+), 37 deletions(-) rename src/main/resources/db/changelog/migrations/{250112212603-changelog.sql => 250112235819-changelog.sql} (84%) diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java index af6f5a6..b16fc6a 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -38,6 +38,7 @@ public class Order extends AggregateRoot { @Column(name = "currency") private String currency; + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) private Set orderLines = new LinkedHashSet<>(); @@ -69,14 +70,16 @@ public class Order extends AggregateRoot { }) private Money totalAmount; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "order_shipping_info_id") private OrderShippingInfo orderShippingInfo; @CreationTimestamp + @Column(name = "created_at") private Instant createdAt; @UpdateTimestamp + @Column(name = "updated_at") private Instant updatedAt; // Factory method diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java index 8d5fada..cf3c47d 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java @@ -39,15 +39,16 @@ public class OrderLine { private Money unitPrice; public static OrderLine create( + Product product, ProductVariant variant, - Integer quantity, - Money unitPrice + Integer quantity ) { OrderLine line = new OrderLine(); line.id = OrderLineId.generate(); + line.product = product; line.productVariant = variant; line.quantity = quantity; - line.unitPrice = unitPrice; + line.unitPrice = product.getEffectivePrice(variant); return line; } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index a5ec04d..77d0ef5 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -64,11 +64,10 @@ public Order createOrder(OrderDetails orderDetails, List products, List validationService.validateVariant(variant, product, lineDetails); } - Money linePrice = calculateLinePrice(product, variant, lineDetails.quantity()); OrderLine line = OrderLine.create( + product, variant, - lineDetails.quantity(), - linePrice + lineDetails.quantity() ); line.setProduct(product); @@ -110,11 +109,4 @@ public void updateOrderStatus(Order order, OrderStatus newStatus) { order.updateStatus(newStatus); } - - private Money calculateLinePrice(Product product, ProductVariant variant, int quantity) { - Money unitPrice = variant != null ? - variant.getEffectivePrice() : - product.getPrice(); - return unitPrice.multiply(quantity); - } } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java index 998a2f5..a5f9700 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/valueobject/OrderDetails.java @@ -29,6 +29,10 @@ private void validate( ) { List violations = new ArrayList<>(); + if (customerId == null) { + violations.add("Customer ID is required"); + } + if (currency == null || currency.isBlank()) { violations.add("Currency is required"); } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java index 3639d0c..3e6ec1b 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java @@ -59,9 +59,11 @@ public class Product extends AggregateRoot { private Set productVariants = new LinkedHashSet<>(); @CreationTimestamp + @Column(name = "created_at") private Instant createdAt; @UpdateTimestamp + @Column(name = "updated_at") private Instant updatedAt; public static Product create(String name, String description, int stock, Money money) { diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java index e2142fd..e67c0d0 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/VariantId.java @@ -23,6 +23,7 @@ public static VariantId generate() { } public static VariantId of(String id) { + if (id == null) return null; return new VariantId(id); } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java index 0f38c6f..d376ae3 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -48,9 +48,11 @@ public class User extends AggregateRoot { private UserStatus status; @CreationTimestamp + @Column(name = "created_at") private Instant createdAt; @UpdateTimestamp + @Column(name = "updated_at") private Instant updatedAt; @Column(name = "last_login_at") diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index eb8ae1a..b3f3584 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -3,5 +3,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - + \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/250112212603-changelog.sql b/src/main/resources/db/changelog/migrations/250112235819-changelog.sql similarity index 84% rename from src/main/resources/db/changelog/migrations/250112212603-changelog.sql rename to src/main/resources/db/changelog/migrations/250112235819-changelog.sql index 7c4cfa1..0335c28 100644 --- a/src/main/resources/db/changelog/migrations/250112212603-changelog.sql +++ b/src/main/resources/db/changelog/migrations/250112235819-changelog.sql @@ -1,6 +1,6 @@ -- liquibase formatted sql --- changeset gkhaavik:1736713563747-1 +-- changeset gkhaavik:1736722698857-1 CREATE TABLE domain_events ( event_id VARCHAR(255) NOT NULL, @@ -12,7 +12,7 @@ CREATE TABLE domain_events CONSTRAINT pk_domain_events PRIMARY KEY (event_id) ); --- changeset gkhaavik:1736713563747-2 +-- changeset gkhaavik:1736722698857-2 CREATE TABLE order_lines ( quantity INT NOT NULL, @@ -25,7 +25,7 @@ CREATE TABLE order_lines CONSTRAINT pk_order_lines PRIMARY KEY (id) ); --- changeset gkhaavik:1736713563747-3 +-- changeset gkhaavik:1736722698857-3 CREATE TABLE order_shipping_info ( id BIGINT AUTO_INCREMENT NOT NULL, @@ -46,13 +46,13 @@ CREATE TABLE order_shipping_info CONSTRAINT pk_order_shipping_info PRIMARY KEY (id) ); --- changeset gkhaavik:1736713563747-4 +-- changeset gkhaavik:1736722698857-4 CREATE TABLE orders ( status VARCHAR(255) NOT NULL, currency VARCHAR(255) NULL, order_shipping_info_id BIGINT NULL, - created_at datetime NOT NULL, + created_at datetime NULL, updated_at datetime NULL, id VARCHAR(255) NOT NULL, subtotal DECIMAL NULL, @@ -63,7 +63,7 @@ CREATE TABLE orders CONSTRAINT pk_orders PRIMARY KEY (id) ); --- changeset gkhaavik:1736713563747-5 +-- changeset gkhaavik:1736722698857-5 CREATE TABLE product_variants ( sku VARCHAR(255) NOT NULL, @@ -76,7 +76,7 @@ CREATE TABLE product_variants CONSTRAINT pk_product_variants PRIMARY KEY (id) ); --- changeset gkhaavik:1736713563747-6 +-- changeset gkhaavik:1736722698857-6 CREATE TABLE products ( name VARCHAR(255) NOT NULL, @@ -84,6 +84,8 @@ CREATE TABLE products stock INT NOT NULL, image_url VARCHAR(255) NULL, active BIT(1) NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, id VARCHAR(255) NOT NULL, unit_price DECIMAL NULL, currency VARCHAR(255) NULL, @@ -91,14 +93,14 @@ CREATE TABLE products CONSTRAINT pk_products PRIMARY KEY (id) ); --- changeset gkhaavik:1736713563747-7 +-- changeset gkhaavik:1736722698857-7 CREATE TABLE user_roles ( user_id VARCHAR(255) NOT NULL, `role` VARCHAR(255) NULL ); --- changeset gkhaavik:1736713563747-8 +-- changeset gkhaavik:1736722698857-8 CREATE TABLE users ( email VARCHAR(255) NOT NULL, @@ -107,14 +109,14 @@ CREATE TABLE users password VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NULL, status VARCHAR(255) NOT NULL, - created_at datetime NOT NULL, - updated_at datetime NOT NULL, + created_at datetime NULL, + updated_at datetime NULL, last_login_at datetime NULL, id VARCHAR(255) NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id) ); --- changeset gkhaavik:1736713563747-9 +-- changeset gkhaavik:1736722698857-9 CREATE TABLE variant_options ( id BIGINT AUTO_INCREMENT NOT NULL, @@ -124,43 +126,43 @@ CREATE TABLE variant_options CONSTRAINT pk_variant_options PRIMARY KEY (id) ); --- changeset gkhaavik:1736713563747-10 +-- changeset gkhaavik:1736722698857-10 ALTER TABLE product_variants ADD CONSTRAINT uc_product_variants_sku UNIQUE (sku); --- changeset gkhaavik:1736713563747-11 +-- changeset gkhaavik:1736722698857-11 ALTER TABLE users ADD CONSTRAINT uc_users_email UNIQUE (email); --- changeset gkhaavik:1736713563747-12 +-- changeset gkhaavik:1736722698857-12 ALTER TABLE orders ADD CONSTRAINT FK_ORDERS_ON_ORDER_SHIPPING_INFO FOREIGN KEY (order_shipping_info_id) REFERENCES order_shipping_info (id); --- changeset gkhaavik:1736713563747-13 +-- changeset gkhaavik:1736722698857-13 ALTER TABLE orders ADD CONSTRAINT FK_ORDERS_ON_USER FOREIGN KEY (user_id) REFERENCES users (id); --- changeset gkhaavik:1736713563747-14 +-- changeset gkhaavik:1736722698857-14 ALTER TABLE order_lines ADD CONSTRAINT FK_ORDER_LINES_ON_ORDER FOREIGN KEY (order_id) REFERENCES orders (id); --- changeset gkhaavik:1736713563747-15 +-- changeset gkhaavik:1736722698857-15 ALTER TABLE order_lines ADD CONSTRAINT FK_ORDER_LINES_ON_PRODUCT FOREIGN KEY (product_id) REFERENCES products (id); --- changeset gkhaavik:1736713563747-16 +-- changeset gkhaavik:1736722698857-16 ALTER TABLE order_lines ADD CONSTRAINT FK_ORDER_LINES_ON_PRODUCT_VARIANT FOREIGN KEY (product_variant_id) REFERENCES product_variants (id); --- changeset gkhaavik:1736713563747-17 +-- changeset gkhaavik:1736722698857-17 ALTER TABLE product_variants ADD CONSTRAINT FK_PRODUCT_VARIANTS_ON_PRODUCT FOREIGN KEY (product_id) REFERENCES products (id); --- changeset gkhaavik:1736713563747-18 +-- changeset gkhaavik:1736722698857-18 ALTER TABLE variant_options ADD CONSTRAINT FK_VARIANT_OPTIONS_ON_PRODUCT_VARIANT FOREIGN KEY (product_variant_id) REFERENCES product_variants (id); --- changeset gkhaavik:1736713563747-19 +-- changeset gkhaavik:1736722698857-19 ALTER TABLE user_roles ADD CONSTRAINT fk_user_roles_on_user FOREIGN KEY (user_id) REFERENCES users (id); From 7988df55d6a09e33aaea2bdccf76482579669f17 Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 13 Jan 2025 10:45:15 +0100 Subject: [PATCH 20/57] fixes get order by id --- .../api/order/dto/response/OrderLineResponse.java | 7 +++++-- .../commercify/api/order/mapper/OrderDtoMapper.java | 4 ++-- .../commercify/order/domain/model/OrderLine.java | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java index f22b751..0146e11 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java @@ -1,9 +1,12 @@ package com.zenfulcode.commercify.api.order.dto.response; +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.product.domain.valueobject.VariantId; + public record OrderLineResponse( String id, - String productId, - String variantId, + ProductId productId, + VariantId variantId, int quantity, MoneyResponse unitPrice, MoneyResponse total diff --git a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java index 38d636f..56fde45 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java @@ -121,8 +121,8 @@ private OrderSummaryResponse toSummaryResponse(Order order) { private OrderLineResponse toOrderLineResponse(OrderLineDTO line) { return new OrderLineResponse( line.id().toString(), - line.productId().toString(), - line.variantId().toString(), + line.productId(), + line.variantId(), line.quantity(), new MoneyResponse( line.unitPrice().getAmount().doubleValue(), diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java index cf3c47d..f60ae93 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderLine.java @@ -34,7 +34,7 @@ public class OrderLine { @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "unit_price")), - @AttributeOverride(name = "currency", column = @Column(name = "currency", insertable = false, updatable = false)) + @AttributeOverride(name = "currency", column = @Column(name = "currency")) }) private Money unitPrice; From a7ca0952a19f70ce3efba1624ff106a703548a04 Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 13 Jan 2025 10:50:03 +0100 Subject: [PATCH 21/57] Making sure that variant id is given to an order for a product that has variants --- .../order/domain/service/OrderValidationService.java | 8 ++++++++ .../commercify/product/domain/model/Product.java | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java index fe740ca..7fb5c0a 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java @@ -46,6 +46,14 @@ public void validateCreateOrder(Order order) { private void validateOrderLines(Set orderLines, List violations) { for (OrderLine line : orderLines) { + if (line.getProduct() == null) { + violations.add("Product is required for order line"); + } + + if (line.getProduct().hasVariants() && line.getProductVariant() == null) { + violations.add("Variant is required for product with variants"); + } + if (line.getQuantity() <= 0) { violations.add("Quantity must be greater than 0 for product: " + line.getProduct().getId()); } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java index 3e6ec1b..712e089 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java @@ -181,4 +181,8 @@ public Optional findVariantBySku(String sku) { public boolean isActive() { return active; } + + public boolean hasVariants() { + return !productVariants.isEmpty(); + } } \ No newline at end of file From 75a23ce5a9ffb5394b49fcce5e5c689d9e9dc07b Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 13 Jan 2025 10:57:52 +0100 Subject: [PATCH 22/57] commented debugging --- src/main/resources/application.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 68e05c9..8706c74 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -35,4 +35,5 @@ spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true # Application Configuration -app.frontend-url=${FRONTEND_URL:http://localhost:3000} \ No newline at end of file +app.frontend-url=${FRONTEND_URL:http://localhost:3000} +#logging.level.org.springframework.security=debug \ No newline at end of file From d0414316c69916a47d19fad9e09e161b930ea209 Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 13 Jan 2025 19:07:07 +0100 Subject: [PATCH 23/57] adding payment domain --- .../order/domain/model/OrderStatus.java | 1 + .../command/InitiatePaymentCommand.java | 14 + .../application/dto/PaymentResponse.java | 11 + .../service/PaymentApplicationService.java | 65 +++++ .../domain/event/IssuedRefundEvent.java | 44 ++++ .../domain/event/PaymentCancelledEvent.java | 33 +++ .../domain/event/PaymentCapturedEvent.java | 37 +++ .../domain/event/PaymentCreatedEvent.java | 42 +++ .../domain/event/PaymentFailedEvent.java | 33 +++ .../event/PaymentStatusChangedEvent.java | 48 ++++ .../InvalidPaymentStateException.java | 36 +++ .../PaymentProviderNotFoundException.java | 17 ++ .../exception/PaymentValidationException.java | 18 ++ .../payment/domain/model/Payment.java | 249 ++++++++++++++++++ .../payment/domain/model/PaymentMethod.java | 6 + .../payment/domain/model/PaymentProvider.java | 6 + .../service/MobilepayProviderService.java | 43 +++ .../domain/service/PaymentDomainService.java | 94 +++++++ .../service/PaymentProviderFactory.java | 33 +++ .../service/PaymentProviderService.java | 46 ++++ .../domain/service/PaymentStateFlow.java | 135 ++++++++++ .../service/PaymentValidationService.java | 151 +++++++++++ .../payment/domain/valueobject/PaymentId.java | 46 ++++ .../valueobject/PaymentProviderConfig.java | 7 + .../valueobject/PaymentProviderRequest.java | 5 + .../valueobject/PaymentProviderResponse.java | 9 + .../valueobject/PaymentStateMetadata.java | 20 ++ .../domain/valueobject/PaymentStatus.java | 12 + .../valueobject/refund/RefundReason.java | 28 ++ .../valueobject/refund/RefundRequest.java | 94 +++++++ .../valueobject/refund/RefundStatus.java | 10 + .../valueobject/webhook/WebhookPayload.java | 16 ++ .../DomainInvariantViolationException.java | 1 - .../exception/DomainValidationException.java | 1 - .../exception/EntityNotFoundException.java | 1 - 35 files changed, 1409 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/command/InitiatePaymentCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/dto/PaymentResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/exception/InvalidPaymentStateException.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentProviderNotFoundException.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentValidationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/model/PaymentMethod.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/model/PaymentProvider.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/service/MobilepayProviderService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderFactory.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentId.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStateMetadata.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundReason.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundStatus.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/WebhookPayload.java diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java index 00518c9..9561a1c 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java @@ -4,6 +4,7 @@ public enum OrderStatus { PENDING, // Order has been created but not yet confirmed CONFIRMED, // Order has been confirmed by the customer SHIPPED, // Order has been shipped + PAID, // Order has been paid COMPLETED, // Order has been delivered CANCELLED, // Order has been cancelled FAILED, // Order has failed diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/command/InitiatePaymentCommand.java b/src/main/java/com/zenfulcode/commercify/payment/application/command/InitiatePaymentCommand.java new file mode 100644 index 0000000..1f90547 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/command/InitiatePaymentCommand.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.payment.application.command; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; + +public record InitiatePaymentCommand( + Order order, + PaymentMethod paymentMethod, + PaymentProvider provider, + PaymentProviderRequest providerRequest +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/dto/PaymentResponse.java b/src/main/java/com/zenfulcode/commercify/payment/application/dto/PaymentResponse.java new file mode 100644 index 0000000..903725e --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/dto/PaymentResponse.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.payment.application.dto; + +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; + +import java.util.Map; + +public record PaymentResponse( + PaymentId paymentId, + String redirectUrl, + Map additionalData +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java new file mode 100644 index 0000000..7b3f7cd --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java @@ -0,0 +1,65 @@ +package com.zenfulcode.commercify.payment.application.service; + +import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; +import com.zenfulcode.commercify.payment.application.dto.PaymentResponse; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.service.PaymentDomainService; +import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; +import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderResponse; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PaymentApplicationService { + private final PaymentDomainService paymentDomainService; + private final PaymentProviderFactory providerFactory; + private final DomainEventPublisher eventPublisher; + + @Transactional + public PaymentResponse initiatePayment(InitiatePaymentCommand command) { + // Get the appropriate provider service + PaymentProviderService providerService = providerFactory.getProvider(command.provider()); + + // Validate provider-specific request + providerService.validateRequest(command.providerRequest()); + + // Create domain payment entity + Payment payment = paymentDomainService.createPayment( + command.order(), + command.paymentMethod(), + command.provider() + ); + + // Initiate payment with provider + PaymentProviderResponse providerResponse = providerService.initiatePayment( + payment, + command.order().getId(), + command.providerRequest() + ); + + // Update payment with provider reference + payment.updateProviderReference(providerResponse.providerReference()); + + // Publish events + eventPublisher.publish(payment.getDomainEvents()); + + // Return response + return new PaymentResponse( + payment.getId(), + providerResponse.redirectUrl(), + providerResponse.additionalData() + ); + } + + @Transactional + public void handlePaymentCallback(PaymentProvider provider, WebhookPayload payload) { + PaymentProviderService providerService = providerFactory.getProvider(provider); + providerService.handleCallback(payload); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java new file mode 100644 index 0000000..797c167 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java @@ -0,0 +1,44 @@ +package com.zenfulcode.commercify.payment.domain.event; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.payment.domain.valueobject.refund.RefundReason; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import lombok.Getter; + +import java.time.Instant; + +/** + * Event raised when payment is refunded + */ +@Getter +public class IssuedRefundEvent extends DomainEvent { + // Getters + @AggregateId + private final PaymentId paymentId; + private final OrderId orderId; + private final Money refundAmount; + private final RefundReason reason; + private final String notes; + private final boolean isFullRefund; + private final Instant refundedAt; + + public IssuedRefundEvent( + PaymentId paymentId, + OrderId orderId, + Money refundAmount, + RefundReason reason, + String notes, + boolean isFullRefund + ) { + this.paymentId = paymentId; + this.orderId = orderId; + this.refundAmount = refundAmount; + this.reason = reason; + this.notes = notes; + this.isFullRefund = isFullRefund; + this.refundedAt = Instant.now(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java new file mode 100644 index 0000000..480a1de --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java @@ -0,0 +1,33 @@ +package com.zenfulcode.commercify.payment.domain.event; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import lombok.Getter; + +import java.time.Instant; + +/** + * Event raised when payment is cancelled + */ +@Getter +public class PaymentCancelledEvent extends DomainEvent { + // Getters + @AggregateId + private final PaymentId paymentId; + private final OrderId orderId; + private final String reason; + private final Instant cancelledAt; + + public PaymentCancelledEvent( + PaymentId paymentId, + OrderId orderId, + String reason + ) { + this.paymentId = paymentId; + this.orderId = orderId; + this.reason = reason; + this.cancelledAt = Instant.now(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java new file mode 100644 index 0000000..78edb4c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java @@ -0,0 +1,37 @@ +package com.zenfulcode.commercify.payment.domain.event; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import lombok.Getter; + +import java.time.Instant; + +/** + * Event raised when payment is captured + */ +@Getter +public class PaymentCapturedEvent extends DomainEvent { + // Getters + @AggregateId + private final PaymentId paymentId; + private final OrderId orderId; + private final Money amount; + private final String transactionId; + private final Instant capturedAt; + + public PaymentCapturedEvent( + PaymentId paymentId, + OrderId orderId, + Money amount, + String transactionId + ) { + this.paymentId = paymentId; + this.orderId = orderId; + this.amount = amount; + this.transactionId = transactionId; + this.capturedAt = Instant.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java new file mode 100644 index 0000000..3f4ae34 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java @@ -0,0 +1,42 @@ +package com.zenfulcode.commercify.payment.domain.event; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import lombok.Getter; + +import java.time.Instant; + +/** + * Event raised when a payment is created + */ +@Getter +public class PaymentCreatedEvent extends DomainEvent { + // Getters + @AggregateId + private final PaymentId paymentId; + private final OrderId orderId; + private final Money amount; + private final PaymentMethod paymentMethod; + private final PaymentProvider provider; + private final Instant createdAt; + + public PaymentCreatedEvent( + PaymentId paymentId, + OrderId orderId, + Money amount, + PaymentMethod paymentMethod, + PaymentProvider provider + ) { + this.paymentId = paymentId; + this.orderId = orderId; + this.amount = amount; + this.paymentMethod = paymentMethod; + this.provider = provider; + this.createdAt = Instant.now(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java new file mode 100644 index 0000000..43a0558 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java @@ -0,0 +1,33 @@ +package com.zenfulcode.commercify.payment.domain.event; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import lombok.Getter; + +import java.time.Instant; + +/** + * Event raised when payment fails + */ +@Getter +public class PaymentFailedEvent extends DomainEvent { + // Getters + @AggregateId + private final PaymentId paymentId; + private final OrderId orderId; + private final String errorMessage; + private final Instant failedAt; + + public PaymentFailedEvent( + PaymentId paymentId, + OrderId orderId, + String errorMessage + ) { + this.paymentId = paymentId; + this.orderId = orderId; + this.errorMessage = errorMessage; + this.failedAt = Instant.now(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java new file mode 100644 index 0000000..4c8b616 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java @@ -0,0 +1,48 @@ +package com.zenfulcode.commercify.payment.domain.event; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import lombok.Getter; + +import java.time.Instant; + +/** + * Event raised when payment status changes + */ +@Getter +public class PaymentStatusChangedEvent extends DomainEvent { + // Getters + @AggregateId + private final PaymentId paymentId; + private final OrderId orderId; + private final PaymentStatus oldStatus; + private final PaymentStatus newStatus; + private final String transactionId; + private final Instant changedAt; + + public PaymentStatusChangedEvent( + PaymentId paymentId, + OrderId orderId, + PaymentStatus oldStatus, + PaymentStatus newStatus, + String transactionId + ) { + this.paymentId = paymentId; + this.orderId = orderId; + this.oldStatus = oldStatus; + this.newStatus = newStatus; + this.transactionId = transactionId; + this.changedAt = Instant.now(); + } + + public boolean isCompletedTransition() { + return newStatus == PaymentStatus.CAPTURED; + } + + public boolean isFailedTransition() { + return newStatus == PaymentStatus.FAILED; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/exception/InvalidPaymentStateException.java b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/InvalidPaymentStateException.java new file mode 100644 index 0000000..9e5332d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/InvalidPaymentStateException.java @@ -0,0 +1,36 @@ +package com.zenfulcode.commercify.payment.domain.exception; + +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import lombok.Getter; + +/** + * Thrown when payment is in invalid state for requested operation + */ +@Getter +public class InvalidPaymentStateException extends DomainException { + private final PaymentId paymentId; + private final PaymentStatus currentStatus; + private final PaymentStatus targetStatus; + private final String reason; + + public InvalidPaymentStateException( + PaymentId paymentId, + PaymentStatus currentStatus, + PaymentStatus targetStatus, + String reason + ) { + super(String.format( + "Cannot transition payment %s from %s to %s: %s", + paymentId, + currentStatus, + targetStatus, + reason + )); + this.paymentId = paymentId; + this.currentStatus = currentStatus; + this.targetStatus = targetStatus; + this.reason = reason; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentProviderNotFoundException.java b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentProviderNotFoundException.java new file mode 100644 index 0000000..398613b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentProviderNotFoundException.java @@ -0,0 +1,17 @@ +package com.zenfulcode.commercify.payment.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.EntityNotFoundException; +import lombok.Getter; + +/** + * Thrown when payment provider is not found or not supported + */ +@Getter +public class PaymentProviderNotFoundException extends EntityNotFoundException { + private final String provider; + + public PaymentProviderNotFoundException(String provider) { + super("PaymentProvider", provider); + this.provider = provider; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentValidationException.java b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentValidationException.java new file mode 100644 index 0000000..f9e27ad --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentValidationException.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.payment.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainValidationException; + +import java.util.List; + +/** + * Thrown when payment validation fails + */ +public class PaymentValidationException extends DomainValidationException { + public PaymentValidationException(String message, List violations) { + super(message, violations); + } + + public PaymentValidationException(String message) { + super(message, List.of(message)); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java new file mode 100644 index 0000000..ca1991e --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java @@ -0,0 +1,249 @@ +package com.zenfulcode.commercify.payment.domain.model; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.payment.domain.event.*; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.refund.RefundReason; +import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; +import com.zenfulcode.commercify.shared.domain.model.Money; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "payments") +@Getter +@Setter +@NoArgsConstructor +public class Payment extends AggregateRoot { + + @EmbeddedId + private PaymentId id; + + @OneToOne(orphanRemoval = true) + @JoinColumns({ + @JoinColumn(name = "order_id", referencedColumnName = "id") + }) + private Order order; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private PaymentStatus status; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "amount", column = @Column(name = "amount")), + @AttributeOverride(name = "currency", column = @Column(name = "currency")) + }) + private Money amount; + + @Enumerated(EnumType.STRING) + @Column(name = "payment_method", nullable = false) + private PaymentMethod paymentMethod; + + @Enumerated(EnumType.STRING) + @Column(name = "payment_provider", nullable = false) + private PaymentProvider provider; + + @Column(name = "provider_reference") + private String providerReference; + + @Column(name = "transaction_id") + private String transactionId; + + @Column(name = "error_message") + private String errorMessage; + + @ElementCollection + @CollectionTable( + name = "payment_attempts", + joinColumns = @JoinColumn(name = "payment_id") + ) + private List paymentAttempts = new ArrayList<>(); + + @Column(name = "retry_count") + private int retryCount = 0; + + @Column(name = "max_retries") + private int maxRetries = 3; + + @CreationTimestamp + @Column(name = "created_at") + private Instant createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private Instant updatedAt; + + @Column(name = "completed_at") + private Instant completedAt; + + // Factory method for creating new payments + public static Payment create( + Money amount, + PaymentMethod paymentMethod, + PaymentProvider provider + ) { + Payment payment = new Payment(); + payment.id = PaymentId.generate(); + payment.amount = amount; + payment.paymentMethod = paymentMethod; + payment.provider = provider; + payment.status = PaymentStatus.PENDING; + + // Register domain event + payment.registerEvent(new PaymentCreatedEvent( + payment.getId(), + payment.getOrder().getId(), + payment.getAmount(), + payment.getPaymentMethod(), + payment.getProvider() + )); + + return payment; + } + + // Domain methods + public void markAsCaptured(String transactionId, Money capturedAmount) { + PaymentStatus oldStatus = this.status; + + this.status = PaymentStatus.CAPTURED; + this.transactionId = transactionId; + this.completedAt = Instant.now(); + + registerEvent(new PaymentStatusChangedEvent( + this.id, + order.getId(), + oldStatus, + PaymentStatus.CAPTURED, + transactionId + )); + + registerEvent(new PaymentCapturedEvent( + id, + order.getId(), + capturedAmount, + transactionId + )); + } + + public void markAsFailed(String reason) { + PaymentStatus oldStatus = this.status; + + this.status = PaymentStatus.FAILED; + this.errorMessage = reason; + recordPaymentAttempt(false, reason); + + registerEvent(new PaymentStatusChangedEvent( + this.id, + order.getId(), + oldStatus, + PaymentStatus.FAILED, + null + )); + + registerEvent(new PaymentFailedEvent( + this.id, + order.getId(), + reason + )); + } + + public void processRefund(Money refundAmount, RefundReason reason, String notes) { + PaymentStatus oldStatus = this.status; + + // For full refunds + if (refundAmount.equals(this.amount)) { + this.status = PaymentStatus.REFUNDED; + } else { + this.status = PaymentStatus.PARTIALLY_REFUNDED; + } + + registerEvent(new PaymentStatusChangedEvent( + this.id, + order.getId(), + oldStatus, + this.status, + null + )); + + registerEvent(new IssuedRefundEvent( + this.id, + order.getId(), + refundAmount, + reason, + notes, + refundAmount.equals(this.amount) + )); + } + + public void cancel() { + PaymentStatus oldStatus = this.status; + + this.status = PaymentStatus.CANCELLED; + + registerEvent(new PaymentStatusChangedEvent( + this.id, + order.getId(), + oldStatus, + PaymentStatus.CANCELLED, + null + )); + + registerEvent(new PaymentCancelledEvent( + this.id, + order.getId(), + "Payment cancelled" + )); + } + + public void updateProviderReference(String reference) { + this.providerReference = reference; + } + + public boolean canRetry() { + return this.status == PaymentStatus.FAILED && + this.retryCount < this.maxRetries; + } + + public void recordPaymentAttempt(boolean successful, String details) { + PaymentAttempt attempt = new PaymentAttempt( + Instant.now(), + successful, + details + ); + + this.paymentAttempts.add(attempt); + if (!successful) { + this.retryCount++; + } + } + + @Embeddable + @Getter + @NoArgsConstructor + public static class PaymentAttempt { + @Column(name = "attempt_time") + private Instant attemptTime; + + @Column(name = "successful") + private boolean successful; + + @Column(name = "details") + private String details; + + public PaymentAttempt(Instant attemptTime, boolean successful, String details) { + this.attemptTime = attemptTime; + this.successful = successful; + this.details = details; + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/PaymentMethod.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/PaymentMethod.java new file mode 100644 index 0000000..4cc1d2d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/PaymentMethod.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.payment.domain.model; + +public enum PaymentMethod { + WALLET, // Payment was made using a wallet + CARD // Payment was made using a card +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/PaymentProvider.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/PaymentProvider.java new file mode 100644 index 0000000..fead84d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/PaymentProvider.java @@ -0,0 +1,6 @@ +package com.zenfulcode.commercify.payment.domain.model; + +public enum PaymentProvider { + MOBILEPAY, // Payment was made using MobilePay + STRIPE, // Payment was made using Stripe +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/MobilepayProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/MobilepayProviderService.java new file mode 100644 index 0000000..5858f3e --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/MobilepayProviderService.java @@ -0,0 +1,43 @@ +package com.zenfulcode.commercify.payment.domain.service; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderConfig; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderResponse; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; + +import java.util.Set; + +public class MobilepayProviderService implements PaymentProviderService { + @Override + public PaymentProviderResponse initiatePayment(Payment payment, OrderId orderId, PaymentProviderRequest request) { + return null; + } + + @Override + public void handleCallback(WebhookPayload payload) { + + } + + @Override + public Set getSupportedPaymentMethods() { + return Set.of(PaymentMethod.WALLET); + } + + @Override + public PaymentProviderConfig getProviderConfig() { + return null; + } + + @Override + public void validateRequest(PaymentProviderRequest request) { + + } + + @Override + public boolean supportsPaymentMethod(PaymentMethod method) { + return false; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java new file mode 100644 index 0000000..7810a64 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java @@ -0,0 +1,94 @@ +package com.zenfulcode.commercify.payment.domain.service; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.refund.RefundRequest; +import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PaymentDomainService { + private final PaymentValidationService validationService; + private final PaymentStateFlow paymentStateFlow; + private final DomainEventPublisher eventPublisher; + + /** + * Creates a new payment for an order + */ + public Payment createPayment(Order order, PaymentMethod paymentMethod, PaymentProvider provider) { + validationService.validateCreatePayment(order, paymentMethod, provider); + + // Create payment + Payment payment = Payment.create( + order.getTotalAmount(), + paymentMethod, + provider + ); + + payment.setOrder(order); + + return payment; + } + + /** + * Processes a successful payment capture + */ + public void capturePayment(Payment payment, String transactionId, Money capturedAmount) { + validationService.validatePaymentCapture(payment, capturedAmount); + + payment.markAsCaptured(transactionId, capturedAmount); + + // Publish payment captured event + eventPublisher.publish(payment.getDomainEvents()); + } + + /** + * Handles payment failures + */ + public void failPayment(Payment payment, String failureReason) { + // Validate current state + paymentStateFlow.validateStateTransition(payment.getStatus(), PaymentStatus.FAILED); + + payment.markAsFailed(failureReason); + + // Publish payment failed event + eventPublisher.publish(payment.getDomainEvents()); + } + + /** + * Processes payment refunds + */ + public void refundPayment(Payment payment, RefundRequest refundRequest) { + // Validate refund request + validationService.validateRefundRequest(payment, refundRequest); + + // Process refund + payment.processRefund( + payment.getAmount(), + refundRequest.reason(), + refundRequest.notes() + ); + + // Publish refund event + eventPublisher.publish(payment.getDomainEvents()); + } + + /** + * Cancels a pending payment + */ + public void cancelPayment(Payment payment) { + // Validate cancellation + validationService.validateStatusTransition(payment, PaymentStatus.CANCELLED); + + payment.cancel(); + + // Publish payment cancelled event + eventPublisher.publish(payment.getDomainEvents()); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderFactory.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderFactory.java new file mode 100644 index 0000000..6f3be44 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderFactory.java @@ -0,0 +1,33 @@ +package com.zenfulcode.commercify.payment.domain.service; + +import com.zenfulcode.commercify.payment.domain.exception.PaymentProviderNotFoundException; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PaymentProviderFactory { + private final Map providerServices = new HashMap<>(); + + public PaymentProviderService getProvider(PaymentProvider provider) { + PaymentProviderService service = providerServices.get(provider); + if (service == null) { + throw new PaymentProviderNotFoundException("Payment provider not found: " + provider); + } + return service; + } + + public List getAvailableProviders() { + return providerServices.values().stream() + .map(PaymentProviderService::getProviderConfig) + .filter(PaymentProviderConfig::isActive) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java new file mode 100644 index 0000000..caf57ee --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java @@ -0,0 +1,46 @@ +package com.zenfulcode.commercify.payment.domain.service; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderConfig; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderResponse; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; + +import java.util.Set; + +/** + * Base interface for all payment provider integrations + */ +public interface PaymentProviderService { + /** + * Initialize a payment with the provider + */ + PaymentProviderResponse initiatePayment(Payment payment, OrderId orderId, PaymentProviderRequest request); + + /** + * Handle provider webhook callbacks + */ + void handleCallback(WebhookPayload payload); + + /** + * Get supported payment methods + */ + Set getSupportedPaymentMethods(); + + /** + * Get provider-specific configuration + */ + PaymentProviderConfig getProviderConfig(); + + /** + * Validate provider-specific request + */ + void validateRequest(PaymentProviderRequest request); + + /** + * Check if provider supports payment method + */ + boolean supportsPaymentMethod(PaymentMethod method); +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java new file mode 100644 index 0000000..4ac64f1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java @@ -0,0 +1,135 @@ +package com.zenfulcode.commercify.payment.domain.service; + +import com.zenfulcode.commercify.payment.domain.exception.InvalidPaymentStateException; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStateMetadata; +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.Set; + +@Component +public class PaymentStateFlow { + private final EnumMap> validTransitions; + private final EnumMap terminalStates; + + public PaymentStateFlow() { + this.validTransitions = new EnumMap<>(PaymentStatus.class); + this.terminalStates = new EnumMap<>(PaymentStatus.class); + initializeStateTransitions(); + } + + /** + * Initialize valid state transitions + */ + private void initializeStateTransitions() { + // Initial state -> PENDING + validTransitions.put(PaymentStatus.PENDING, Set.of( + PaymentStatus.CAPTURED, + PaymentStatus.FAILED, + PaymentStatus.CANCELLED, + PaymentStatus.EXPIRED + )); + + // PAID -> REFUNDED or PARTIALLY_REFUNDED + validTransitions.put(PaymentStatus.CAPTURED, Set.of( + PaymentStatus.REFUNDED, + PaymentStatus.PARTIALLY_REFUNDED + )); + + // FAILED -> Can retry (go back to PENDING) or CANCELLED + validTransitions.put(PaymentStatus.FAILED, Set.of( + PaymentStatus.PENDING, + PaymentStatus.CANCELLED + )); + + // PARTIALLY_REFUNDED -> REFUNDED + validTransitions.put(PaymentStatus.PARTIALLY_REFUNDED, Set.of( + PaymentStatus.REFUNDED + )); + + // Terminal states have no further transitions + validTransitions.put(PaymentStatus.REFUNDED, Set.of()); + validTransitions.put(PaymentStatus.CANCELLED, Set.of()); + validTransitions.put(PaymentStatus.EXPIRED, Set.of()); + + // Mark terminal states + terminalStates.put(PaymentStatus.REFUNDED, true); + terminalStates.put(PaymentStatus.CANCELLED, true); + terminalStates.put(PaymentStatus.EXPIRED, true); + terminalStates.put(PaymentStatus.CAPTURED, false); + terminalStates.put(PaymentStatus.PENDING, false); + terminalStates.put(PaymentStatus.FAILED, false); + terminalStates.put(PaymentStatus.PARTIALLY_REFUNDED, false); + } + + /** + * Check if a state transition is valid + */ + public boolean canTransitionTo(PaymentStatus currentState, PaymentStatus targetState) { + Set allowedTransitions = validTransitions.get(currentState); + return allowedTransitions != null && allowedTransitions.contains(targetState); + } + + /** + * Get valid next states for a given state + */ + public Set getValidTransitions(PaymentStatus currentState) { + return validTransitions.getOrDefault(currentState, Set.of()); + } + + /** + * Check if a state is terminal + */ + public boolean isTerminalState(PaymentStatus state) { + return terminalStates.getOrDefault(state, false); + } + + /** + * Validate state transition and throw exception if invalid + */ + public void validateStateTransition(PaymentStatus currentState, PaymentStatus targetState) { + if (!canTransitionTo(currentState, targetState)) { + throw new InvalidPaymentStateException( + null, // PaymentId would be set by the calling service + currentState, + targetState, + "Invalid payment status transition" + ); + } + } + + /** + * Get state transition diagram for documentation + */ + public String getStateTransitionDiagram() { + StringBuilder diagram = new StringBuilder(); + diagram.append("Payment State Transitions:\n"); + diagram.append("------------------------\n"); + + validTransitions.forEach((state, transitions) -> { + if (!transitions.isEmpty()) { + diagram.append(state).append(" -> "); + diagram.append(String.join(" | ", transitions.stream() + .map(PaymentStatus::name) + .toList())); + diagram.append("\n"); + } else { + diagram.append(state).append(" (Terminal State)\n"); + } + }); + + return diagram.toString(); + } + + /** + * Get state metadata including whether it's terminal and valid transitions + */ + public PaymentStateMetadata getStateMetadata(PaymentStatus state) { + return new PaymentStateMetadata( + state, + isTerminalState(state), + getValidTransitions(state) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java new file mode 100644 index 0000000..a1be716 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java @@ -0,0 +1,151 @@ +package com.zenfulcode.commercify.payment.domain.service; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.service.OrderStateFlow; +import com.zenfulcode.commercify.payment.domain.exception.InvalidPaymentStateException; +import com.zenfulcode.commercify.payment.domain.exception.PaymentValidationException; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.refund.RefundRequest; +import com.zenfulcode.commercify.shared.domain.model.Money; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class PaymentValidationService { + private final PaymentStateFlow stateFlow; + + private static final Set SUPPORTED_CURRENCIES = Set.of("USD", "EUR", "GBP", "DKK"); + private final PaymentProviderFactory paymentProviderFactory; + private final OrderStateFlow orderStateFlow; + + /** + * Validates payment creation + */ + public void validateCreatePayment(Order order, PaymentMethod paymentMethod, PaymentProvider provider) { + List violations = new ArrayList<>(); + + validateAmount(order.getTotalAmount(), violations); + + // Validate payment method + validatePaymentMethod(paymentMethod, provider, violations); + + // Validate order state + validateOrderForPayment(order, violations); + + if (!violations.isEmpty()) { + throw new PaymentValidationException("Payment validation failed", violations); + } + } + + /** + * Validates payment for capture + */ + public void validatePaymentCapture(Payment payment, Money captureAmount) { + List violations = new ArrayList<>(); + + // Validate payment state + if (payment.getStatus() != PaymentStatus.PENDING) { + violations.add("Payment must be in PENDING state to be captured"); + } + + // Validate capture amount matches payment amount + if (!captureAmount.equals(payment.getAmount())) { + violations.add("Capture amount must match payment amount"); + } + + if (!violations.isEmpty()) { + throw new PaymentValidationException("Payment capture validation failed", violations); + } + } + + /** + * Validates payment for refund + */ + public void validateRefundRequest(Payment payment, RefundRequest refundRequest) { + List violations = new ArrayList<>(); + + // Validate payment state + if (payment.getStatus() != PaymentStatus.CAPTURED) { + violations.add("Only paid payments can be refunded"); + } + + if (refundRequest.reason() == null) { + violations.add("Refund reason is required"); + } + + // Validate refund amount + validateRefundAmount(payment, refundRequest.amount(), violations); + + if (!violations.isEmpty()) { + throw new PaymentValidationException("Refund validation failed", violations); + } + } + + /** + * Validates payment status transition + */ + public void validateStatusTransition(Payment payment, PaymentStatus newStatus) { + if (!stateFlow.canTransitionTo(payment.getStatus(), newStatus)) { + throw new InvalidPaymentStateException( + payment.getId(), + payment.getStatus(), + newStatus, + "Invalid payment status transition" + ); + } + } + + private void validateAmount(Money amount, List violations) { + // Validate currency + if (!SUPPORTED_CURRENCIES.contains(amount.getCurrency())) { + violations.add("Unsupported currency: " + amount.getCurrency()); + } + } + + private void validatePaymentMethod(PaymentMethod method, PaymentProvider provider, List violations) { + PaymentProviderService providerService = paymentProviderFactory.getProvider(provider); + + // Validate payment method is supported by provider + if (!providerService.supportsPaymentMethod(method)) { + violations.add("Payment method " + method + " is not supported by provider " + provider); + } + } + + private void validateOrderForPayment(Order order, List violations) { + // Validate order has total amount + if (order.getTotalAmount() == null || order.getTotalAmount().isZero()) { + violations.add("Order must have a valid total amount"); + } + + // Validate order status allows payment + if (orderStateFlow.canTransition(order.getStatus(), OrderStatus.PAID)) { + violations.add("Order status does not allow payment"); + } + } + + private void validateRefundAmount(Payment payment, Money refundAmount, List violations) { + // Validate currency matches + if (!refundAmount.getCurrency().equals(payment.getAmount().getCurrency())) { + violations.add("Refund currency must match payment currency"); + } + + // Validate refund amount does not exceed payment amount + if (refundAmount.isGreaterThan(payment.getAmount())) { + violations.add("Refund amount cannot exceed payment amount"); + } + + // Validate refund amount is positive + if (refundAmount.isZero() || refundAmount.isNegative()) { + violations.add("Refund amount must be positive"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentId.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentId.java new file mode 100644 index 0000000..c81ef20 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentId.java @@ -0,0 +1,46 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.UUID; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PaymentId { + private String id; + + private PaymentId(String id) { + this.id = Objects.requireNonNull(id); + } + + public static PaymentId generate() { + return new PaymentId(UUID.randomUUID().toString()); + } + + public static PaymentId of(String id) { + return new PaymentId(id); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PaymentId paymentId = (PaymentId) o; + return Objects.equals(id, paymentId.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderConfig.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderConfig.java new file mode 100644 index 0000000..4f4c324 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderConfig.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +import java.util.Map; + +public record PaymentProviderConfig(String provider, boolean isActive, Map config) { +} + diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderRequest.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderRequest.java new file mode 100644 index 0000000..dab0a1b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderRequest.java @@ -0,0 +1,5 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +public interface PaymentProviderRequest { +} + diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderResponse.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderResponse.java new file mode 100644 index 0000000..1f3054b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderResponse.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +import java.util.Map; + +public record PaymentProviderResponse( + String providerReference, + String redirectUrl, + Map additionalData +) {} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStateMetadata.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStateMetadata.java new file mode 100644 index 0000000..3d8b06c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStateMetadata.java @@ -0,0 +1,20 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +import java.util.Set; + +/** + * Record to hold state metadata + */ +public record PaymentStateMetadata( + PaymentStatus state, + boolean isTerminal, + Set validTransitions +) { + public boolean canTransitionTo(PaymentStatus targetState) { + return validTransitions.contains(targetState); + } + + public boolean hasTransitions() { + return !validTransitions.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java new file mode 100644 index 0000000..685fb33 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +public enum PaymentStatus { + PENDING, // Payment has been initiated but not completed + CAPTURED, // Payment has been successfully captured + FAILED, // Payment attempt failed + CANCELLED, // Payment was cancelled + REFUNDED, // Payment was fully refunded + PARTIALLY_REFUNDED, // Payment was partially refunded + EXPIRED, // Payment expired before completion + TERMINATED // Payment was terminated by the system +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundReason.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundReason.java new file mode 100644 index 0000000..3b8cd29 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundReason.java @@ -0,0 +1,28 @@ +package com.zenfulcode.commercify.payment.domain.valueobject.refund; + +import lombok.Getter; + +/** + * Enum for refund reasons + */ +@Getter +public enum RefundReason { + CUSTOMER_REQUEST("Customer requested refund"), + DUPLICATE_PAYMENT("Duplicate payment"), + FRAUDULENT_CHARGE("Fraudulent charge"), + ORDER_CANCELLED("Order cancelled"), + PRODUCT_UNAVAILABLE("Product unavailable"), + SHIPPING_ADDRESS_INVALID("Invalid shipping address"), + PRODUCT_DAMAGED("Product damaged"), + WRONG_ITEM("Wrong item delivered"), + QUALITY_ISSUE("Product quality issue"), + LATE_DELIVERY("Late delivery"), + OTHER("Other reason"); + + private final String description; + + RefundReason(String description) { + this.description = description; + } + +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundRequest.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundRequest.java new file mode 100644 index 0000000..151f3cf --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundRequest.java @@ -0,0 +1,94 @@ +package com.zenfulcode.commercify.payment.domain.valueobject.refund; + +import com.zenfulcode.commercify.payment.domain.exception.PaymentValidationException; +import com.zenfulcode.commercify.shared.domain.model.Money; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.Builder; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +@Embeddable +public class RefundRequest { + @Column(name = "refund_amount") + private Money amount; + + @Enumerated(EnumType.STRING) + @Column(name = "refund_reason") + private RefundReason reason; + + @Column(name = "refund_notes") + private String notes; + + @Column(name = "requested_by") + private String requestedBy; + + @Column(name = "requested_at") + private Instant requestedAt; + + protected RefundRequest() { + // For JPA + } + + @Builder + public RefundRequest( + Money amount, + RefundReason reason, + String notes, + String requestedBy + ) { + validate(amount, reason); + + this.amount = amount; + this.reason = reason; + this.notes = notes; + this.requestedBy = requestedBy; + this.requestedAt = Instant.now(); + } + + private void validate(Money amount, RefundReason reason) { + List violations = new ArrayList<>(); + + if (amount == null) { + violations.add("Refund amount is required"); + } else if (amount.isNegative() || amount.isZero()) { + violations.add("Refund amount must be positive"); + } + + if (reason == null) { + violations.add("Refund reason is required"); + } + + if (!violations.isEmpty()) { + throw new PaymentValidationException("Invalid refund request", violations); + } + } + + public Money amount() { + return amount; + } + + public RefundReason reason() { + return reason; + } + + public String notes() { + return notes; + } + + public String requestedBy() { + return requestedBy; + } + + public Instant requestedAt() { + return requestedAt; + } + + public boolean isFullRefund(Money paymentAmount) { + return amount.equals(paymentAmount); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundStatus.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundStatus.java new file mode 100644 index 0000000..eaebcca --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/refund/RefundStatus.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.payment.domain.valueobject.refund; + +/** + * Enum for refund status + */ +public enum RefundStatus { + PENDING, + COMPLETED, + FAILED +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/WebhookPayload.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/WebhookPayload.java new file mode 100644 index 0000000..917c0ff --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/WebhookPayload.java @@ -0,0 +1,16 @@ +package com.zenfulcode.commercify.payment.domain.valueobject.webhook; + +import java.time.Instant; + +/** + * Base interface for all webhook payloads + */ +public interface WebhookPayload { + String getEventType(); + + String getPaymentReference(); + + Instant getTimestamp(); + + boolean isValid(); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainInvariantViolationException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainInvariantViolationException.java index 5b57998..ce0383b 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainInvariantViolationException.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainInvariantViolationException.java @@ -10,5 +10,4 @@ public DomainInvariantViolationException(String invariantName, String message) { super(message); this.invariantName = invariantName; } - } diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainValidationException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainValidationException.java index a34be16..eec40d5 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainValidationException.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainValidationException.java @@ -13,5 +13,4 @@ public DomainValidationException(String message, List violations) { super(message); this.violations = Collections.unmodifiableList(violations); } - } diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EntityNotFoundException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EntityNotFoundException.java index 3740431..8deb29a 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EntityNotFoundException.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EntityNotFoundException.java @@ -12,5 +12,4 @@ public EntityNotFoundException(String entityType, Object entityId) { this.entityType = entityType; this.entityId = entityId; } - } \ No newline at end of file From 2c0888a08c99c9d34040afd30718c94fe48ffad6 Mon Sep 17 00:00:00 2001 From: GustavH Date: Tue, 21 Jan 2025 21:09:18 +0100 Subject: [PATCH 24/57] adding payments migration adding capture command moving payment created event into the domain service adding payment repository --- .../command/CapturePaymentCommand.java | 11 +++++ .../application/dto/CaptureResponse.java | 9 ++++ .../service/PaymentApplicationService.java | 14 ++++++ .../exception/PaymentNotFoundException.java | 9 ++++ .../payment/domain/model/Payment.java | 11 +---- .../domain/repository/PaymentRepository.java | 23 +++++++++ .../domain/service/PaymentDomainService.java | 22 ++++++++- .../domain/service/PaymentStateFlow.java | 16 +++++-- .../service/PaymentValidationService.java | 4 +- .../domain/valueobject/PaymentStatus.java | 1 + .../persistence/JpaPaymentRepository.java | 48 +++++++++++++++++++ .../SpringDataJpaPaymentRepository.java | 18 +++++++ src/main/resources/application.properties | 2 +- .../db/changelog/db.changelog-master.xml | 1 + ...250121194314-adding-payments-changelog.sql | 40 ++++++++++++++++ 15 files changed, 211 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentNotFoundException.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/repository/PaymentRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/JpaPaymentRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/SpringDataJpaPaymentRepository.java create mode 100644 src/main/resources/db/changelog/migrations/250121194314-adding-payments-changelog.sql diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java b/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java new file mode 100644 index 0000000..397e3e9 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.payment.application.command; + +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.shared.domain.model.Money; + +public record CapturePaymentCommand( + PaymentId paymentId, + String transactionId, + Money captureAmount +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureResponse.java b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureResponse.java new file mode 100644 index 0000000..f64b114 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureResponse.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.payment.application.dto; + +import com.zenfulcode.commercify.shared.domain.model.Money; + +public record CaptureResponse( + String transactionId, + Money capturedAmount +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java index 7b3f7cd..9926f9e 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java @@ -1,6 +1,8 @@ package com.zenfulcode.commercify.payment.application.service; +import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; +import com.zenfulcode.commercify.payment.application.dto.CaptureResponse; import com.zenfulcode.commercify.payment.application.dto.PaymentResponse; import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; @@ -62,4 +64,16 @@ public void handlePaymentCallback(PaymentProvider provider, WebhookPayload paylo PaymentProviderService providerService = providerFactory.getProvider(provider); providerService.handleCallback(payload); } + + @Transactional + public CaptureResponse capturePayment(CapturePaymentCommand command) { + Payment payment = paymentDomainService.getPaymentById(command.paymentId()); + + paymentDomainService.capturePayment(payment, command.transactionId(), command.captureAmount()); + + // Publish events + eventPublisher.publish(payment.getDomainEvents()); + + return new CaptureResponse(payment.getTransactionId(), payment.getAmount()); + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentNotFoundException.java b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentNotFoundException.java new file mode 100644 index 0000000..d42d679 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentNotFoundException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.payment.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.EntityNotFoundException; + +public class PaymentNotFoundException extends EntityNotFoundException { + public PaymentNotFoundException(Object entityId) { + super("Payment", entityId); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java index ca1991e..12512a9 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java @@ -65,7 +65,7 @@ public class Payment extends AggregateRoot { @ElementCollection @CollectionTable( name = "payment_attempts", - joinColumns = @JoinColumn(name = "payment_id") + joinColumns = @JoinColumn(name = "payment_id", referencedColumnName = "id") ) private List paymentAttempts = new ArrayList<>(); @@ -99,15 +99,6 @@ public static Payment create( payment.provider = provider; payment.status = PaymentStatus.PENDING; - // Register domain event - payment.registerEvent(new PaymentCreatedEvent( - payment.getId(), - payment.getOrder().getId(), - payment.getAmount(), - payment.getPaymentMethod(), - payment.getProvider() - )); - return payment; } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/repository/PaymentRepository.java b/src/main/java/com/zenfulcode/commercify/payment/domain/repository/PaymentRepository.java new file mode 100644 index 0000000..aa5b329 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/repository/PaymentRepository.java @@ -0,0 +1,23 @@ +package com.zenfulcode.commercify.payment.domain.repository; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.Optional; + +public interface PaymentRepository { + Payment save(Payment payment); + + Optional findById(PaymentId id); + + Optional findByProviderReference(String providerReference); + + Optional findByTransactionId(String transactionId); + + Optional findByOrderId(OrderId orderId); + + Page findAll(PageRequest pageRequest); +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java index 7810a64..9ad1d92 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java @@ -1,9 +1,13 @@ package com.zenfulcode.commercify.payment.domain.service; import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.payment.domain.event.PaymentCreatedEvent; +import com.zenfulcode.commercify.payment.domain.exception.PaymentNotFoundException; import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.repository.PaymentRepository; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; import com.zenfulcode.commercify.payment.domain.valueobject.refund.RefundRequest; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; @@ -17,6 +21,7 @@ public class PaymentDomainService { private final PaymentValidationService validationService; private final PaymentStateFlow paymentStateFlow; private final DomainEventPublisher eventPublisher; + private final PaymentRepository paymentRepository; /** * Creates a new payment for an order @@ -33,6 +38,14 @@ public Payment createPayment(Order order, PaymentMethod paymentMethod, PaymentPr payment.setOrder(order); + payment.registerEvent(new PaymentCreatedEvent( + payment.getId(), + payment.getOrder().getId(), + payment.getAmount(), + payment.getPaymentMethod(), + payment.getProvider() + )); + return payment; } @@ -88,7 +101,14 @@ public void cancelPayment(Payment payment) { payment.cancel(); - // Publish payment cancelled event eventPublisher.publish(payment.getDomainEvents()); } + + /** + * Get payment by ID + */ + public Payment getPaymentById(PaymentId paymentId) { + return paymentRepository.findById(paymentId) + .orElseThrow(() -> new PaymentNotFoundException(paymentId)); + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java index 4ac64f1..7cf7421 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java @@ -1,8 +1,8 @@ package com.zenfulcode.commercify.payment.domain.service; import com.zenfulcode.commercify.payment.domain.exception.InvalidPaymentStateException; -import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStateMetadata; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; import org.springframework.stereotype.Component; import java.util.EnumMap; @@ -25,13 +25,21 @@ public PaymentStateFlow() { private void initializeStateTransitions() { // Initial state -> PENDING validTransitions.put(PaymentStatus.PENDING, Set.of( + PaymentStatus.FAILED, + PaymentStatus.RESERVED, + PaymentStatus.CANCELLED + )); + + // RESERVED/PAID -> RESERVED or CANCELLED + validTransitions.put(PaymentStatus.RESERVED, Set.of( PaymentStatus.CAPTURED, + PaymentStatus.EXPIRED, PaymentStatus.FAILED, - PaymentStatus.CANCELLED, - PaymentStatus.EXPIRED + PaymentStatus.PARTIALLY_REFUNDED, + PaymentStatus.REFUNDED )); - // PAID -> REFUNDED or PARTIALLY_REFUNDED + // CAPTURED -> REFUNDED or PARTIALLY_REFUNDED validTransitions.put(PaymentStatus.CAPTURED, Set.of( PaymentStatus.REFUNDED, PaymentStatus.PARTIALLY_REFUNDED diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java index a1be716..ac448b6 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java @@ -53,8 +53,8 @@ public void validatePaymentCapture(Payment payment, Money captureAmount) { List violations = new ArrayList<>(); // Validate payment state - if (payment.getStatus() != PaymentStatus.PENDING) { - violations.add("Payment must be in PENDING state to be captured"); + if (payment.getStatus() != PaymentStatus.RESERVED) { + violations.add("Payment must be in RESERVED state to be captured"); } // Validate capture amount matches payment amount diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java index 685fb33..6eb6bab 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java @@ -2,6 +2,7 @@ public enum PaymentStatus { PENDING, // Payment has been initiated but not completed + RESERVED, // Payment has been reserved but not captured CAPTURED, // Payment has been successfully captured FAILED, // Payment attempt failed CANCELLED, // Payment was cancelled diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/JpaPaymentRepository.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/JpaPaymentRepository.java new file mode 100644 index 0000000..cfe9830 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/JpaPaymentRepository.java @@ -0,0 +1,48 @@ +package com.zenfulcode.commercify.payment.infrastructure.persistence; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.repository.PaymentRepository; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class JpaPaymentRepository implements PaymentRepository { + private final SpringDataJpaPaymentRepository repository; + + @Override + public Payment save(Payment payment) { + return repository.save(payment); + } + + @Override + public Optional findById(PaymentId id) { + return repository.findById(id); + } + + @Override + public Optional findByProviderReference(String providerReference) { + return repository.findPaymentByProviderReference(providerReference); + } + + @Override + public Optional findByTransactionId(String transactionId) { + return repository.findPaymentByTransactionId(transactionId); + } + + @Override + public Optional findByOrderId(OrderId orderId) { + return repository.findPaymentByOrder_Id(orderId); + } + + @Override + public Page findAll(PageRequest pageRequest) { + return repository.findAll(pageRequest); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/SpringDataJpaPaymentRepository.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/SpringDataJpaPaymentRepository.java new file mode 100644 index 0000000..78df66f --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/SpringDataJpaPaymentRepository.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.payment.infrastructure.persistence; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SpringDataJpaPaymentRepository extends JpaRepository { + Optional findPaymentByProviderReference(String providerReference); + + Optional findPaymentByTransactionId(String transactionId); + + Optional findPaymentByOrder_Id(OrderId orderId); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8706c74..f270f01 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ spring.application.name=commercify server.port=6091 # Database configuration -spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATABASE_NAME:commercifydb}?createDatabaseIfNotExist=true +spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATABASE_NAME:commercify_ddd_db}?createDatabaseIfNotExist=true spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index b3f3584..fc29453 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -4,4 +4,5 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/250121194314-adding-payments-changelog.sql b/src/main/resources/db/changelog/migrations/250121194314-adding-payments-changelog.sql new file mode 100644 index 0000000..1b40166 --- /dev/null +++ b/src/main/resources/db/changelog/migrations/250121194314-adding-payments-changelog.sql @@ -0,0 +1,40 @@ +-- liquibase formatted sql + +-- changeset gkhaavik:1737484993955-1 +CREATE TABLE payment_attempts +( + payment_id VARCHAR(255) NOT NULL, + attempt_time datetime NULL, + successful BIT(1) NULL, + details VARCHAR(255) NULL +); + +-- changeset gkhaavik:1737484993955-2 +CREATE TABLE payments +( + status VARCHAR(255) NOT NULL, + payment_method VARCHAR(255) NOT NULL, + payment_provider VARCHAR(255) NOT NULL, + provider_reference VARCHAR(255) NULL, + transaction_id VARCHAR(255) NULL, + error_message VARCHAR(255) NULL, + retry_count INT NULL, + max_retries INT NULL, + created_at datetime NULL, + updated_at datetime NULL, + completed_at datetime NULL, + id VARCHAR(255) NOT NULL, + amount DECIMAL NULL, + currency VARCHAR(255) NULL, + order_id VARCHAR(255) NULL, + CONSTRAINT pk_payments PRIMARY KEY (id) +); + +-- changeset gkhaavik:1737484993955-3 +ALTER TABLE payments + ADD CONSTRAINT FK_PAYMENTS_ON_ORDER FOREIGN KEY (order_id) REFERENCES orders (id); + +-- changeset gkhaavik:1737484993955-4 +ALTER TABLE payment_attempts + ADD CONSTRAINT fk_payment_attempts_on_payment FOREIGN KEY (payment_id) REFERENCES payments (id); + From 0e7a851e5c198a304d878fc82729b16e8d4d4cee Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 25 Jan 2025 19:58:25 +0100 Subject: [PATCH 25/57] cleanup for PaymentStateFlow --- .../domain/service/PaymentDomainService.java | 3 +- .../domain/service/PaymentStateFlow.java | 40 ++++--------------- .../service/PaymentValidationService.java | 2 +- .../valueobject/PaymentProviderConfig.java | 4 +- .../domain/valueobject/PaymentStatus.java | 28 ++++++++----- 5 files changed, 32 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java index 9ad1d92..e516c16 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java @@ -19,7 +19,6 @@ @RequiredArgsConstructor public class PaymentDomainService { private final PaymentValidationService validationService; - private final PaymentStateFlow paymentStateFlow; private final DomainEventPublisher eventPublisher; private final PaymentRepository paymentRepository; @@ -66,7 +65,7 @@ public void capturePayment(Payment payment, String transactionId, Money captured */ public void failPayment(Payment payment, String failureReason) { // Validate current state - paymentStateFlow.validateStateTransition(payment.getStatus(), PaymentStatus.FAILED); + validationService.validateStatusTransition(payment, PaymentStatus.FAILED); payment.markAsFailed(failureReason); diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java index 7cf7421..6836a7b 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java @@ -1,6 +1,5 @@ package com.zenfulcode.commercify.payment.domain.service; -import com.zenfulcode.commercify.payment.domain.exception.InvalidPaymentStateException; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStateMetadata; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; import org.springframework.stereotype.Component; @@ -11,11 +10,9 @@ @Component public class PaymentStateFlow { private final EnumMap> validTransitions; - private final EnumMap terminalStates; public PaymentStateFlow() { this.validTransitions = new EnumMap<>(PaymentStatus.class); - this.terminalStates = new EnumMap<>(PaymentStatus.class); initializeStateTransitions(); } @@ -60,29 +57,20 @@ private void initializeStateTransitions() { validTransitions.put(PaymentStatus.REFUNDED, Set.of()); validTransitions.put(PaymentStatus.CANCELLED, Set.of()); validTransitions.put(PaymentStatus.EXPIRED, Set.of()); - - // Mark terminal states - terminalStates.put(PaymentStatus.REFUNDED, true); - terminalStates.put(PaymentStatus.CANCELLED, true); - terminalStates.put(PaymentStatus.EXPIRED, true); - terminalStates.put(PaymentStatus.CAPTURED, false); - terminalStates.put(PaymentStatus.PENDING, false); - terminalStates.put(PaymentStatus.FAILED, false); - terminalStates.put(PaymentStatus.PARTIALLY_REFUNDED, false); } /** * Check if a state transition is valid */ public boolean canTransitionTo(PaymentStatus currentState, PaymentStatus targetState) { - Set allowedTransitions = validTransitions.get(currentState); - return allowedTransitions != null && allowedTransitions.contains(targetState); + final PaymentStateMetadata metadata = getStateMetadata(currentState); + return metadata.canTransitionTo(targetState); } /** * Get valid next states for a given state */ - public Set getValidTransitions(PaymentStatus currentState) { + private Set getValidTransitions(PaymentStatus currentState) { return validTransitions.getOrDefault(currentState, Set.of()); } @@ -90,21 +78,7 @@ public Set getValidTransitions(PaymentStatus currentState) { * Check if a state is terminal */ public boolean isTerminalState(PaymentStatus state) { - return terminalStates.getOrDefault(state, false); - } - - /** - * Validate state transition and throw exception if invalid - */ - public void validateStateTransition(PaymentStatus currentState, PaymentStatus targetState) { - if (!canTransitionTo(currentState, targetState)) { - throw new InvalidPaymentStateException( - null, // PaymentId would be set by the calling service - currentState, - targetState, - "Invalid payment status transition" - ); - } + return state.isTerminalState(); } /** @@ -116,9 +90,11 @@ public String getStateTransitionDiagram() { diagram.append("------------------------\n"); validTransitions.forEach((state, transitions) -> { - if (!transitions.isEmpty()) { + PaymentStateMetadata metadata = getStateMetadata(state); + + if (metadata.hasTransitions()) { diagram.append(state).append(" -> "); - diagram.append(String.join(" | ", transitions.stream() + diagram.append(String.join(" | ", metadata.validTransitions().stream() .map(PaymentStatus::name) .toList())); diagram.append("\n"); diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java index ac448b6..38086a7 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java @@ -94,7 +94,7 @@ public void validateRefundRequest(Payment payment, RefundRequest refundRequest) * Validates payment status transition */ public void validateStatusTransition(Payment payment, PaymentStatus newStatus) { - if (!stateFlow.canTransitionTo(payment.getStatus(), newStatus)) { + if (stateFlow.canTransitionTo(payment.getStatus(), newStatus)) { throw new InvalidPaymentStateException( payment.getId(), payment.getStatus(), diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderConfig.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderConfig.java index 4f4c324..90cec17 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderConfig.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderConfig.java @@ -1,7 +1,9 @@ package com.zenfulcode.commercify.payment.domain.valueobject; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; + import java.util.Map; -public record PaymentProviderConfig(String provider, boolean isActive, Map config) { +public record PaymentProviderConfig(PaymentProvider provider, boolean isActive, Map config) { } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java index 6eb6bab..4ed26bd 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java @@ -1,13 +1,23 @@ package com.zenfulcode.commercify.payment.domain.valueobject; +import lombok.Getter; + +@Getter public enum PaymentStatus { - PENDING, // Payment has been initiated but not completed - RESERVED, // Payment has been reserved but not captured - CAPTURED, // Payment has been successfully captured - FAILED, // Payment attempt failed - CANCELLED, // Payment was cancelled - REFUNDED, // Payment was fully refunded - PARTIALLY_REFUNDED, // Payment was partially refunded - EXPIRED, // Payment expired before completion - TERMINATED // Payment was terminated by the system + PENDING(false), // Payment has been initiated but not completed + RESERVED(false), // Payment has been reserved but not captured + CAPTURED(false), // Payment has been successfully captured + FAILED(false), // Payment attempt failed + CANCELLED(true), // Payment was cancelled + REFUNDED(true), // Payment was fully refunded + PARTIALLY_REFUNDED(false), // Payment was partially refunded + EXPIRED(true), // Payment expired before completion + TERMINATED(true); // Payment was terminated by the system + + private final boolean terminalState; + + PaymentStatus(boolean terminalState) { + this.terminalState = terminalState; + } } + From 426638aa3a83daced2786e81e24969b371b8e377 Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 27 Jan 2025 21:04:44 +0100 Subject: [PATCH 26/57] Adding mobilepay integration v0.1 and code clean Adding webhook config persistence Cleaning up mobilepay integration propeties Renaming PaymentResponse DTO to InitializedPayment --- .../commercify/CommercifyApplication.java | 7 + .../commercify/api/order/OrderController.java | 2 +- .../api/payment/PaymentAdminController.java | 40 +++ .../api/payment/PaymentController.java | 33 +++ .../api/payment/PaymentWebhookController.java | 73 ++++++ .../api/payment/mapper/PaymentDtoMapper.java | 110 ++++++++ .../request/InitiatePaymentRequest.java | 11 + .../request/PaymentDetailsRequest.java | 11 + .../api/payment/response/PaymentResponse.java | 10 + .../infrastructure/config/SecurityConfig.java | 2 +- .../service/OrderApplicationService.java | 12 +- .../application/dto/CaptureAmount.java | 10 + .../application/dto/CaptureResponse.java | 9 - .../application/dto/CapturedPayment.java | 8 + ...tResponse.java => InitializedPayment.java} | 2 +- .../service/MobilepayWebhookService.java | 52 ++++ .../service/PaymentApplicationService.java | 32 ++- .../exception/PaymentProcessingException.java | 18 ++ .../exception/WebhookValidationException.java | 12 + .../payment/domain/model/WebhookConfig.java | 44 ++++ .../repository/WebhookConfigRepository.java | 12 + .../service/MobilepayProviderService.java | 43 ---- .../service/PaymentProviderFactory.java | 12 + .../service/PaymentProviderService.java | 15 ++ .../provider/MobilepayProviderService.java | 119 +++++++++ .../valueobject/MobilepayPaymentRequest.java | 14 + .../valueobject/MobilepayWebhookResponse.java | 7 + .../valueobject/PaymentProviderRequest.java | 3 + .../domain/valueobject/WebhookRequest.java | 12 + .../webhook/MobilepayWebhookPayload.java | 41 +++ .../config/PaymentProviderConfig.java | 20 ++ .../MobilepayCreatePaymentRequest.java | 13 + .../gateway/MobilepayPaymentResponse.java | 7 + .../gateway/MobilepayTokenResponse.java | 20 ++ .../gateway/MobilepayTokenService.java | 107 ++++++++ .../gateway/client/MobilepayClient.java | 242 ++++++++++++++++++ .../gateway/config/MobilepayConfig.java | 21 ++ .../JpaWebhookConfigRepository.java | 25 ++ .../SpringDataJpaWebhookConfigRepository.java | 11 + .../webhook/WebhookHandler.java | 19 ++ .../resources/application-docker.properties | 17 +- src/main/resources/application.properties | 16 +- .../db/changelog/db.changelog-master.xml | 1 + .../migrations/250126103929-changelog.sql | 11 + 44 files changed, 1219 insertions(+), 87 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/PaymentController.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/response/PaymentResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureAmount.java delete mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/dto/CapturedPayment.java rename src/main/java/com/zenfulcode/commercify/payment/application/dto/{PaymentResponse.java => InitializedPayment.java} (88%) create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentProcessingException.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/exception/WebhookValidationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/model/WebhookConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/repository/WebhookConfigRepository.java delete mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/service/MobilepayProviderService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayPaymentRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/WebhookRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/MobilepayWebhookPayload.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/config/PaymentProviderConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayCreatePaymentRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayPaymentResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/JpaWebhookConfigRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/SpringDataJpaWebhookConfigRepository.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/infrastructure/webhook/WebhookHandler.java create mode 100644 src/main/resources/db/changelog/migrations/250126103929-changelog.sql diff --git a/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java b/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java index 6e73c3e..68b7527 100644 --- a/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java +++ b/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java @@ -2,11 +2,18 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; @SpringBootApplication public class CommercifyApplication { public static void main(String[] args) { SpringApplication.run(CommercifyApplication.class, args); } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } diff --git a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java index 854cfbf..6c40b9e 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java @@ -49,7 +49,7 @@ public ResponseEntity> createOrder( @GetMapping("/{orderId}") public ResponseEntity> getOrder( @PathVariable String orderId) { - OrderDetailsDTO order = orderApplicationService.getOrderById(OrderId.of(orderId)); + OrderDetailsDTO order = orderApplicationService.getOrderDetailsById(OrderId.of(orderId)); OrderDetailsResponse response = orderDtoMapper.toResponse(order); return ResponseEntity.ok(ApiResponse.success(response)); } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java new file mode 100644 index 0000000..8516c64 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java @@ -0,0 +1,40 @@ +package com.zenfulcode.commercify.api.payment; + +import com.zenfulcode.commercify.api.payment.mapper.PaymentDtoMapper; +import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; +import com.zenfulcode.commercify.payment.application.dto.CapturedPayment; +import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v2/payments/admin") +@PreAuthorize("hasRole('ADMIN')") +@RequiredArgsConstructor +public class PaymentAdminController { + private final PaymentApplicationService paymentService; + private final PaymentDtoMapper paymentDtoMapper; + + @PostMapping("/{paymentId}/capture") + public ResponseEntity> capturePayment( + @PathVariable String paymentId, + @RequestBody String transactionId) { + + CapturePaymentCommand command = paymentDtoMapper.toCaptureCommand(PaymentId.of(paymentId), transactionId); + CapturedPayment response = paymentService.capturePayment(command); + + return ResponseEntity.ok(ApiResponse.success(response)); + } + + @PostMapping("/{orderId}/refund") + public ResponseEntity> refundPayment( + @PathVariable String orderId) { + +// paymentService.refundPayment(OrderId.of(orderId)); + return ResponseEntity.ok(ApiResponse.success("Refund initiated")); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentController.java new file mode 100644 index 0000000..01aa75c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentController.java @@ -0,0 +1,33 @@ +package com.zenfulcode.commercify.api.payment; + +import com.zenfulcode.commercify.api.payment.mapper.PaymentDtoMapper; +import com.zenfulcode.commercify.api.payment.request.InitiatePaymentRequest; +import com.zenfulcode.commercify.api.payment.response.PaymentResponse; +import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; +import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; +import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; +import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v2/payments") +@RequiredArgsConstructor +public class PaymentController { + private final PaymentApplicationService paymentService; + private final PaymentDtoMapper paymentDtoMapper; + + @PostMapping("/initiate") + public ResponseEntity> initiatePayment( + @RequestBody InitiatePaymentRequest request) { + + InitiatePaymentCommand command = paymentDtoMapper.toCommand(request); + InitializedPayment response = paymentService.initiatePayment(command); + + return ResponseEntity.ok(ApiResponse.success(paymentDtoMapper.toResponse(response))); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java new file mode 100644 index 0000000..a0bc6e8 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java @@ -0,0 +1,73 @@ +package com.zenfulcode.commercify.api.payment; + +import com.zenfulcode.commercify.payment.application.service.MobilepayWebhookService; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.valueobject.WebhookRequest; +import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/v2/payments/webhooks") +@RequiredArgsConstructor +@Slf4j +public class PaymentWebhookController { + private final MobilepayWebhookService webhookService; + + @PostMapping("/{provider}/callback") + public ResponseEntity> handleCallback( + @PathVariable String provider, + @RequestBody String body, + HttpServletRequest request + ) { + try { + PaymentProvider paymentProvider = PaymentProvider.valueOf(provider.toUpperCase()); + + WebhookRequest webhookRequest = WebhookRequest.builder() + .body(body) + .headers(extractHeaders(request)) + .build(); + + webhookService.handleWebhook(paymentProvider, webhookRequest); + + return ResponseEntity.ok(ApiResponse.success("Webhook processed successfully")); + } catch (Exception e) { + log.error("Error processing {} webhook: {}", provider, e.getMessage()); + return ResponseEntity.badRequest().body( + ApiResponse.error("Error processing webhook", "WEBHOOK_ERROR", 400) + ); + } + } + + @PreAuthorize("hasRole('ADMIN')") + @PostMapping("/{provider}/register") + public ResponseEntity> registerWebhook( + @PathVariable String provider, + @RequestBody String callbackUrl + ) { + try { + PaymentProvider paymentProvider = PaymentProvider.valueOf(provider.toUpperCase()); + + webhookService.registerWebhook(paymentProvider, callbackUrl); + return ResponseEntity.ok(ApiResponse.success("Webhook registered successfully")); + } catch (Exception e) { + log.error("Error registering {} webhook: {}", provider, e.getMessage()); + return ResponseEntity.badRequest().body( + ApiResponse.error("Error registering webhook", "WEBHOOK_ERROR", 400) + ); + } + } + + private Map extractHeaders(HttpServletRequest request) { + Map headers = new HashMap<>(); + request.getHeaderNames().asIterator().forEachRemaining(name -> headers.put(name, request.getHeader(name))); + return headers; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java new file mode 100644 index 0000000..564c185 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java @@ -0,0 +1,110 @@ +package com.zenfulcode.commercify.api.payment.mapper; + +import com.zenfulcode.commercify.api.payment.request.InitiatePaymentRequest; +import com.zenfulcode.commercify.api.payment.request.PaymentDetailsRequest; +import com.zenfulcode.commercify.api.payment.response.PaymentResponse; +import com.zenfulcode.commercify.order.application.service.OrderApplicationService; +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; +import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; +import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class PaymentDtoMapper { + private final OrderApplicationService orderService; + + public InitiatePaymentCommand toCommand(InitiatePaymentRequest request) { + Order order = orderService.getOrderById(request.orderId()); + + return new InitiatePaymentCommand( + order, + PaymentMethod.valueOf(request.paymentMethod()), + PaymentProvider.valueOf(request.provider()), + toProviderRequest(request.paymentDetails()) + ); + } + + public PaymentResponse toResponse(InitializedPayment response) { + return new PaymentResponse( + response.paymentId().toString(), + response.redirectUrl(), + response.additionalData() + ); + } + + public WebhookPayload toWebhookPayload(String payload, String signature) { + return new WebhookPayload() { + @Override + public String getEventType() { + return "payment.callback"; + } + + @Override + public String getPaymentReference() { + return null; + } + + @Override + public Instant getTimestamp() { + return Instant.now(); + } + + @Override + public boolean isValid() { + return true; + } + + public String getPayload() { + return payload; + } + + public String getSignature() { + return signature; + } + }; + } + + public CapturePaymentCommand toCaptureCommand(PaymentId paymentId, String transactionId) { + return new CapturePaymentCommand( + paymentId, + transactionId, + null + ); + } + + private PaymentProviderRequest toProviderRequest(PaymentDetailsRequest details) { + return new PaymentProviderRequest() { + @Override + public PaymentMethod getPaymentMethod() { + return PaymentMethod.valueOf(details.paymentMethodId()); + } + + public String getPaymentMethodId() { + return details.paymentMethodId(); + } + + public String getReturnUrl() { + return details.returnUrl(); + } + + public String getCancelUrl() { + return details.cancelUrl(); + } + + public Map getAdditionalData() { + return details.additionalData(); + } + }; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java b/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java new file mode 100644 index 0000000..068e6ff --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.api.payment.request; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; + +public record InitiatePaymentRequest( + OrderId orderId, + String paymentMethod, + String provider, + PaymentDetailsRequest paymentDetails +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java b/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java new file mode 100644 index 0000000..2fc0a35 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.api.payment.request; + +import java.util.Map; + +public record PaymentDetailsRequest( + String paymentMethodId, + String returnUrl, + String cancelUrl, + Map additionalData +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/response/PaymentResponse.java b/src/main/java/com/zenfulcode/commercify/api/payment/response/PaymentResponse.java new file mode 100644 index 0000000..2a24e82 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/response/PaymentResponse.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.api.payment.response; + +import java.util.Map; + +public record PaymentResponse( + String paymentId, + String redirectUrl, + Map additionalData +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index b56f1fb..563b4e7 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -42,7 +42,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, "/api/v2/auth/**", "/api/v2/products/active", "/api/v2/products/{id}", - "/api/v2/payments/mobilepay/callback").permitAll() + "/api/v2/payments/webhooks/mobilepay/callback").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session diff --git a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java index e67e1b7..8e3ecb3 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java @@ -107,13 +107,17 @@ public Page findAllOrders(FindAllOrdersQuery query) { } @Transactional(readOnly = true) - public OrderDetailsDTO getOrderById(OrderId orderId) { - Order order = orderRepository.findById(orderId) - .orElseThrow(() -> new OrderNotFoundException(orderId)); - + public OrderDetailsDTO getOrderDetailsById(OrderId orderId) { + Order order = getOrderById(orderId); return OrderDetailsDTO.fromOrder(order); } + @Transactional(readOnly = true) + public Order getOrderById(OrderId orderId) { + return orderRepository.findById(orderId) + .orElseThrow(() -> new OrderNotFoundException(orderId)); + } + @Transactional(readOnly = true) public boolean isOrderOwnedByUser(OrderId orderId, UserId userId) { return orderRepository.existsByIdAndUserId(orderId, userId); diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureAmount.java b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureAmount.java new file mode 100644 index 0000000..2a49674 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureAmount.java @@ -0,0 +1,10 @@ +package com.zenfulcode.commercify.payment.application.dto; + +import java.math.BigDecimal; + +public record CaptureAmount( + BigDecimal capturedAmount, + BigDecimal remainingAmount, + String currency +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureResponse.java b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureResponse.java deleted file mode 100644 index f64b114..0000000 --- a/src/main/java/com/zenfulcode/commercify/payment/application/dto/CaptureResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.zenfulcode.commercify.payment.application.dto; - -import com.zenfulcode.commercify.shared.domain.model.Money; - -public record CaptureResponse( - String transactionId, - Money capturedAmount -) { -} diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/dto/CapturedPayment.java b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CapturedPayment.java new file mode 100644 index 0000000..ed0dbe2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CapturedPayment.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.payment.application.dto; + +public record CapturedPayment( + String transactionId, + CaptureAmount captureAmount, + boolean isFullyCaptured +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/dto/PaymentResponse.java b/src/main/java/com/zenfulcode/commercify/payment/application/dto/InitializedPayment.java similarity index 88% rename from src/main/java/com/zenfulcode/commercify/payment/application/dto/PaymentResponse.java rename to src/main/java/com/zenfulcode/commercify/payment/application/dto/InitializedPayment.java index 903725e..5805cdd 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/dto/PaymentResponse.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/dto/InitializedPayment.java @@ -4,7 +4,7 @@ import java.util.Map; -public record PaymentResponse( +public record InitializedPayment( PaymentId paymentId, String redirectUrl, Map additionalData diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java new file mode 100644 index 0000000..0de5d73 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java @@ -0,0 +1,52 @@ +package com.zenfulcode.commercify.payment.application.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; +import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; +import com.zenfulcode.commercify.payment.domain.service.provider.MobilepayProviderService; +import com.zenfulcode.commercify.payment.domain.valueobject.WebhookRequest; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.MobilepayWebhookPayload; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MobilepayWebhookService { + private final PaymentProviderFactory providerFactory; + private final ObjectMapper objectMapper; + + @Transactional + public void handleWebhook(PaymentProvider provider, WebhookRequest request) { + PaymentProviderService service = providerFactory.getProvider(provider); + + // TODO: Refactor this, it's bad practice + if (service instanceof MobilepayProviderService mobilePayService) { + String contentSha256 = request.headers().get("Content-SHA256"); + String authorization = request.headers().get("Authorization"); + String date = request.headers().get("Date"); + + mobilePayService.authenticateWebhook(date, contentSha256, authorization, request.body()); + } + + MobilepayWebhookPayload payload = objectMapper.convertValue(request.body(), MobilepayWebhookPayload.class); + + service.handleCallback(payload); + } + + @Transactional + public void registerWebhook(PaymentProvider provider, String callbackUrl) { + providerFactory.getProvider(provider).registerWebhook(callbackUrl); + } + + @Transactional + public void deleteWebhook(PaymentProvider provider, String webhookId) { + providerFactory.getProvider(provider).deleteWebhook(webhookId); + } + + @Transactional(readOnly = true) + public Object getWebhooks(PaymentProvider provider) { + return providerFactory.getProvider(provider).getWebhooks(); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java index 9926f9e..c3476f2 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java @@ -2,29 +2,36 @@ import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; -import com.zenfulcode.commercify.payment.application.dto.CaptureResponse; -import com.zenfulcode.commercify.payment.application.dto.PaymentResponse; +import com.zenfulcode.commercify.payment.application.dto.CaptureAmount; +import com.zenfulcode.commercify.payment.application.dto.CapturedPayment; +import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; import com.zenfulcode.commercify.payment.domain.service.PaymentDomainService; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderResponse; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; +import com.zenfulcode.commercify.payment.infrastructure.webhook.WebhookHandler; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.shared.domain.model.Money; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; + @Service @RequiredArgsConstructor public class PaymentApplicationService { private final PaymentDomainService paymentDomainService; private final PaymentProviderFactory providerFactory; private final DomainEventPublisher eventPublisher; + private final WebhookHandler webhookHandler; @Transactional - public PaymentResponse initiatePayment(InitiatePaymentCommand command) { + public InitializedPayment initiatePayment(InitiatePaymentCommand command) { // Get the appropriate provider service PaymentProviderService providerService = providerFactory.getProvider(command.provider()); @@ -52,7 +59,7 @@ public PaymentResponse initiatePayment(InitiatePaymentCommand command) { eventPublisher.publish(payment.getDomainEvents()); // Return response - return new PaymentResponse( + return new InitializedPayment( payment.getId(), providerResponse.redirectUrl(), providerResponse.additionalData() @@ -61,19 +68,26 @@ public PaymentResponse initiatePayment(InitiatePaymentCommand command) { @Transactional public void handlePaymentCallback(PaymentProvider provider, WebhookPayload payload) { - PaymentProviderService providerService = providerFactory.getProvider(provider); - providerService.handleCallback(payload); + webhookHandler.handleWebhook(provider, payload); } + // TODO: Make sure the capture currency is the same as the payment currency @Transactional - public CaptureResponse capturePayment(CapturePaymentCommand command) { + public CapturedPayment capturePayment(CapturePaymentCommand command) { Payment payment = paymentDomainService.getPaymentById(command.paymentId()); - paymentDomainService.capturePayment(payment, command.transactionId(), command.captureAmount()); + Money captureAmount = command.captureAmount() == null ? payment.getAmount() : command.captureAmount(); + + paymentDomainService.capturePayment(payment, command.transactionId(), captureAmount); // Publish events eventPublisher.publish(payment.getDomainEvents()); - return new CaptureResponse(payment.getTransactionId(), payment.getAmount()); + // This is going to be used for partial captures, which is not implemented yet + BigDecimal remainingAmount = payment.getAmount().subtract(captureAmount).getAmount(); + CaptureAmount captureAmountDto = new CaptureAmount(captureAmount.getAmount(), remainingAmount, captureAmount.getCurrency()); + boolean isFullyCaptured = payment.getStatus() == PaymentStatus.CAPTURED; + + return new CapturedPayment(payment.getTransactionId(), captureAmountDto, isFullyCaptured); } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentProcessingException.java b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentProcessingException.java new file mode 100644 index 0000000..b4d21ab --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/PaymentProcessingException.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.payment.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import lombok.Getter; + +@Getter +public class PaymentProcessingException extends DomainException { + private final String providerReference; + + public PaymentProcessingException(String message, Throwable cause) { + this(message, null, cause); + } + + public PaymentProcessingException(String message, String providerReference, Throwable cause) { + super(message, cause); + this.providerReference = providerReference; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/exception/WebhookValidationException.java b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/WebhookValidationException.java new file mode 100644 index 0000000..d3a06ac --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/WebhookValidationException.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.payment.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainValidationException; + +import java.util.List; + +public class WebhookValidationException extends DomainValidationException { + + public WebhookValidationException(String message, List violations) { + super(message, violations); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/WebhookConfig.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/WebhookConfig.java new file mode 100644 index 0000000..2356afc --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/WebhookConfig.java @@ -0,0 +1,44 @@ +package com.zenfulcode.commercify.payment.domain.model; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.proxy.HibernateProxy; + +import java.util.Objects; + +@Entity +@Table(name = "webhook_config") +@Builder +@Getter +@Setter +@ToString +@RequiredArgsConstructor +@AllArgsConstructor +public class WebhookConfig { + @Id + @Enumerated(EnumType.STRING) + @Column(name = "provider", nullable = false) + private PaymentProvider provider; + + @Column(name = "callback_url", nullable = false) + private String callbackUrl; + + @Column(name = "secret", nullable = false) + private String secret; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + WebhookConfig that = (WebhookConfig) o; + return getProvider() != null && Objects.equals(getProvider(), that.getProvider()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/repository/WebhookConfigRepository.java b/src/main/java/com/zenfulcode/commercify/payment/domain/repository/WebhookConfigRepository.java new file mode 100644 index 0000000..32facb3 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/repository/WebhookConfigRepository.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.payment.domain.repository; + +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.model.WebhookConfig; + +import java.util.Optional; + +public interface WebhookConfigRepository { + WebhookConfig save(WebhookConfig config); + + Optional findByProvider(PaymentProvider provider); +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/MobilepayProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/MobilepayProviderService.java deleted file mode 100644 index 5858f3e..0000000 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/MobilepayProviderService.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.zenfulcode.commercify.payment.domain.service; - -import com.zenfulcode.commercify.order.domain.valueobject.OrderId; -import com.zenfulcode.commercify.payment.domain.model.Payment; -import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; -import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderConfig; -import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; -import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderResponse; -import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; - -import java.util.Set; - -public class MobilepayProviderService implements PaymentProviderService { - @Override - public PaymentProviderResponse initiatePayment(Payment payment, OrderId orderId, PaymentProviderRequest request) { - return null; - } - - @Override - public void handleCallback(WebhookPayload payload) { - - } - - @Override - public Set getSupportedPaymentMethods() { - return Set.of(PaymentMethod.WALLET); - } - - @Override - public PaymentProviderConfig getProviderConfig() { - return null; - } - - @Override - public void validateRequest(PaymentProviderRequest request) { - - } - - @Override - public boolean supportsPaymentMethod(PaymentMethod method) { - return false; - } -} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderFactory.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderFactory.java index 6f3be44..b341160 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderFactory.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderFactory.java @@ -30,4 +30,16 @@ public List getAvailableProviders() { .filter(PaymentProviderConfig::isActive) .collect(Collectors.toList()); } + + public void registerProvider(PaymentProvider provider, PaymentProviderService service) { + providerServices.put(provider, service); + } + + public void unregisterProvider(PaymentProvider provider) { + providerServices.remove(provider); + } + + public boolean supportsProvider(PaymentProvider provider) { + return providerServices.containsKey(provider); + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java index caf57ee..2f2984a 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java @@ -43,4 +43,19 @@ public interface PaymentProviderService { * Check if provider supports payment method */ boolean supportsPaymentMethod(PaymentMethod method); + + /** + * Register a webhook with the provider + */ + void registerWebhook(String callbackUrl); + + /** + * Delete a webhook with the provider + */ + void deleteWebhook(String webhookId); + + /** + * Get all webhooks registered with the provider + */ + Object getWebhooks(); } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java new file mode 100644 index 0000000..e623a0c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java @@ -0,0 +1,119 @@ +package com.zenfulcode.commercify.payment.domain.service.provider; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.exception.PaymentValidationException; +import com.zenfulcode.commercify.payment.domain.model.Payment; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; +import com.zenfulcode.commercify.payment.domain.valueobject.MobilepayPaymentRequest; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderConfig; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderResponse; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.MobilepayWebhookPayload; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; +import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayCreatePaymentRequest; +import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayPaymentResponse; +import com.zenfulcode.commercify.payment.infrastructure.gateway.client.MobilepayClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MobilepayProviderService implements PaymentProviderService { + private final MobilepayClient mobilePayClient; + + @Override + public PaymentProviderResponse initiatePayment(Payment payment, OrderId orderId, PaymentProviderRequest request) { + // Convert to MobilePay specific request + MobilepayPaymentRequest mobilePayRequest = (MobilepayPaymentRequest) request; + + // Create payment through MobilePay client + MobilepayPaymentResponse response = mobilePayClient.createPayment( + new MobilepayCreatePaymentRequest( + payment.getAmount(), + mobilePayRequest.getPaymentMethod(), + mobilePayRequest.phoneNumber(), + mobilePayRequest.returnUrl(), + orderId.toString() + ) + ); + + // Return provider response + return new PaymentProviderResponse( + response.reference(), + response.redirectUrl(), + Map.of("providerReference", response.reference()) + ); + } + + @Override + public void handleCallback(WebhookPayload payload) { + MobilepayWebhookPayload webhookPayload = (MobilepayWebhookPayload) payload; + + // Handle the webhook + if (webhookPayload.isValid()) { + log.info("MobilePay webhook received: {}", webhookPayload); + } + } + + @Override + public Set getSupportedPaymentMethods() { + return Set.of(PaymentMethod.WALLET); + } + + @Override + public PaymentProviderConfig getProviderConfig() { + return new PaymentProviderConfig( + PaymentProvider.MOBILEPAY, + true, + new HashMap<>() + ); + } + + @Override + public void validateRequest(PaymentProviderRequest request) { + MobilepayPaymentRequest mobilePayRequest = (MobilepayPaymentRequest) request; + List violations = new ArrayList<>(); + + // TODO Make sure the phone number is valid (DK, FI, SE or NO) + if (mobilePayRequest.phoneNumber() == null || mobilePayRequest.phoneNumber().isBlank()) { + violations.add("Phone number is required"); + } + + if (mobilePayRequest.returnUrl() == null || mobilePayRequest.returnUrl().isBlank()) { + violations.add("Return URL is required"); + } + + if (!violations.isEmpty()) { + throw new PaymentValidationException("Mobilepay validation: ", violations); + } + } + + @Override + public boolean supportsPaymentMethod(PaymentMethod method) { + return getSupportedPaymentMethods().contains(method); + } + + public void registerWebhook(String callbackUrl) { + mobilePayClient.registerWebhook(callbackUrl); + } + + @Override + public void deleteWebhook(String webhookId) { + mobilePayClient.deleteWebhook(webhookId); + } + + @Override + public Object getWebhooks() { + return mobilePayClient.getWebhooks(); + } + + public void authenticateWebhook(String date, String contentSha256, String authorization, String payload) { + mobilePayClient.validateWebhook(date, contentSha256, authorization, payload); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayPaymentRequest.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayPaymentRequest.java new file mode 100644 index 0000000..0355519 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayPaymentRequest.java @@ -0,0 +1,14 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; + +public record MobilepayPaymentRequest( + PaymentMethod paymentMethod, + String phoneNumber, + String returnUrl +) implements PaymentProviderRequest { + @Override + public PaymentMethod getPaymentMethod() { + return paymentMethod; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookResponse.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookResponse.java new file mode 100644 index 0000000..4594acf --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookResponse.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +public record MobilepayWebhookResponse( + String secret, + String id +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderRequest.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderRequest.java index dab0a1b..8422d6d 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderRequest.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentProviderRequest.java @@ -1,5 +1,8 @@ package com.zenfulcode.commercify.payment.domain.valueobject; +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; + public interface PaymentProviderRequest { + PaymentMethod getPaymentMethod(); } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/WebhookRequest.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/WebhookRequest.java new file mode 100644 index 0000000..66aa47c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/WebhookRequest.java @@ -0,0 +1,12 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +import lombok.Builder; + +import java.util.Map; + +@Builder +public record WebhookRequest( + String body, + Map headers +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/MobilepayWebhookPayload.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/MobilepayWebhookPayload.java new file mode 100644 index 0000000..36f4361 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/MobilepayWebhookPayload.java @@ -0,0 +1,41 @@ +package com.zenfulcode.commercify.payment.domain.valueobject.webhook; + +import java.time.Instant; + +public record MobilepayWebhookPayload( + String msn, + String reference, + String pspReference, + String name, + MobilepayAmount amount, + Instant timestamp, + String idempotencyKey, + boolean success +) implements WebhookPayload { + + @Override + public String getEventType() { + return name; + } + + @Override + public String getPaymentReference() { + return reference; + } + + @Override + public Instant getTimestamp() { + return timestamp; + } + + @Override + public boolean isValid() { + return reference != null && name != null; + } + + record MobilepayAmount( + String currency, + long value + ) { + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/config/PaymentProviderConfig.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/config/PaymentProviderConfig.java new file mode 100644 index 0000000..dc83fa6 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/config/PaymentProviderConfig.java @@ -0,0 +1,20 @@ +package com.zenfulcode.commercify.payment.infrastructure.config; + +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; +import com.zenfulcode.commercify.payment.domain.service.provider.MobilepayProviderService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class PaymentProviderConfig { + private final PaymentProviderFactory providerFactory; + private final MobilepayProviderService mobilePayService; + + @PostConstruct + public void registerProviders() { + providerFactory.registerProvider(PaymentProvider.MOBILEPAY, mobilePayService); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayCreatePaymentRequest.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayCreatePaymentRequest.java new file mode 100644 index 0000000..402744b --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayCreatePaymentRequest.java @@ -0,0 +1,13 @@ +package com.zenfulcode.commercify.payment.infrastructure.gateway; + +import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; +import com.zenfulcode.commercify.shared.domain.model.Money; + +public record MobilepayCreatePaymentRequest( + Money amount, + PaymentMethod paymentMethod, + String phoneNumber, + String returnUrl, + String orderId +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayPaymentResponse.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayPaymentResponse.java new file mode 100644 index 0000000..47a9c45 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayPaymentResponse.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.payment.infrastructure.gateway; + +public record MobilepayPaymentResponse( + String redirectUrl, + String reference +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenResponse.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenResponse.java new file mode 100644 index 0000000..8a0d086 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenResponse.java @@ -0,0 +1,20 @@ +package com.zenfulcode.commercify.payment.infrastructure.gateway; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record MobilepayTokenResponse( + @JsonProperty("token_type") + String tokenType, + + @JsonProperty("expires_in") + String expiresIn, + + @JsonProperty("access_token") + String accessToken, + + @JsonProperty("expires_on") + String expiresOn, + + @JsonProperty("resource") + String resource) { +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java new file mode 100644 index 0000000..6d1d835 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java @@ -0,0 +1,107 @@ +package com.zenfulcode.commercify.payment.infrastructure.gateway; + +import com.zenfulcode.commercify.payment.domain.exception.PaymentProcessingException; +import com.zenfulcode.commercify.payment.infrastructure.gateway.config.MobilepayConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.Instant; +import java.util.concurrent.locks.ReentrantLock; + +@Service +@Slf4j +public class MobilepayTokenService { + private final RestTemplate restTemplate; + private final ReentrantLock tokenLock = new ReentrantLock(); + private final MobilepayConfig config; + + private String accessToken; + private Instant tokenExpiration; + + public MobilepayTokenService(RestTemplate restTemplate, MobilepayConfig config) { + this.restTemplate = restTemplate; + this.config = config; + } + + public String getAccessToken() { + if (shouldRefreshToken()) { + refreshAccessToken(); + } + return accessToken; + } + + private boolean shouldRefreshToken() { + return accessToken == null || tokenExpiration == null || + Instant.now().plusSeconds(60).isAfter(tokenExpiration); + } + + private void refreshAccessToken() { + tokenLock.lock(); + try { + // Double-check after acquiring lock + if (shouldRefreshToken()) { + MobilepayTokenResponse tokenResponse = requestNewAccessToken(); + accessToken = tokenResponse.accessToken(); + + // Parse expires_on timestamp + try { + long expiresOn = Long.parseLong(tokenResponse.expiresOn()); + tokenExpiration = Instant.ofEpochSecond(expiresOn); + log.info("Access token refreshed, expires at: {}", tokenExpiration); + } catch (NumberFormatException e) { + log.error("Failed to parse token expiration timestamp", e); + // Fallback expiration + tokenExpiration = Instant.now().plusSeconds(3600); + } + } + } finally { + tokenLock.unlock(); + } + } + + private MobilepayTokenResponse requestNewAccessToken() { + try { + HttpHeaders headers = createTokenRequestHeaders(); + + ResponseEntity response = restTemplate.exchange( + config.getApiUrl() + "/accesstoken/get", + HttpMethod.POST, + new HttpEntity<>(headers), + MobilepayTokenResponse.class + ); + + if (response.getBody() == null) { + throw new PaymentProcessingException("No response from MobilePay token API", null); + } + + return response.getBody(); + } catch (Exception e) { + log.error("Failed to obtain access token", e); + throw new PaymentProcessingException("Failed to obtain access token", e); + } + } + + private HttpHeaders createTokenRequestHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("client_id", config.getClientId()); + headers.set("client_secret", config.getClientSecret()); + headers.set("Ocp-Apim-Subscription-Key", config.getSubscriptionKey()); + headers.set("Merchant-Serial-Number", config.getMerchantId()); + headers.set("Vipps-System-Name", config.getSystemName()); + headers.set("Vipps-System-Version", "1.0"); + headers.set("Vipps-System-Plugin-Name", "commercify"); + headers.set("Vipps-System-Plugin-Version", "1.0"); + return headers; + } + + @Scheduled(fixedRate = 3600000) // Every hour + public void scheduleTokenRefresh() { + if (shouldRefreshToken()) { + refreshAccessToken(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java new file mode 100644 index 0000000..2e397cf --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java @@ -0,0 +1,242 @@ +package com.zenfulcode.commercify.payment.infrastructure.gateway.client; + +import com.zenfulcode.commercify.payment.domain.exception.PaymentProcessingException; +import com.zenfulcode.commercify.payment.domain.exception.WebhookValidationException; +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.model.WebhookConfig; +import com.zenfulcode.commercify.payment.domain.repository.WebhookConfigRepository; +import com.zenfulcode.commercify.payment.domain.valueobject.MobilepayWebhookResponse; +import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayCreatePaymentRequest; +import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayPaymentResponse; +import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayTokenService; +import com.zenfulcode.commercify.payment.infrastructure.gateway.config.MobilepayConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@Slf4j +public class MobilepayClient { + private final WebhookConfigRepository webhookRepository; + private final RestTemplate restTemplate; + private final MobilepayTokenService tokenService; + + private final MobilepayConfig config; + + public MobilepayPaymentResponse createPayment(MobilepayCreatePaymentRequest request) { + try { + HttpEntity> entity = new HttpEntity<>( + createPaymentRequest(request), + createHeaders() + ); + + ResponseEntity response = restTemplate.exchange( + config.getApiUrl() + "/epayment/v1/payments", + HttpMethod.POST, + entity, + MobilepayPaymentResponse.class + ); + + if (response.getBody() == null) { + throw new PaymentProcessingException("Empty response from MobilePay", null); + } + + return response.getBody(); + } catch (Exception e) { + throw new PaymentProcessingException("Failed to create MobilePay payment", e); + } + } + + public void validateWebhook(String contentSha256, String authorization, String date, String payload) { + try { +// Verify content + log.info("Verifying content"); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(payload.getBytes(StandardCharsets.UTF_8)); + String encodedHash = Base64.getEncoder().encodeToString(hash); + + if (!encodedHash.equals(contentSha256)) { + throw new SecurityException("Hash mismatch"); + } + + log.info("Content verified"); + +// Verify signature + log.info("Verifying signature"); + URI uri = new URI(config.getHost()); + + String expectedSignedString = String.format("POST\n%s\n%s;%s;%s", uri.getPath(), date, uri.getHost(), encodedHash); + + Mac hmacSha256 = Mac.getInstance("HmacSHA256"); + + byte[] secretByteArray = tokenService.getAccessToken().getBytes(StandardCharsets.UTF_8); + + SecretKeySpec secretKey = new SecretKeySpec(secretByteArray, "HmacSHA256"); + hmacSha256.init(secretKey); + + byte[] hmacSha256Bytes = hmacSha256.doFinal(expectedSignedString.getBytes(StandardCharsets.UTF_8)); + String expectedSignature = Base64.getEncoder().encodeToString(hmacSha256Bytes); + String expectedAuthorization = String.format("HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=%s", expectedSignature); + + if (!authorization.equals(expectedAuthorization)) { + throw new SecurityException("Signature mismatch"); + } + + log.info("Signature verified"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 algorithm not found", e); + } catch (InvalidKeyException | URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + tokenService.getAccessToken()); + headers.set("Ocp-Apim-Subscription-Key", config.getSubscriptionKey()); + headers.set("Merchant-Serial-Number", config.getMerchantId()); + headers.set("Idempotency-Key", UUID.randomUUID().toString()); + return headers; + } + + private Map createPaymentRequest(MobilepayCreatePaymentRequest request) { + Map paymentRequest = new HashMap<>(); + + // Amount + Map amount = new HashMap<>(); + amount.put("value", Math.round(request.amount().getAmount().doubleValue() * 100)); + amount.put("currency", request.amount().getCurrency()); + paymentRequest.put("amount", amount); + + // Payment method + Map paymentMethod = new HashMap<>(); + paymentMethod.put("type", request.paymentMethod().name()); + paymentRequest.put("paymentMethod", paymentMethod); + + // Customer + Map customer = new HashMap<>(); + customer.put("phoneNumber", request.phoneNumber()); + paymentRequest.put("customer", customer); + + // Other fields + String reference = String.join("-", config.getMerchantId(), config.getSystemName(), request.orderId()); + paymentRequest.put("reference", reference); + paymentRequest.put("returnUrl", request.returnUrl()); + paymentRequest.put("userFlow", "WEB_REDIRECT"); + + return paymentRequest; + } + + public void registerWebhook(String callbackUrl) { + HttpHeaders headers = createHeaders(); + + Map request = new HashMap<>(); + request.put("url", callbackUrl); + request.put("events", new String[]{ + "epayments.payment.aborted.v1", + "epayments.payment.expired.v1", + "epayments.payment.cancelled.v1", + "epayments.payment.captured.v1", + "epayments.payment.refunded.v1", + "epayments.payment.authorized.v1" + }); + + HttpEntity> entity = new HttpEntity<>(request, headers); + + try { + ResponseEntity response = restTemplate.exchange( + String.format("%s/webhooks/v1/webhooks", config.getApiUrl()), + HttpMethod.POST, + entity, + MobilepayWebhookResponse.class + ); + + if (response.getBody() == null) { + throw new WebhookValidationException("Empty response from MobilePay", null); + } + + // Save or update webhook configuration + saveOrUpdateWebhook(callbackUrl, response.getBody().secret()); + + log.info("Webhook registration response: {}", response.getBody()); + } catch (Exception e) { + throw new PaymentProcessingException("Failed to register webhook", e); + } + } + + private void saveOrUpdateWebhook(String callbackUrl, String secret) { + webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) + .ifPresentOrElse( + config -> { + config.setCallbackUrl(callbackUrl); + config.setSecret(secret); + webhookRepository.save(config); + + log.info("Webhook updated successfully"); + }, + () -> { + WebhookConfig newConfig = WebhookConfig.builder() + .provider(PaymentProvider.MOBILEPAY) + .callbackUrl(callbackUrl) + .secret(secret) + .build(); + webhookRepository.save(newConfig); + + log.info("Webhook registered successfully"); + } + ); + } + + public void deleteWebhook(String webhookId) { + HttpHeaders headers = createHeaders(); + HttpEntity entity = new HttpEntity<>(headers); + + try { + restTemplate.exchange( + String.format("%s/webhooks/v1/webhooks/%s", config.getApiUrl(), webhookId), + HttpMethod.DELETE, + entity, + Object.class); + + log.info("Webhook deleted successfully: {}", webhookId); + } catch (Exception e) { + log.error("Error deleting MobilePay webhook: {}", e.getMessage()); + throw new RuntimeException("Failed to delete MobilePay webhook", e); + } + } + + public Object getWebhooks() { + HttpHeaders headers = createHeaders(); + HttpEntity entity = new HttpEntity<>(headers); + + try { + ResponseEntity response = restTemplate.exchange( + String.format("%s/webhooks/v1/webhooks", config.getApiUrl()), + HttpMethod.GET, + entity, + Object.class); + + return response.getBody(); + } catch (Exception e) { + log.error("Error getting MobilePay webhooks: {}", e.getMessage()); + throw new RuntimeException("Failed to get MobilePay webhooks", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java new file mode 100644 index 0000000..9eb038d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java @@ -0,0 +1,21 @@ +package com.zenfulcode.commercify.payment.infrastructure.gateway.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Setter +@Getter +@Configuration +@ConfigurationProperties(prefix = "integration.payments.mobilepay") +public class MobilepayConfig { + private String apiKey; + private String merchantId; + private String apiUrl; + private String clientId; + private String clientSecret; + private String subscriptionKey; + private String systemName; + private String host; +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/JpaWebhookConfigRepository.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/JpaWebhookConfigRepository.java new file mode 100644 index 0000000..9000ca7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/JpaWebhookConfigRepository.java @@ -0,0 +1,25 @@ +package com.zenfulcode.commercify.payment.infrastructure.persistence; + +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.model.WebhookConfig; +import com.zenfulcode.commercify.payment.domain.repository.WebhookConfigRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class JpaWebhookConfigRepository implements WebhookConfigRepository { + private final SpringDataJpaWebhookConfigRepository repository; + + @Override + public WebhookConfig save(WebhookConfig config) { + return repository.save(config); + } + + @Override + public Optional findByProvider(PaymentProvider provider) { + return repository.findWebhookConfigByProvider(provider); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/SpringDataJpaWebhookConfigRepository.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/SpringDataJpaWebhookConfigRepository.java new file mode 100644 index 0000000..eb835a7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/persistence/SpringDataJpaWebhookConfigRepository.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.payment.infrastructure.persistence; + +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.model.WebhookConfig; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface SpringDataJpaWebhookConfigRepository extends JpaRepository { + Optional findWebhookConfigByProvider(PaymentProvider provider); +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/webhook/WebhookHandler.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/webhook/WebhookHandler.java new file mode 100644 index 0000000..4719e55 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/webhook/WebhookHandler.java @@ -0,0 +1,19 @@ +package com.zenfulcode.commercify.payment.infrastructure.webhook; + +import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; +import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WebhookHandler { + private final PaymentProviderFactory providerFactory; + + public void handleWebhook(PaymentProvider provider, WebhookPayload payload) { + PaymentProviderService service = providerFactory.getProvider(provider); + service.handleCallback(payload); + } +} diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index 91f4c48..bdede42 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -17,17 +17,15 @@ security.jwt.secret=${JWT_SECRET_KEY} security.jwt.access-token-expiration=3600000 security.jwt.refresh-token-expiration=86400000 logging.level.org.springframework=INFO -stripe.secret-test-key=${STRIPE_SECRET_TEST_KEY} -stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET} admin.email=${ADMIN_EMAIL} admin.password=${ADMIN_PASSWORD} -mobilepay.client-id=${MOBILEPAY_CLIENT_ID} -mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID} -mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} -mobilepay.subscription-key=${MOBILEPAY_SUBSCRIPTION_KEY} -mobilepay.api-url=${MOBILEPAY_API_URL:https://api.vipps.no} -mobilepay.system-name=${MOBILEPAY_SYSTEM_NAME:commercify} - +# MobilePay Configuration +integration.payments.mobilepay.client-id=${MOBILEPAY_CLIENT_ID} +integration.payments.mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID} +integration.payments.mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} +integration.payments.mobilepay.subscription-key=${MOBILEPAY_SUBSCRIPTION_KEY} +integration.payments.mobilepay.api-url=${MOBILEPAY_API_URL:https://apitest.vipps.no} +integration.payments.mobilepay.system-name=${MOBILEPAY_SYSTEM_NAME:commercify} # Email Configuration spring.mail.host=smtp.gmail.com spring.mail.port=587 @@ -35,6 +33,5 @@ spring.mail.username=${MAIL_USERNAME} spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true - # Application Configuration app.frontend-url=${FRONTEND_URL:http://localhost:3000} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f270f01..a106ea0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,19 +14,17 @@ security.jwt.secret=${JWT_SECRET_KEY} # 1h in millisecond security.jwt.access-token-expiration=3600000 security.jwt.refresh-token-expiration=86400000 -# Stripe Configuration -stripe.secret-test-key=${STRIPE_SECRET_TEST_KEY:undefined} -stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET:undefined} # Admin Configuration admin.email=admin@commercify.app admin.password=commercifyadmin123! # MobilePay Configuration -mobilepay.client-id=${MOBILEPAY_CLIENT_ID} -mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID} -mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} -mobilepay.subscription-key=${MOBILEPAY_SUBSCRIPTION_KEY} -mobilepay.api-url=${MOBILEPAY_API_URL:https://apitest.vipps.no} -mobilepay.system-name=${MOBILEPAY_SYSTEM_NAME:commercify} +integration.payments.mobilepay.client-id=${MOBILEPAY_CLIENT_ID} +integration.payments.mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID} +integration.payments.mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} +integration.payments.mobilepay.subscription-key=${MOBILEPAY_SUBSCRIPTION_KEY} +integration.payments.mobilepay.api-url=${MOBILEPAY_API_URL:https://apitest.vipps.no} +integration.payments.mobilepay.system-name=${MOBILEPAY_SYSTEM_NAME:commercify} +integration.payments.mobilepay.host=${MOBILEPAY_HOST} # Email Configuration spring.mail.host=${MAIL_HOST:smtp.ethereal.email} spring.mail.port=${MAIL_PORT:587} diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index fc29453..d2e7805 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -5,4 +5,5 @@ https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/250126103929-changelog.sql b/src/main/resources/db/changelog/migrations/250126103929-changelog.sql new file mode 100644 index 0000000..94236ce --- /dev/null +++ b/src/main/resources/db/changelog/migrations/250126103929-changelog.sql @@ -0,0 +1,11 @@ +-- liquibase formatted sql + +-- changeset gkhaavik:1737884368941-1 +CREATE TABLE webhook_config +( + provider VARCHAR(255) NOT NULL, + callback_url VARCHAR(255) NOT NULL, + secret VARCHAR(255) NOT NULL, + CONSTRAINT pk_webhook_config PRIMARY KEY (provider) +); + From b883727380f7531a9b56804acae4abb2b396a245 Mon Sep 17 00:00:00 2001 From: GustavH Date: Wed, 29 Jan 2025 22:42:02 +0100 Subject: [PATCH 27/57] Fixing webhook registration and code cleanup Renames the BACKEND_HOST environment variable to MOBILEPAY_WEBHOOK_CALLBACK Fixing docker compose error --- .dockerignore | 1 - .gitignore | 3 +- deploy/.env.example | 6 +- deploy/docker-compose.yml | 7 +- .../api/payment/PaymentWebhookController.java | 68 +++++++++++++++++-- .../MobilepayWebhookRegistrationRequest.java | 4 ++ .../infrastructure/config/SecurityConfig.java | 2 +- ...MobilepayWebhookRegistrationResponse.java} | 2 +- .../gateway/MobilepayTokenService.java | 1 + .../gateway/client/MobilepayClient.java | 29 +++++--- .../gateway/config/MobilepayConfig.java | 5 +- .../resources/application-docker.properties | 23 +++---- src/main/resources/application.properties | 15 ++-- 13 files changed, 117 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/request/MobilepayWebhookRegistrationRequest.java rename src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/{MobilepayWebhookResponse.java => MobilepayWebhookRegistrationResponse.java} (67%) diff --git a/.dockerignore b/.dockerignore index ef52432..16ac0c2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,7 +23,6 @@ deploy/ *.log # Environment files -.env *.env # Documentation diff --git a/.gitignore b/.gitignore index 1b2f74c..23e83a3 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ build/ ### VS Code ### .vscode/ -/deploy/.env \ No newline at end of file +.env +deploy/.env \ No newline at end of file diff --git a/deploy/.env.example b/deploy/.env.example index 18ff18a..aa122b6 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -4,7 +4,7 @@ DATASOURCE_USERNAME=commercifyapp DATASOURCE_PASSWORD=password123! STRIPE_SECRET_TEST_KEY= STRIPE_WEBHOOK_SECRET= -STRIPE_WEBHOOK_ENDPOINT=http://localhost:6091/api/v1/payments/stripe/webhooks +STRIPE_WEBHOOK_ENDPOINT=https:///api/v2/stripe/webhook/callback JWT_SECRET_KEY= ADMIN_EMAIL=admin@commercify.app ADMIN_PASSWORD=admin @@ -14,8 +14,8 @@ MOBILEPAY_SUBSCRIPTION_KEY= MOBILEPAY_MERCHANT_ID= MOBILEPAY_API_URL= MOBILEPAY_SYSTEM_NAME=Commercify +MOBILEPAY_WEBHOOK_CALLBACK=https:///api/v2/mobilepay/webhook/callback MAIL_USERNAME= MAIL_PASSWORD= MAIL_HOST=smtp.ethereal.email -MAIL_PORT=587 -FRONTEND_URL=http://localhost:3000 \ No newline at end of file +MAIL_PORT=587 \ No newline at end of file diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index f670adc..3769602 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,6 +1,6 @@ services: mysql: - image: mysql:8.0 + image: mysql:9.1 container_name: commercify-mysql environment: - MYSQL_DATABASE=commercifydb @@ -40,10 +40,11 @@ services: - MOBILEPAY_SUBSCRIPTION_KEY=${MOBILEPAY_SUBSCRIPTION_KEY} - MOBILEPAY_API_URL=${MOBILEPAY_API_URL} - MOBILEPAY_SYSTEM_NAME=${MOBILEPAY_SYSTEM_NAME} - - MAIL_USERNAME=${MAIL_USERNAME} - - MAIL_PASSWORD=${MAIL_PASSWORD} + - MOBILEPAY_WEBHOOK_CALLBACK=${MOBILEPAY_WEBHOOK_CALLBACK} - MAIL_HOST=${MAIL_HOST} - MAIL_PORT=${MAIL_PORT} + - MAIL_USERNAME=${MAIL_USERNAME} + - MAIL_PASSWORD=${MAIL_PASSWORD} depends_on: mysql: condition: service_healthy diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java index a0bc6e8..80d835e 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java @@ -1,8 +1,10 @@ package com.zenfulcode.commercify.api.payment; +import com.zenfulcode.commercify.api.payment.request.MobilepayWebhookRegistrationRequest; import com.zenfulcode.commercify.payment.application.service.MobilepayWebhookService; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; import com.zenfulcode.commercify.payment.domain.valueobject.WebhookRequest; +import com.zenfulcode.commercify.shared.domain.exception.DomainException; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -28,7 +30,7 @@ public ResponseEntity> handleCallback( HttpServletRequest request ) { try { - PaymentProvider paymentProvider = PaymentProvider.valueOf(provider.toUpperCase()); + PaymentProvider paymentProvider = getPaymentProvider(provider); WebhookRequest webhookRequest = WebhookRequest.builder() .body(body) @@ -38,30 +40,78 @@ public ResponseEntity> handleCallback( webhookService.handleWebhook(paymentProvider, webhookRequest); return ResponseEntity.ok(ApiResponse.success("Webhook processed successfully")); - } catch (Exception e) { + } catch (DomainException e) { log.error("Error processing {} webhook: {}", provider, e.getMessage()); return ResponseEntity.badRequest().body( ApiResponse.error("Error processing webhook", "WEBHOOK_ERROR", 400) ); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body( + ApiResponse.error("Invalid payment provider", "INVALID_PROVIDER", 400) + ); } } @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/{provider}/register") + @PostMapping("/{provider}") public ResponseEntity> registerWebhook( @PathVariable String provider, - @RequestBody String callbackUrl + @RequestBody MobilepayWebhookRegistrationRequest request ) { try { - PaymentProvider paymentProvider = PaymentProvider.valueOf(provider.toUpperCase()); + PaymentProvider paymentProvider = getPaymentProvider(provider); - webhookService.registerWebhook(paymentProvider, callbackUrl); + webhookService.registerWebhook(paymentProvider, request.callbackUrl()); return ResponseEntity.ok(ApiResponse.success("Webhook registered successfully")); - } catch (Exception e) { + } catch (DomainException e) { log.error("Error registering {} webhook: {}", provider, e.getMessage()); return ResponseEntity.badRequest().body( ApiResponse.error("Error registering webhook", "WEBHOOK_ERROR", 400) ); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body( + ApiResponse.error("Invalid payment provider", "INVALID_PROVIDER", 400) + ); + } + } + + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/{provider}") + public ResponseEntity> getWebhooks(@PathVariable String provider) { + try { + PaymentProvider paymentProvider = getPaymentProvider(provider); + + Object webhooks = webhookService.getWebhooks(paymentProvider); + return ResponseEntity.ok(ApiResponse.success(webhooks)); + } catch (DomainException e) { + log.error("Error getting {} webhooks: {}", provider, e.getMessage()); + return ResponseEntity.badRequest().body( + ApiResponse.error("Error getting webhooks", "WEBHOOK_ERROR", 400) + ); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body( + ApiResponse.error("Invalid payment provider", "INVALID_PROVIDER", 400) + ); + } + } + + @PreAuthorize("hasRole('ADMIN')") + @DeleteMapping("/{provider}/{webhookId}") + public ResponseEntity> getWebhooks(@PathVariable String provider, @PathVariable String webhookId) { + try { + PaymentProvider paymentProvider = getPaymentProvider(provider); + + webhookService.deleteWebhook(paymentProvider, webhookId); + return ResponseEntity.ok(ApiResponse.success("Webhook deleted successfully")); + } catch (DomainException e) { + log.error("Error deleting {} webhook: {}", provider, e.getMessage()); + return ResponseEntity.badRequest().body( + ApiResponse.error("Error deleting webhook", "WEBHOOK_ERROR", 400) + ); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body( + ApiResponse.error("Invalid payment provider", "INVALID_PROVIDER", 400) + ); } } @@ -70,4 +120,8 @@ private Map extractHeaders(HttpServletRequest request) { request.getHeaderNames().asIterator().forEachRemaining(name -> headers.put(name, request.getHeader(name))); return headers; } + + private PaymentProvider getPaymentProvider(String provider) { + return PaymentProvider.valueOf(provider.toUpperCase()); + } } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/request/MobilepayWebhookRegistrationRequest.java b/src/main/java/com/zenfulcode/commercify/api/payment/request/MobilepayWebhookRegistrationRequest.java new file mode 100644 index 0000000..84d270d --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/request/MobilepayWebhookRegistrationRequest.java @@ -0,0 +1,4 @@ +package com.zenfulcode.commercify.api.payment.request; + +public record MobilepayWebhookRegistrationRequest(String callbackUrl) { +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index 563b4e7..bfb00b5 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -42,7 +42,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, "/api/v2/auth/**", "/api/v2/products/active", "/api/v2/products/{id}", - "/api/v2/payments/webhooks/mobilepay/callback").permitAll() + "/api/v2/payments/webhooks/{provider}/callback").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookResponse.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookRegistrationResponse.java similarity index 67% rename from src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookResponse.java rename to src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookRegistrationResponse.java index 4594acf..c52df5e 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookResponse.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/MobilepayWebhookRegistrationResponse.java @@ -1,6 +1,6 @@ package com.zenfulcode.commercify.payment.domain.valueobject; -public record MobilepayWebhookResponse( +public record MobilepayWebhookRegistrationResponse( String secret, String id ) { diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java index 6d1d835..653ba8f 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java @@ -30,6 +30,7 @@ public String getAccessToken() { if (shouldRefreshToken()) { refreshAccessToken(); } + return accessToken; } diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java index 2e397cf..43a95a1 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java @@ -5,7 +5,7 @@ import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; import com.zenfulcode.commercify.payment.domain.model.WebhookConfig; import com.zenfulcode.commercify.payment.domain.repository.WebhookConfigRepository; -import com.zenfulcode.commercify.payment.domain.valueobject.MobilepayWebhookResponse; +import com.zenfulcode.commercify.payment.domain.valueobject.MobilepayWebhookRegistrationResponse; import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayCreatePaymentRequest; import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayPaymentResponse; import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayTokenService; @@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import javax.crypto.Mac; @@ -24,10 +25,7 @@ import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.util.*; @Component @RequiredArgsConstructor @@ -39,6 +37,7 @@ public class MobilepayClient { private final MobilepayConfig config; + @Transactional public MobilepayPaymentResponse createPayment(MobilepayCreatePaymentRequest request) { try { HttpEntity> entity = new HttpEntity<>( @@ -63,6 +62,7 @@ public MobilepayPaymentResponse createPayment(MobilepayCreatePaymentRequest requ } } + @Transactional public void validateWebhook(String contentSha256, String authorization, String date, String payload) { try { // Verify content @@ -106,12 +106,18 @@ public void validateWebhook(String contentSha256, String authorization, String d } } - private HttpHeaders createHeaders() { + @Transactional + protected HttpHeaders createHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.set("Authorization", "Bearer " + tokenService.getAccessToken()); headers.set("Ocp-Apim-Subscription-Key", config.getSubscriptionKey()); headers.set("Merchant-Serial-Number", config.getMerchantId()); + headers.set("Vipps-System-Name", config.getSystemName()); + headers.set("Vipps-System-Version", "1.0"); + headers.set("Vipps-System-Plugin-Name", "commercify"); + headers.set("Vipps-System-Plugin-Version", "1.0"); headers.set("Idempotency-Key", UUID.randomUUID().toString()); return headers; } @@ -144,6 +150,7 @@ private Map createPaymentRequest(MobilepayCreatePaymentRequest r return paymentRequest; } + @Transactional public void registerWebhook(String callbackUrl) { HttpHeaders headers = createHeaders(); @@ -161,11 +168,11 @@ public void registerWebhook(String callbackUrl) { HttpEntity> entity = new HttpEntity<>(request, headers); try { - ResponseEntity response = restTemplate.exchange( + ResponseEntity response = restTemplate.exchange( String.format("%s/webhooks/v1/webhooks", config.getApiUrl()), HttpMethod.POST, entity, - MobilepayWebhookResponse.class + MobilepayWebhookRegistrationResponse.class ); if (response.getBody() == null) { @@ -177,11 +184,13 @@ public void registerWebhook(String callbackUrl) { log.info("Webhook registration response: {}", response.getBody()); } catch (Exception e) { + log.error("Error registering MobilePay webhook: {}", e.getMessage()); throw new PaymentProcessingException("Failed to register webhook", e); } } - private void saveOrUpdateWebhook(String callbackUrl, String secret) { + @Transactional + protected void saveOrUpdateWebhook(String callbackUrl, String secret) { webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) .ifPresentOrElse( config -> { @@ -204,6 +213,7 @@ private void saveOrUpdateWebhook(String callbackUrl, String secret) { ); } + @Transactional public void deleteWebhook(String webhookId) { HttpHeaders headers = createHeaders(); HttpEntity entity = new HttpEntity<>(headers); @@ -222,6 +232,7 @@ public void deleteWebhook(String webhookId) { } } + @Transactional public Object getWebhooks() { HttpHeaders headers = createHeaders(); HttpEntity entity = new HttpEntity<>(headers); diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java index 9eb038d..4c26631 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java @@ -10,12 +10,11 @@ @Configuration @ConfigurationProperties(prefix = "integration.payments.mobilepay") public class MobilepayConfig { - private String apiKey; - private String merchantId; - private String apiUrl; private String clientId; + private String merchantId; private String clientSecret; private String subscriptionKey; + private String apiUrl; private String systemName; private String host; } diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index bdede42..3684a0d 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -1,22 +1,20 @@ spring.application.name=commercify-docker server.port=6091 # Database configuration -spring.datasource.url=${SPRING_DATASOURCE_URL} -spring.datasource.username=${SPRING_DATASOURCE_USERNAME} -spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USERNAME} +spring.datasource.password=${DATASOURCE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -# JPA and Hibernate spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect -spring.jpa.open-in-view=false -# Liquibase +# Migrations spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml -# Other configurations remain the same +#spring.liquibase.enabled=false +#The secret key must be an HMAC hash string of 256 bits; security.jwt.secret=${JWT_SECRET_KEY} # 1h in millisecond security.jwt.access-token-expiration=3600000 security.jwt.refresh-token-expiration=86400000 -logging.level.org.springframework=INFO +# Admin Configuration admin.email=${ADMIN_EMAIL} admin.password=${ADMIN_PASSWORD} # MobilePay Configuration @@ -26,12 +24,13 @@ integration.payments.mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} integration.payments.mobilepay.subscription-key=${MOBILEPAY_SUBSCRIPTION_KEY} integration.payments.mobilepay.api-url=${MOBILEPAY_API_URL:https://apitest.vipps.no} integration.payments.mobilepay.system-name=${MOBILEPAY_SYSTEM_NAME:commercify} +integration.payments.mobilepay.host=${MOBILEPAY_WEBHOOK_CALLBACK} # Email Configuration -spring.mail.host=smtp.gmail.com -spring.mail.port=587 +spring.mail.host=${MAIL_HOST} +spring.mail.port=${MAIL_PORT:587} spring.mail.username=${MAIL_USERNAME} spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true # Application Configuration -app.frontend-url=${FRONTEND_URL:http://localhost:3000} \ No newline at end of file +#logging.level.org.springframework.security=debug \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a106ea0..8362eb1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,9 @@ spring.application.name=commercify server.port=6091 # Database configuration -spring.datasource.url=jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT:3306}/${DATABASE_NAME:commercify_ddd_db}?createDatabaseIfNotExist=true -spring.datasource.username=${DATABASE_USER} -spring.datasource.password=${DATABASE_PASSWORD} +spring.datasource.url=${DATASOURCE_URL} +spring.datasource.username=${DATASOURCE_USERNAME} +spring.datasource.password=${DATASOURCE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=none # Migrations @@ -15,8 +15,8 @@ security.jwt.secret=${JWT_SECRET_KEY} security.jwt.access-token-expiration=3600000 security.jwt.refresh-token-expiration=86400000 # Admin Configuration -admin.email=admin@commercify.app -admin.password=commercifyadmin123! +admin.email=${ADMIN_EMAIL} +admin.password=${ADMIN_PASSWORD} # MobilePay Configuration integration.payments.mobilepay.client-id=${MOBILEPAY_CLIENT_ID} integration.payments.mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID} @@ -24,14 +24,13 @@ integration.payments.mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} integration.payments.mobilepay.subscription-key=${MOBILEPAY_SUBSCRIPTION_KEY} integration.payments.mobilepay.api-url=${MOBILEPAY_API_URL:https://apitest.vipps.no} integration.payments.mobilepay.system-name=${MOBILEPAY_SYSTEM_NAME:commercify} -integration.payments.mobilepay.host=${MOBILEPAY_HOST} +integration.payments.mobilepay.host=${MOBILEPAY_WEBHOOK_CALLBACK} # Email Configuration -spring.mail.host=${MAIL_HOST:smtp.ethereal.email} +spring.mail.host=${MAIL_HOST} spring.mail.port=${MAIL_PORT:587} spring.mail.username=${MAIL_USERNAME} spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true # Application Configuration -app.frontend-url=${FRONTEND_URL:http://localhost:3000} #logging.level.org.springframework.security=debug \ No newline at end of file From b4ca2064993a103f7cc5f38606ade3f86edee816 Mon Sep 17 00:00:00 2001 From: GustavH Date: Wed, 29 Jan 2025 22:43:06 +0100 Subject: [PATCH 28/57] Fixed .env example typo --- deploy/.env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/.env.example b/deploy/.env.example index aa122b6..c1fcff0 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -4,7 +4,7 @@ DATASOURCE_USERNAME=commercifyapp DATASOURCE_PASSWORD=password123! STRIPE_SECRET_TEST_KEY= STRIPE_WEBHOOK_SECRET= -STRIPE_WEBHOOK_ENDPOINT=https:///api/v2/stripe/webhook/callback +STRIPE_WEBHOOK_ENDPOINT=https:///api/v2/payments/webhooks/stripe/callback JWT_SECRET_KEY= ADMIN_EMAIL=admin@commercify.app ADMIN_PASSWORD=admin @@ -14,7 +14,7 @@ MOBILEPAY_SUBSCRIPTION_KEY= MOBILEPAY_MERCHANT_ID= MOBILEPAY_API_URL= MOBILEPAY_SYSTEM_NAME=Commercify -MOBILEPAY_WEBHOOK_CALLBACK=https:///api/v2/mobilepay/webhook/callback +MOBILEPAY_WEBHOOK_CALLBACK=https:///api/v2/payments/webhooks/mobilepay/callback MAIL_USERNAME= MAIL_PASSWORD= MAIL_HOST=smtp.ethereal.email From 304eb339d2c1819b09b7e9357aedd9c23bac3dcf Mon Sep 17 00:00:00 2001 From: GustavH Date: Thu, 30 Jan 2025 00:24:31 +0100 Subject: [PATCH 29/57] Fixing webhook callbacks (not changing any state yet) Temporarely disabling shippingCost as it doesnt work with mobilepay integration --- .../api/payment/mapper/PaymentDtoMapper.java | 31 +++++-------------- .../request/InitiatePaymentRequest.java | 1 - .../request/PaymentDetailsRequest.java | 2 +- .../service/DefaultOrderPricingStrategy.java | 1 + .../domain/service/OrderDomainService.java | 4 +-- .../service/MobilepayWebhookService.java | 8 +++-- .../provider/MobilepayProviderService.java | 2 +- .../gateway/MobilepayTokenService.java | 2 +- .../gateway/client/MobilepayClient.java | 29 ++++++++++------- .../gateway/config/MobilepayConfig.java | 2 +- .../resources/application-docker.properties | 2 +- src/main/resources/application.properties | 2 +- 12 files changed, 39 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java index 564c185..ce2c955 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java @@ -10,6 +10,7 @@ import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.valueobject.MobilepayPaymentRequest; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; @@ -17,7 +18,6 @@ import org.springframework.stereotype.Component; import java.time.Instant; -import java.util.Map; @Component @RequiredArgsConstructor @@ -29,7 +29,7 @@ public InitiatePaymentCommand toCommand(InitiatePaymentRequest request) { return new InitiatePaymentCommand( order, - PaymentMethod.valueOf(request.paymentMethod()), + PaymentMethod.valueOf(request.paymentDetails().paymentMethod()), PaymentProvider.valueOf(request.provider()), toProviderRequest(request.paymentDetails()) ); @@ -84,27 +84,10 @@ public CapturePaymentCommand toCaptureCommand(PaymentId paymentId, String transa } private PaymentProviderRequest toProviderRequest(PaymentDetailsRequest details) { - return new PaymentProviderRequest() { - @Override - public PaymentMethod getPaymentMethod() { - return PaymentMethod.valueOf(details.paymentMethodId()); - } - - public String getPaymentMethodId() { - return details.paymentMethodId(); - } - - public String getReturnUrl() { - return details.returnUrl(); - } - - public String getCancelUrl() { - return details.cancelUrl(); - } - - public Map getAdditionalData() { - return details.additionalData(); - } - }; + return new MobilepayPaymentRequest( + PaymentMethod.valueOf(details.paymentMethod()), + details.additionalData().get("phoneNumber"), + details.returnUrl() + ); } } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java b/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java index 068e6ff..be949a3 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java @@ -4,7 +4,6 @@ public record InitiatePaymentRequest( OrderId orderId, - String paymentMethod, String provider, PaymentDetailsRequest paymentDetails ) { diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java b/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java index 2fc0a35..9eca57c 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java @@ -3,7 +3,7 @@ import java.util.Map; public record PaymentDetailsRequest( - String paymentMethodId, + String paymentMethod, String returnUrl, String cancelUrl, Map additionalData diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/DefaultOrderPricingStrategy.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/DefaultOrderPricingStrategy.java index 3f418e8..13688d7 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/DefaultOrderPricingStrategy.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/DefaultOrderPricingStrategy.java @@ -11,6 +11,7 @@ @Service @RequiredArgsConstructor public class DefaultOrderPricingStrategy implements OrderPricingStrategy { + // TODO: Currency conversion private static final BigDecimal TAX_RATE = new BigDecimal("0.20"); // 20% tax private static final Money FREE_SHIPPING_THRESHOLD = Money.of(new BigDecimal("100"), "USD"); private static final Money STANDARD_SHIPPING = Money.of(new BigDecimal("10"), "USD"); diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index 77d0ef5..54f35d4 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -88,8 +88,8 @@ private void applyPricing(Order order) { Money subtotal = pricingStrategy.calculateSubtotal(order); order.setSubtotal(subtotal); - Money shippingCost = pricingStrategy.calculateShippingCost(order); - order.setShippingCost(shippingCost); +// Money shippingCost = pricingStrategy.calculateShippingCost(order); +// order.setShippingCost(shippingCost); Money tax = pricingStrategy.calculateTax(order); order.setTax(tax); diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java index 0de5d73..761c42c 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java @@ -8,9 +8,11 @@ import com.zenfulcode.commercify.payment.domain.valueobject.WebhookRequest; import com.zenfulcode.commercify.payment.domain.valueobject.webhook.MobilepayWebhookPayload; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class MobilepayWebhookService { @@ -23,9 +25,9 @@ public void handleWebhook(PaymentProvider provider, WebhookRequest request) { // TODO: Refactor this, it's bad practice if (service instanceof MobilepayProviderService mobilePayService) { - String contentSha256 = request.headers().get("Content-SHA256"); - String authorization = request.headers().get("Authorization"); - String date = request.headers().get("Date"); + String contentSha256 = request.headers().get("x-ms-content-sha256"); + String authorization = request.headers().get("authorization"); + String date = request.headers().get("x-ms-date"); mobilePayService.authenticateWebhook(date, contentSha256, authorization, request.body()); } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java index e623a0c..fd57c76 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java @@ -114,6 +114,6 @@ public Object getWebhooks() { } public void authenticateWebhook(String date, String contentSha256, String authorization, String payload) { - mobilePayClient.validateWebhook(date, contentSha256, authorization, payload); + mobilePayClient.validateWebhook(contentSha256, authorization, date, payload); } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java index 653ba8f..def8d75 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/MobilepayTokenService.java @@ -25,7 +25,7 @@ public MobilepayTokenService(RestTemplate restTemplate, MobilepayConfig config) this.restTemplate = restTemplate; this.config = config; } - + public String getAccessToken() { if (shouldRefreshToken()) { refreshAccessToken(); diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java index 43a95a1..a7cf1d7 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java @@ -58,6 +58,7 @@ public MobilepayPaymentResponse createPayment(MobilepayCreatePaymentRequest requ return response.getBody(); } catch (Exception e) { + log.error("Error creating MobilePay payment: {}", e.getMessage()); throw new PaymentProcessingException("Failed to create MobilePay payment", e); } } @@ -79,15 +80,13 @@ public void validateWebhook(String contentSha256, String authorization, String d // Verify signature log.info("Verifying signature"); - URI uri = new URI(config.getHost()); - - String expectedSignedString = String.format("POST\n%s\n%s;%s;%s", uri.getPath(), date, uri.getHost(), encodedHash); - - Mac hmacSha256 = Mac.getInstance("HmacSHA256"); - - byte[] secretByteArray = tokenService.getAccessToken().getBytes(StandardCharsets.UTF_8); + URI uri = new URI(config.getWebhookCallback()); + String expectedSignedString = String.format("POST\n%s\n%s;%s;%s", uri.getPath(), date, uri.getHost(), contentSha256); + String secret = getWebhookSecret(); + byte[] secretByteArray = secret.getBytes(StandardCharsets.UTF_8); SecretKeySpec secretKey = new SecretKeySpec(secretByteArray, "HmacSHA256"); + Mac hmacSha256 = Mac.getInstance("HmacSHA256"); hmacSha256.init(secretKey); byte[] hmacSha256Bytes = hmacSha256.doFinal(expectedSignedString.getBytes(StandardCharsets.UTF_8)); @@ -122,12 +121,15 @@ protected HttpHeaders createHeaders() { return headers; } + // TODO: Improve error handling for invalid currency not including (DKK, NOK, SEK and FIN) private Map createPaymentRequest(MobilepayCreatePaymentRequest request) { Map paymentRequest = new HashMap<>(); // Amount + final long value = Math.round(request.amount().getAmount().doubleValue() * 100); + Map amount = new HashMap<>(); - amount.put("value", Math.round(request.amount().getAmount().doubleValue() * 100)); + amount.put("value", value); amount.put("currency", request.amount().getCurrency()); paymentRequest.put("amount", amount); @@ -142,9 +144,8 @@ private Map createPaymentRequest(MobilepayCreatePaymentRequest r paymentRequest.put("customer", customer); // Other fields - String reference = String.join("-", config.getMerchantId(), config.getSystemName(), request.orderId()); - paymentRequest.put("reference", reference); - paymentRequest.put("returnUrl", request.returnUrl()); + paymentRequest.put("reference", "mp-" + request.orderId()); + paymentRequest.put("returnUrl", request.returnUrl() + "?orderId=" + request.orderId()); paymentRequest.put("userFlow", "WEB_REDIRECT"); return paymentRequest; @@ -213,6 +214,12 @@ protected void saveOrUpdateWebhook(String callbackUrl, String secret) { ); } + protected String getWebhookSecret() { + return webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) + .map(WebhookConfig::getSecret) + .orElseThrow(() -> new WebhookValidationException("Webhook secret not found", null)); + } + @Transactional public void deleteWebhook(String webhookId) { HttpHeaders headers = createHeaders(); diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java index 4c26631..a7b133a 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/config/MobilepayConfig.java @@ -16,5 +16,5 @@ public class MobilepayConfig { private String subscriptionKey; private String apiUrl; private String systemName; - private String host; + private String webhookCallback; } diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index 3684a0d..b46c1f4 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -24,7 +24,7 @@ integration.payments.mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} integration.payments.mobilepay.subscription-key=${MOBILEPAY_SUBSCRIPTION_KEY} integration.payments.mobilepay.api-url=${MOBILEPAY_API_URL:https://apitest.vipps.no} integration.payments.mobilepay.system-name=${MOBILEPAY_SYSTEM_NAME:commercify} -integration.payments.mobilepay.host=${MOBILEPAY_WEBHOOK_CALLBACK} +integration.payments.mobilepay.webhook-callback=${MOBILEPAY_WEBHOOK_CALLBACK} # Email Configuration spring.mail.host=${MAIL_HOST} spring.mail.port=${MAIL_PORT:587} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8362eb1..e588514 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -24,7 +24,7 @@ integration.payments.mobilepay.client-secret=${MOBILEPAY_CLIENT_SECRET} integration.payments.mobilepay.subscription-key=${MOBILEPAY_SUBSCRIPTION_KEY} integration.payments.mobilepay.api-url=${MOBILEPAY_API_URL:https://apitest.vipps.no} integration.payments.mobilepay.system-name=${MOBILEPAY_SYSTEM_NAME:commercify} -integration.payments.mobilepay.host=${MOBILEPAY_WEBHOOK_CALLBACK} +integration.payments.mobilepay.webhook-callback=${MOBILEPAY_WEBHOOK_CALLBACK} # Email Configuration spring.mail.host=${MAIL_HOST} spring.mail.port=${MAIL_PORT:587} From 8c2bda59a97f7d4e12f6c03236a0cd97e058a313 Mon Sep 17 00:00:00 2001 From: GustavH Date: Fri, 31 Jan 2025 00:09:09 +0100 Subject: [PATCH 30/57] Add PaymentReservedEvent and enhance payment processing with transaction ID handling Improvements to webhook error handling --- .../api/payment/PaymentAdminController.java | 10 +- .../api/payment/PaymentWebhookController.java | 88 ++++-------------- .../api/payment/mapper/PaymentDtoMapper.java | 47 +++------- .../command/CapturePaymentCommand.java | 1 - .../application/dto/CapturedPayment.java | 4 +- .../service/MobilepayWebhookService.java | 30 +++--- .../service/PaymentApplicationService.java | 21 ++++- .../domain/event/PaymentCapturedEvent.java | 5 +- .../domain/event/PaymentReservedEvent.java | 21 +++++ .../event/PaymentStatusChangedEvent.java | 8 +- .../exception/WebhookProcessingException.java | 9 ++ .../payment/domain/model/Payment.java | 27 +++++- .../domain/service/PaymentDomainService.java | 22 ++++- .../service/PaymentProviderService.java | 2 +- .../provider/MobilepayProviderService.java | 44 +++++++-- .../domain/valueobject/TransactionId.java | 48 ++++++++++ .../webhook/MobilepayWebhookPayload.java | 2 +- .../gateway/client/MobilepayClient.java | 92 +++++++++++-------- .../webhook/WebhookHandler.java | 5 +- 19 files changed, 297 insertions(+), 189 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/exception/WebhookProcessingException.java create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/TransactionId.java diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java index 8516c64..4dac3e7 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java @@ -9,7 +9,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v2/payments/admin") @@ -21,10 +24,9 @@ public class PaymentAdminController { @PostMapping("/{paymentId}/capture") public ResponseEntity> capturePayment( - @PathVariable String paymentId, - @RequestBody String transactionId) { + @PathVariable String paymentId) { - CapturePaymentCommand command = paymentDtoMapper.toCaptureCommand(PaymentId.of(paymentId), transactionId); + CapturePaymentCommand command = paymentDtoMapper.toCaptureCommand(PaymentId.of(paymentId)); CapturedPayment response = paymentService.capturePayment(command); return ResponseEntity.ok(ApiResponse.success(response)); diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java index 80d835e..e077e8e 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java @@ -2,9 +2,10 @@ import com.zenfulcode.commercify.api.payment.request.MobilepayWebhookRegistrationRequest; import com.zenfulcode.commercify.payment.application.service.MobilepayWebhookService; +import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; import com.zenfulcode.commercify.payment.domain.valueobject.WebhookRequest; -import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -21,6 +22,7 @@ @RequiredArgsConstructor @Slf4j public class PaymentWebhookController { + private final PaymentApplicationService paymentService; private final MobilepayWebhookService webhookService; @PostMapping("/{provider}/callback") @@ -29,27 +31,17 @@ public ResponseEntity> handleCallback( @RequestBody String body, HttpServletRequest request ) { - try { - PaymentProvider paymentProvider = getPaymentProvider(provider); + PaymentProvider paymentProvider = paymentService.getPaymentProvider(provider); - WebhookRequest webhookRequest = WebhookRequest.builder() - .body(body) - .headers(extractHeaders(request)) - .build(); + WebhookRequest webhookRequest = WebhookRequest.builder() + .body(body) + .headers(extractHeaders(request)) + .build(); - webhookService.handleWebhook(paymentProvider, webhookRequest); + WebhookPayload payload = webhookService.authenticate(paymentProvider, webhookRequest); + paymentService.handlePaymentCallback(paymentProvider, payload); - return ResponseEntity.ok(ApiResponse.success("Webhook processed successfully")); - } catch (DomainException e) { - log.error("Error processing {} webhook: {}", provider, e.getMessage()); - return ResponseEntity.badRequest().body( - ApiResponse.error("Error processing webhook", "WEBHOOK_ERROR", 400) - ); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body( - ApiResponse.error("Invalid payment provider", "INVALID_PROVIDER", 400) - ); - } + return ResponseEntity.ok(ApiResponse.success("Webhook processed successfully")); } @PreAuthorize("hasRole('ADMIN')") @@ -58,61 +50,25 @@ public ResponseEntity> registerWebhook( @PathVariable String provider, @RequestBody MobilepayWebhookRegistrationRequest request ) { - try { - PaymentProvider paymentProvider = getPaymentProvider(provider); - - webhookService.registerWebhook(paymentProvider, request.callbackUrl()); - return ResponseEntity.ok(ApiResponse.success("Webhook registered successfully")); - } catch (DomainException e) { - log.error("Error registering {} webhook: {}", provider, e.getMessage()); - return ResponseEntity.badRequest().body( - ApiResponse.error("Error registering webhook", "WEBHOOK_ERROR", 400) - ); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body( - ApiResponse.error("Invalid payment provider", "INVALID_PROVIDER", 400) - ); - } + PaymentProvider paymentProvider = paymentService.getPaymentProvider(provider); + webhookService.registerWebhook(paymentProvider, request.callbackUrl()); + return ResponseEntity.ok(ApiResponse.success("Webhook registered successfully")); } @PreAuthorize("hasRole('ADMIN')") @GetMapping("/{provider}") public ResponseEntity> getWebhooks(@PathVariable String provider) { - try { - PaymentProvider paymentProvider = getPaymentProvider(provider); - - Object webhooks = webhookService.getWebhooks(paymentProvider); - return ResponseEntity.ok(ApiResponse.success(webhooks)); - } catch (DomainException e) { - log.error("Error getting {} webhooks: {}", provider, e.getMessage()); - return ResponseEntity.badRequest().body( - ApiResponse.error("Error getting webhooks", "WEBHOOK_ERROR", 400) - ); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body( - ApiResponse.error("Invalid payment provider", "INVALID_PROVIDER", 400) - ); - } + PaymentProvider paymentProvider = paymentService.getPaymentProvider(provider); + Object webhooks = webhookService.getWebhooks(paymentProvider); + return ResponseEntity.ok(ApiResponse.success(webhooks)); } @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{provider}/{webhookId}") public ResponseEntity> getWebhooks(@PathVariable String provider, @PathVariable String webhookId) { - try { - PaymentProvider paymentProvider = getPaymentProvider(provider); - - webhookService.deleteWebhook(paymentProvider, webhookId); - return ResponseEntity.ok(ApiResponse.success("Webhook deleted successfully")); - } catch (DomainException e) { - log.error("Error deleting {} webhook: {}", provider, e.getMessage()); - return ResponseEntity.badRequest().body( - ApiResponse.error("Error deleting webhook", "WEBHOOK_ERROR", 400) - ); - } catch (IllegalArgumentException e) { - return ResponseEntity.badRequest().body( - ApiResponse.error("Invalid payment provider", "INVALID_PROVIDER", 400) - ); - } + PaymentProvider paymentProvider = paymentService.getPaymentProvider(provider); + webhookService.deleteWebhook(paymentProvider, webhookId); + return ResponseEntity.ok(ApiResponse.success("Webhook deleted successfully")); } private Map extractHeaders(HttpServletRequest request) { @@ -120,8 +76,4 @@ private Map extractHeaders(HttpServletRequest request) { request.getHeaderNames().asIterator().forEachRemaining(name -> headers.put(name, request.getHeader(name))); return headers; } - - private PaymentProvider getPaymentProvider(String provider) { - return PaymentProvider.valueOf(provider.toUpperCase()); - } } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java index ce2c955..c6dc1c1 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java @@ -1,5 +1,6 @@ package com.zenfulcode.commercify.api.payment.mapper; +import com.fasterxml.jackson.databind.ObjectMapper; import com.zenfulcode.commercify.api.payment.request.InitiatePaymentRequest; import com.zenfulcode.commercify.api.payment.request.PaymentDetailsRequest; import com.zenfulcode.commercify.api.payment.response.PaymentResponse; @@ -8,21 +9,23 @@ import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; +import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; -import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; import com.zenfulcode.commercify.payment.domain.valueobject.MobilepayPaymentRequest; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; +import com.zenfulcode.commercify.payment.domain.valueobject.WebhookRequest; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.MobilepayWebhookPayload; import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import java.time.Instant; - @Component @RequiredArgsConstructor public class PaymentDtoMapper { + private final PaymentApplicationService paymentService; private final OrderApplicationService orderService; + private final ObjectMapper jacksonObjectMapper; public InitiatePaymentCommand toCommand(InitiatePaymentRequest request) { Order order = orderService.getOrderById(request.orderId()); @@ -30,7 +33,7 @@ public InitiatePaymentCommand toCommand(InitiatePaymentRequest request) { return new InitiatePaymentCommand( order, PaymentMethod.valueOf(request.paymentDetails().paymentMethod()), - PaymentProvider.valueOf(request.provider()), + paymentService.getPaymentProvider(request.provider()), toProviderRequest(request.paymentDetails()) ); } @@ -43,42 +46,14 @@ public PaymentResponse toResponse(InitializedPayment response) { ); } - public WebhookPayload toWebhookPayload(String payload, String signature) { - return new WebhookPayload() { - @Override - public String getEventType() { - return "payment.callback"; - } - - @Override - public String getPaymentReference() { - return null; - } - - @Override - public Instant getTimestamp() { - return Instant.now(); - } - - @Override - public boolean isValid() { - return true; - } - - public String getPayload() { - return payload; - } - - public String getSignature() { - return signature; - } - }; + // TODO: Make more generic to support other providers + public WebhookPayload toWebhookPayload(WebhookRequest request) { + return jacksonObjectMapper.convertValue(request.body(), MobilepayWebhookPayload.class); } - public CapturePaymentCommand toCaptureCommand(PaymentId paymentId, String transactionId) { + public CapturePaymentCommand toCaptureCommand(PaymentId paymentId) { return new CapturePaymentCommand( paymentId, - transactionId, null ); } diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java b/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java index 397e3e9..f3c86bb 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java @@ -5,7 +5,6 @@ public record CapturePaymentCommand( PaymentId paymentId, - String transactionId, Money captureAmount ) { } diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/dto/CapturedPayment.java b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CapturedPayment.java index ed0dbe2..e20aa55 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/dto/CapturedPayment.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/dto/CapturedPayment.java @@ -1,7 +1,9 @@ package com.zenfulcode.commercify.payment.application.dto; +import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; + public record CapturedPayment( - String transactionId, + TransactionId transactionId, CaptureAmount captureAmount, boolean isFullyCaptured ) { diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java index 761c42c..1866b19 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java @@ -1,12 +1,11 @@ package com.zenfulcode.commercify.payment.application.service; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.zenfulcode.commercify.api.payment.mapper.PaymentDtoMapper; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; -import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; import com.zenfulcode.commercify.payment.domain.service.provider.MobilepayProviderService; import com.zenfulcode.commercify.payment.domain.valueobject.WebhookRequest; -import com.zenfulcode.commercify.payment.domain.valueobject.webhook.MobilepayWebhookPayload; +import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -17,24 +16,25 @@ @RequiredArgsConstructor public class MobilepayWebhookService { private final PaymentProviderFactory providerFactory; - private final ObjectMapper objectMapper; + private final PaymentDtoMapper mapper; @Transactional - public void handleWebhook(PaymentProvider provider, WebhookRequest request) { - PaymentProviderService service = providerFactory.getProvider(provider); + public WebhookPayload authenticate(PaymentProvider provider, WebhookRequest request) { + MobilepayProviderService paymentProvider = (MobilepayProviderService) providerFactory.getProvider(provider); - // TODO: Refactor this, it's bad practice - if (service instanceof MobilepayProviderService mobilePayService) { - String contentSha256 = request.headers().get("x-ms-content-sha256"); - String authorization = request.headers().get("authorization"); - String date = request.headers().get("x-ms-date"); + String contentSha256 = request.headers().get("x-ms-content-sha256"); + String authorization = request.headers().get("authorization"); + String date = request.headers().get("x-ms-date"); - mobilePayService.authenticateWebhook(date, contentSha256, authorization, request.body()); - } + paymentProvider.authenticateWebhook(date, contentSha256, authorization, request.body()); - MobilepayWebhookPayload payload = objectMapper.convertValue(request.body(), MobilepayWebhookPayload.class); + log.info("Authenticated webhook"); - service.handleCallback(payload); + WebhookPayload payload = mapper.toWebhookPayload(request); + + log.info("Authenticated webhook: {}", payload); + + return payload; } @Transactional diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java index c3476f2..35fd508 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java @@ -5,13 +5,16 @@ import com.zenfulcode.commercify.payment.application.dto.CaptureAmount; import com.zenfulcode.commercify.payment.application.dto.CapturedPayment; import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; +import com.zenfulcode.commercify.payment.domain.exception.PaymentProviderNotFoundException; import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.repository.PaymentRepository; import com.zenfulcode.commercify.payment.domain.service.PaymentDomainService; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderResponse; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; import com.zenfulcode.commercify.payment.infrastructure.webhook.WebhookHandler; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; @@ -29,6 +32,7 @@ public class PaymentApplicationService { private final PaymentProviderFactory providerFactory; private final DomainEventPublisher eventPublisher; private final WebhookHandler webhookHandler; + private final PaymentRepository paymentRepository; @Transactional public InitializedPayment initiatePayment(InitiatePaymentCommand command) { @@ -55,6 +59,8 @@ public InitializedPayment initiatePayment(InitiatePaymentCommand command) { // Update payment with provider reference payment.updateProviderReference(providerResponse.providerReference()); + paymentRepository.save(payment); + // Publish events eventPublisher.publish(payment.getDomainEvents()); @@ -68,7 +74,9 @@ public InitializedPayment initiatePayment(InitiatePaymentCommand command) { @Transactional public void handlePaymentCallback(PaymentProvider provider, WebhookPayload payload) { - webhookHandler.handleWebhook(provider, payload); + Payment payment = paymentDomainService.getPaymentByProviderReference(payload.getPaymentReference()); + webhookHandler.handleWebhook(provider, payload, payment); + paymentRepository.save(payment); } // TODO: Make sure the capture currency is the same as the payment currency @@ -78,7 +86,8 @@ public CapturedPayment capturePayment(CapturePaymentCommand command) { Money captureAmount = command.captureAmount() == null ? payment.getAmount() : command.captureAmount(); - paymentDomainService.capturePayment(payment, command.transactionId(), captureAmount); + paymentDomainService.capturePayment(payment, TransactionId.generate(), captureAmount); + paymentRepository.save(payment); // Publish events eventPublisher.publish(payment.getDomainEvents()); @@ -90,4 +99,12 @@ public CapturedPayment capturePayment(CapturePaymentCommand command) { return new CapturedPayment(payment.getTransactionId(), captureAmountDto, isFullyCaptured); } + + public PaymentProvider getPaymentProvider(String provider) { + try { + return PaymentProvider.valueOf(provider.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new PaymentProviderNotFoundException(provider); + } + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java index 78edb4c..444e286 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java @@ -2,6 +2,7 @@ import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.model.Money; import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; @@ -19,14 +20,14 @@ public class PaymentCapturedEvent extends DomainEvent { private final PaymentId paymentId; private final OrderId orderId; private final Money amount; - private final String transactionId; + private final TransactionId transactionId; private final Instant capturedAt; public PaymentCapturedEvent( PaymentId paymentId, OrderId orderId, Money amount, - String transactionId + TransactionId transactionId ) { this.paymentId = paymentId; this.orderId = orderId; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java new file mode 100644 index 0000000..7a4f2dd --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java @@ -0,0 +1,21 @@ +package com.zenfulcode.commercify.payment.domain.event; + +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import lombok.Getter; + +@Getter +public class PaymentReservedEvent extends DomainEvent { + private final PaymentId paymentId; + private final OrderId orderId; + private final TransactionId transactionId; + + public PaymentReservedEvent(PaymentId paymentId, OrderId orderId, TransactionId transactionId) { + this.paymentId = paymentId; + this.orderId = orderId; + this.transactionId = transactionId; + } + +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java index 4c8b616..14c77a8 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java @@ -1,8 +1,9 @@ package com.zenfulcode.commercify.payment.domain.event; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; -import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; import lombok.Getter; @@ -20,7 +21,8 @@ public class PaymentStatusChangedEvent extends DomainEvent { private final OrderId orderId; private final PaymentStatus oldStatus; private final PaymentStatus newStatus; - private final String transactionId; + @AggregateId + private final TransactionId transactionId; private final Instant changedAt; public PaymentStatusChangedEvent( @@ -28,7 +30,7 @@ public PaymentStatusChangedEvent( OrderId orderId, PaymentStatus oldStatus, PaymentStatus newStatus, - String transactionId + TransactionId transactionId ) { this.paymentId = paymentId; this.orderId = orderId; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/exception/WebhookProcessingException.java b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/WebhookProcessingException.java new file mode 100644 index 0000000..87b02ac --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/exception/WebhookProcessingException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.payment.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; + +public class WebhookProcessingException extends DomainException { + public WebhookProcessingException(String message) { + super("Webhook validation mismatch: " + message); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java index 12512a9..0dc078d 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java @@ -4,6 +4,7 @@ import com.zenfulcode.commercify.payment.domain.event.*; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; import com.zenfulcode.commercify.payment.domain.valueobject.refund.RefundReason; import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; import com.zenfulcode.commercify.shared.domain.model.Money; @@ -56,8 +57,8 @@ public class Payment extends AggregateRoot { @Column(name = "provider_reference") private String providerReference; - @Column(name = "transaction_id") - private String transactionId; + @Embedded + private TransactionId transactionId; @Column(name = "error_message") private String errorMessage; @@ -103,7 +104,7 @@ public static Payment create( } // Domain methods - public void markAsCaptured(String transactionId, Money capturedAmount) { + public void markAsCaptured(TransactionId transactionId, Money capturedAmount) { PaymentStatus oldStatus = this.status; this.status = PaymentStatus.CAPTURED; @@ -218,6 +219,26 @@ public void recordPaymentAttempt(boolean successful, String details) { } } + public void reserve() { + PaymentStatus oldStatus = this.status; + + this.status = PaymentStatus.RESERVED; + + registerEvent(new PaymentStatusChangedEvent( + this.id, + order.getId(), + oldStatus, + PaymentStatus.RESERVED, + null + )); + + registerEvent(new PaymentReservedEvent( + this.id, + order.getId(), + transactionId + )); + } + @Embeddable @Getter @NoArgsConstructor diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java index e516c16..49a5bd7 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java @@ -9,6 +9,7 @@ import com.zenfulcode.commercify.payment.domain.repository.PaymentRepository; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; +import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; import com.zenfulcode.commercify.payment.domain.valueobject.refund.RefundRequest; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import com.zenfulcode.commercify.shared.domain.model.Money; @@ -51,7 +52,7 @@ public Payment createPayment(Order order, PaymentMethod paymentMethod, PaymentPr /** * Processes a successful payment capture */ - public void capturePayment(Payment payment, String transactionId, Money capturedAmount) { + public void capturePayment(Payment payment, TransactionId transactionId, Money capturedAmount) { validationService.validatePaymentCapture(payment, capturedAmount); payment.markAsCaptured(transactionId, capturedAmount); @@ -64,8 +65,12 @@ public void capturePayment(Payment payment, String transactionId, Money captured * Handles payment failures */ public void failPayment(Payment payment, String failureReason) { + failPayment(payment, failureReason, PaymentStatus.FAILED); + } + + public void failPayment(Payment payment, String failureReason, PaymentStatus status) { // Validate current state - validationService.validateStatusTransition(payment, PaymentStatus.FAILED); + validationService.validateStatusTransition(payment, status); payment.markAsFailed(failureReason); @@ -110,4 +115,17 @@ public Payment getPaymentById(PaymentId paymentId) { return paymentRepository.findById(paymentId) .orElseThrow(() -> new PaymentNotFoundException(paymentId)); } + + public Payment getPaymentByProviderReference(String providerReference) { + return paymentRepository.findByProviderReference(providerReference) + .orElseThrow(() -> new PaymentNotFoundException(providerReference)); + } + + public void authorizePayment(Payment payment) { + validationService.validateStatusTransition(payment, PaymentStatus.RESERVED); + + payment.reserve(); + + eventPublisher.publish(payment.getDomainEvents()); + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java index 2f2984a..e4075c2 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentProviderService.java @@ -22,7 +22,7 @@ public interface PaymentProviderService { /** * Handle provider webhook callbacks */ - void handleCallback(WebhookPayload payload); + void handleCallback(Payment payment, WebhookPayload payload); /** * Get supported payment methods diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java index fd57c76..c6737f0 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java @@ -5,16 +5,15 @@ import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; +import com.zenfulcode.commercify.payment.domain.service.PaymentDomainService; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; -import com.zenfulcode.commercify.payment.domain.valueobject.MobilepayPaymentRequest; -import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderConfig; -import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderRequest; -import com.zenfulcode.commercify.payment.domain.valueobject.PaymentProviderResponse; +import com.zenfulcode.commercify.payment.domain.valueobject.*; import com.zenfulcode.commercify.payment.domain.valueobject.webhook.MobilepayWebhookPayload; import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayCreatePaymentRequest; import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayPaymentResponse; import com.zenfulcode.commercify.payment.infrastructure.gateway.client.MobilepayClient; +import com.zenfulcode.commercify.shared.domain.model.Money; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -26,6 +25,7 @@ @RequiredArgsConstructor public class MobilepayProviderService implements PaymentProviderService { private final MobilepayClient mobilePayClient; + private final PaymentDomainService paymentService; @Override public PaymentProviderResponse initiatePayment(Payment payment, OrderId orderId, PaymentProviderRequest request) { @@ -52,12 +52,40 @@ public PaymentProviderResponse initiatePayment(Payment payment, OrderId orderId, } @Override - public void handleCallback(WebhookPayload payload) { + public void handleCallback(Payment payment, WebhookPayload payload) { MobilepayWebhookPayload webhookPayload = (MobilepayWebhookPayload) payload; - // Handle the webhook - if (webhookPayload.isValid()) { - log.info("MobilePay webhook received: {}", webhookPayload); + log.info("Handling MobilePay webhook: {}", webhookPayload); + + if (!webhookPayload.isValid()) { + log.error("Invalid webhook payload: {}", webhookPayload); + return; + } + + switch (payload.getEventType()) { + case "CREATED": + log.info("Payment created: {}", payment.getId()); + break; + case "AUTHORIZED": + paymentService.authorizePayment(payment); + break; + case "ABORTED", "CANCELLED": + paymentService.cancelPayment(payment); + break; + case "EXPIRED": + paymentService.failPayment(payment, "Payment expired", PaymentStatus.EXPIRED); + break; + case "TERMINATED": + paymentService.failPayment(payment, "Payment terminated", PaymentStatus.TERMINATED); + break; + case "CAPTURED": + double amount = webhookPayload.amount().value() / 100.0; + Money captureAmount = Money.of(amount, webhookPayload.amount().currency()); + paymentService.capturePayment(payment, TransactionId.generate(), captureAmount); + break; + case "REFUNDED": + log.info("Handle refund: {}", payment.getId()); + break; } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/TransactionId.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/TransactionId.java new file mode 100644 index 0000000..bcc483c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/TransactionId.java @@ -0,0 +1,48 @@ +package com.zenfulcode.commercify.payment.domain.valueobject; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.UUID; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TransactionId { + @Column(name = "transaction_id") + private String id; + + private TransactionId(String id) { + this.id = Objects.requireNonNull(id); + } + + public static TransactionId generate() { + return new TransactionId(UUID.randomUUID().toString()); + } + + public static TransactionId of(String id) { + return new TransactionId(id); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransactionId paymentId = (TransactionId) o; + return Objects.equals(id, paymentId.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return id; + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/MobilepayWebhookPayload.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/MobilepayWebhookPayload.java index 36f4361..3b79538 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/MobilepayWebhookPayload.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/webhook/MobilepayWebhookPayload.java @@ -33,7 +33,7 @@ public boolean isValid() { return reference != null && name != null; } - record MobilepayAmount( + public record MobilepayAmount( String currency, long value ) { diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java index a7cf1d7..f16277b 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java @@ -1,6 +1,7 @@ package com.zenfulcode.commercify.payment.infrastructure.gateway.client; import com.zenfulcode.commercify.payment.domain.exception.PaymentProcessingException; +import com.zenfulcode.commercify.payment.domain.exception.WebhookProcessingException; import com.zenfulcode.commercify.payment.domain.exception.WebhookValidationException; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; import com.zenfulcode.commercify.payment.domain.model.WebhookConfig; @@ -13,6 +14,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; @@ -26,6 +28,8 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; @Component @RequiredArgsConstructor @@ -73,7 +77,7 @@ public void validateWebhook(String contentSha256, String authorization, String d String encodedHash = Base64.getEncoder().encodeToString(hash); if (!encodedHash.equals(contentSha256)) { - throw new SecurityException("Hash mismatch"); + throw new WebhookProcessingException("Hash mismatch"); } log.info("Content verified"); @@ -83,9 +87,8 @@ public void validateWebhook(String contentSha256, String authorization, String d URI uri = new URI(config.getWebhookCallback()); String expectedSignedString = String.format("POST\n%s\n%s;%s;%s", uri.getPath(), date, uri.getHost(), contentSha256); - String secret = getWebhookSecret(); - byte[] secretByteArray = secret.getBytes(StandardCharsets.UTF_8); - SecretKeySpec secretKey = new SecretKeySpec(secretByteArray, "HmacSHA256"); + CompletableFuture secretByteArray = getWebhookSecret().thenApply(s -> s.getBytes(StandardCharsets.UTF_8)); + SecretKeySpec secretKey = new SecretKeySpec(secretByteArray.get(), "HmacSHA256"); Mac hmacSha256 = Mac.getInstance("HmacSHA256"); hmacSha256.init(secretKey); @@ -94,14 +97,14 @@ public void validateWebhook(String contentSha256, String authorization, String d String expectedAuthorization = String.format("HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=%s", expectedSignature); if (!authorization.equals(expectedAuthorization)) { - throw new SecurityException("Signature mismatch"); + throw new WebhookProcessingException("Signature mismatch"); } log.info("Signature verified"); } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("SHA-256 algorithm not found", e); - } catch (InvalidKeyException | URISyntaxException e) { - throw new RuntimeException(e); + throw new WebhookProcessingException("SHA-256 algorithm not found"); + } catch (InvalidKeyException | URISyntaxException | InterruptedException | ExecutionException e) { + throw new WebhookProcessingException(e.getMessage()); } } @@ -111,8 +114,8 @@ protected HttpHeaders createHeaders() { headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.set("Authorization", "Bearer " + tokenService.getAccessToken()); - headers.set("Ocp-Apim-Subscription-Key", config.getSubscriptionKey()); headers.set("Merchant-Serial-Number", config.getMerchantId()); + headers.set("Ocp-Apim-Subscription-Key", config.getSubscriptionKey()); headers.set("Vipps-System-Name", config.getSystemName()); headers.set("Vipps-System-Version", "1.0"); headers.set("Vipps-System-Plugin-Name", "commercify"); @@ -190,35 +193,6 @@ public void registerWebhook(String callbackUrl) { } } - @Transactional - protected void saveOrUpdateWebhook(String callbackUrl, String secret) { - webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) - .ifPresentOrElse( - config -> { - config.setCallbackUrl(callbackUrl); - config.setSecret(secret); - webhookRepository.save(config); - - log.info("Webhook updated successfully"); - }, - () -> { - WebhookConfig newConfig = WebhookConfig.builder() - .provider(PaymentProvider.MOBILEPAY) - .callbackUrl(callbackUrl) - .secret(secret) - .build(); - webhookRepository.save(newConfig); - - log.info("Webhook registered successfully"); - } - ); - } - - protected String getWebhookSecret() { - return webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) - .map(WebhookConfig::getSecret) - .orElseThrow(() -> new WebhookValidationException("Webhook secret not found", null)); - } @Transactional public void deleteWebhook(String webhookId) { @@ -235,7 +209,7 @@ public void deleteWebhook(String webhookId) { log.info("Webhook deleted successfully: {}", webhookId); } catch (Exception e) { log.error("Error deleting MobilePay webhook: {}", e.getMessage()); - throw new RuntimeException("Failed to delete MobilePay webhook", e); + throw new WebhookProcessingException("Failed to delete MobilePay webhook"); } } @@ -254,7 +228,45 @@ public Object getWebhooks() { return response.getBody(); } catch (Exception e) { log.error("Error getting MobilePay webhooks: {}", e.getMessage()); - throw new RuntimeException("Failed to get MobilePay webhooks", e); + throw new WebhookProcessingException("Failed to get MobilePay webhooks"); } } + + @Async + protected CompletableFuture getWebhookSecret() { + try { + final String secret = webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) + .map(WebhookConfig::getSecret) + .orElseThrow(() -> new PaymentProcessingException("Webhook secret not found", null)); + + return CompletableFuture.completedFuture(secret); + } catch (Exception e) { + log.error("Error getting webhook secret: {}", e.getMessage()); + return CompletableFuture.failedFuture(e); + } + } + + @Transactional + protected void saveOrUpdateWebhook(String callbackUrl, String secret) { + webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) + .ifPresentOrElse( + config -> { + config.setCallbackUrl(callbackUrl); + config.setSecret(secret); + webhookRepository.save(config); + + log.info("Webhook updated successfully"); + }, + () -> { + WebhookConfig newConfig = WebhookConfig.builder() + .provider(PaymentProvider.MOBILEPAY) + .callbackUrl(callbackUrl) + .secret(secret) + .build(); + webhookRepository.save(newConfig); + + log.info("Webhook registered successfully"); + } + ); + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/webhook/WebhookHandler.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/webhook/WebhookHandler.java index 4719e55..75a18fd 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/webhook/WebhookHandler.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/webhook/WebhookHandler.java @@ -1,5 +1,6 @@ package com.zenfulcode.commercify.payment.infrastructure.webhook; +import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; @@ -12,8 +13,8 @@ public class WebhookHandler { private final PaymentProviderFactory providerFactory; - public void handleWebhook(PaymentProvider provider, WebhookPayload payload) { + public void handleWebhook(PaymentProvider provider, WebhookPayload payload, Payment payment) { PaymentProviderService service = providerFactory.getProvider(provider); - service.handleCallback(payload); + service.handleCallback(payment, payload); } } From 2673e899a2a76ecb4633c33893a8fb6db21c0dc2 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sat, 1 Feb 2025 23:02:55 +0100 Subject: [PATCH 31/57] Fixing mobilepay webhooks --- pom.xml | 1 - .../api/payment/PaymentWebhookController.java | 13 +++++-- .../api/payment/mapper/PaymentDtoMapper.java | 10 ++++-- .../service/MobilepayWebhookService.java | 9 +---- .../service/PaymentValidationService.java | 2 +- .../provider/MobilepayProviderService.java | 14 +++++--- .../gateway/client/MobilepayClient.java | 36 ++++++++----------- 7 files changed, 44 insertions(+), 41 deletions(-) diff --git a/pom.xml b/pom.xml index f723a7c..b65ca66 100644 --- a/pom.xml +++ b/pom.xml @@ -174,5 +174,4 @@ - diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java index e077e8e..2acd971 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java @@ -38,10 +38,17 @@ public ResponseEntity> handleCallback( .headers(extractHeaders(request)) .build(); - WebhookPayload payload = webhookService.authenticate(paymentProvider, webhookRequest); - paymentService.handlePaymentCallback(paymentProvider, payload); + log.info("Handling webhook callback for provider: {}", provider); - return ResponseEntity.ok(ApiResponse.success("Webhook processed successfully")); + try { + WebhookPayload payload = webhookService.authenticate(paymentProvider, webhookRequest); + paymentService.handlePaymentCallback(paymentProvider, payload); + + return ResponseEntity.ok(ApiResponse.success("Webhook processed successfully")); + } catch (Exception e) { + log.error("Error handling webhook callback", e); + return ResponseEntity.badRequest().body(ApiResponse.error("Error handling webhook callback", "SERVER_ERROR", 500)); + } } @PreAuthorize("hasRole('ADMIN')") diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java index c6dc1c1..f4e20be 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java @@ -1,5 +1,6 @@ package com.zenfulcode.commercify.api.payment.mapper; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.zenfulcode.commercify.api.payment.request.InitiatePaymentRequest; import com.zenfulcode.commercify.api.payment.request.PaymentDetailsRequest; @@ -10,6 +11,7 @@ import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; +import com.zenfulcode.commercify.payment.domain.exception.WebhookProcessingException; import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; import com.zenfulcode.commercify.payment.domain.valueobject.MobilepayPaymentRequest; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; @@ -25,7 +27,7 @@ public class PaymentDtoMapper { private final PaymentApplicationService paymentService; private final OrderApplicationService orderService; - private final ObjectMapper jacksonObjectMapper; + private final ObjectMapper objectMapper; public InitiatePaymentCommand toCommand(InitiatePaymentRequest request) { Order order = orderService.getOrderById(request.orderId()); @@ -48,7 +50,11 @@ public PaymentResponse toResponse(InitializedPayment response) { // TODO: Make more generic to support other providers public WebhookPayload toWebhookPayload(WebhookRequest request) { - return jacksonObjectMapper.convertValue(request.body(), MobilepayWebhookPayload.class); + try { + return objectMapper.readValue(request.body(), MobilepayWebhookPayload.class); + } catch (JsonProcessingException e) { + throw new WebhookProcessingException(e.getMessage()); + } } public CapturePaymentCommand toCaptureCommand(PaymentId paymentId) { diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java index 1866b19..0f53e1a 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/MobilepayWebhookService.java @@ -27,14 +27,7 @@ public WebhookPayload authenticate(PaymentProvider provider, WebhookRequest requ String date = request.headers().get("x-ms-date"); paymentProvider.authenticateWebhook(date, contentSha256, authorization, request.body()); - - log.info("Authenticated webhook"); - - WebhookPayload payload = mapper.toWebhookPayload(request); - - log.info("Authenticated webhook: {}", payload); - - return payload; + return mapper.toWebhookPayload(request); } @Transactional diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java index 38086a7..ac448b6 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java @@ -94,7 +94,7 @@ public void validateRefundRequest(Payment payment, RefundRequest refundRequest) * Validates payment status transition */ public void validateStatusTransition(Payment payment, PaymentStatus newStatus) { - if (stateFlow.canTransitionTo(payment.getStatus(), newStatus)) { + if (!stateFlow.canTransitionTo(payment.getStatus(), newStatus)) { throw new InvalidPaymentStateException( payment.getId(), payment.getStatus(), diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java index c6737f0..706cfe3 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java @@ -2,6 +2,7 @@ import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.payment.domain.exception.PaymentValidationException; +import com.zenfulcode.commercify.payment.domain.exception.WebhookProcessingException; import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; @@ -17,6 +18,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.*; @@ -52,6 +54,7 @@ public PaymentProviderResponse initiatePayment(Payment payment, OrderId orderId, } @Override + @Transactional public void handleCallback(Payment payment, WebhookPayload payload) { MobilepayWebhookPayload webhookPayload = (MobilepayWebhookPayload) payload; @@ -59,7 +62,7 @@ public void handleCallback(Payment payment, WebhookPayload payload) { if (!webhookPayload.isValid()) { log.error("Invalid webhook payload: {}", webhookPayload); - return; + throw new WebhookProcessingException("Invalid webhook payload"); } switch (payload.getEventType()) { @@ -69,7 +72,7 @@ public void handleCallback(Payment payment, WebhookPayload payload) { case "AUTHORIZED": paymentService.authorizePayment(payment); break; - case "ABORTED", "CANCELLED": + case "ABORTED", "CANCELLED": // ABORTED = When user clicks cancel in checkout paymentService.cancelPayment(payment); break; case "EXPIRED": @@ -127,21 +130,24 @@ public boolean supportsPaymentMethod(PaymentMethod method) { return getSupportedPaymentMethods().contains(method); } + @Transactional public void registerWebhook(String callbackUrl) { mobilePayClient.registerWebhook(callbackUrl); } - @Override + @Transactional public void deleteWebhook(String webhookId) { mobilePayClient.deleteWebhook(webhookId); } - @Override + @Transactional public Object getWebhooks() { return mobilePayClient.getWebhooks(); } + @Transactional public void authenticateWebhook(String date, String contentSha256, String authorization, String payload) { + log.info("Authenticating MobilePay webhook"); mobilePayClient.validateWebhook(contentSha256, authorization, date, payload); } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java index f16277b..8f89fa9 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/gateway/client/MobilepayClient.java @@ -14,7 +14,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; @@ -28,8 +27,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; @Component @RequiredArgsConstructor @@ -67,7 +64,7 @@ public MobilepayPaymentResponse createPayment(MobilepayCreatePaymentRequest requ } } - @Transactional + @Transactional(readOnly = true) public void validateWebhook(String contentSha256, String authorization, String date, String payload) { try { // Verify content @@ -77,7 +74,7 @@ public void validateWebhook(String contentSha256, String authorization, String d String encodedHash = Base64.getEncoder().encodeToString(hash); if (!encodedHash.equals(contentSha256)) { - throw new WebhookProcessingException("Hash mismatch"); + throw new SecurityException("Hash mismatch"); } log.info("Content verified"); @@ -85,11 +82,14 @@ public void validateWebhook(String contentSha256, String authorization, String d // Verify signature log.info("Verifying signature"); URI uri = new URI(config.getWebhookCallback()); - String expectedSignedString = String.format("POST\n%s\n%s;%s;%s", uri.getPath(), date, uri.getHost(), contentSha256); - CompletableFuture secretByteArray = getWebhookSecret().thenApply(s -> s.getBytes(StandardCharsets.UTF_8)); - SecretKeySpec secretKey = new SecretKeySpec(secretByteArray.get(), "HmacSHA256"); + String expectedSignedString = String.format("POST\n%s\n%s;%s;%s", uri.getPath(), date, uri.getHost(), encodedHash); + Mac hmacSha256 = Mac.getInstance("HmacSHA256"); + + byte[] secretByteArray = getWebhookSecret().getBytes(StandardCharsets.UTF_8); + + SecretKeySpec secretKey = new SecretKeySpec(secretByteArray, "HmacSHA256"); hmacSha256.init(secretKey); byte[] hmacSha256Bytes = hmacSha256.doFinal(expectedSignedString.getBytes(StandardCharsets.UTF_8)); @@ -97,13 +97,13 @@ public void validateWebhook(String contentSha256, String authorization, String d String expectedAuthorization = String.format("HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=%s", expectedSignature); if (!authorization.equals(expectedAuthorization)) { - throw new WebhookProcessingException("Signature mismatch"); + throw new SecurityException("Signature mismatch"); } log.info("Signature verified"); } catch (NoSuchAlgorithmException e) { throw new WebhookProcessingException("SHA-256 algorithm not found"); - } catch (InvalidKeyException | URISyntaxException | InterruptedException | ExecutionException e) { + } catch (InvalidKeyException | URISyntaxException e) { throw new WebhookProcessingException(e.getMessage()); } } @@ -232,18 +232,10 @@ public Object getWebhooks() { } } - @Async - protected CompletableFuture getWebhookSecret() { - try { - final String secret = webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) - .map(WebhookConfig::getSecret) - .orElseThrow(() -> new PaymentProcessingException("Webhook secret not found", null)); - - return CompletableFuture.completedFuture(secret); - } catch (Exception e) { - log.error("Error getting webhook secret: {}", e.getMessage()); - return CompletableFuture.failedFuture(e); - } + protected String getWebhookSecret() { + return webhookRepository.findByProvider(PaymentProvider.MOBILEPAY) + .map(WebhookConfig::getSecret) + .orElseThrow(() -> new PaymentProcessingException("Webhook secret not found", null)); } @Transactional From b313b43630e385c3f6c8b9bcee1c534cae7fd047 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 2 Feb 2025 00:23:22 +0100 Subject: [PATCH 32/57] Refactoring event handling and adding PaymentCancelledEventHandler --- .../AuthenticationApplicationService.java | 2 +- .../domain/event/UserAuthenticatedEvent.java | 3 +- .../service/OrderApplicationService.java | 16 ++- .../order/domain/event/OrderCreatedEvent.java | 3 +- .../domain/event/OrderStatusChangedEvent.java | 2 + .../commercify/order/domain/model/Order.java | 2 + .../domain/service/OrderDomainService.java | 21 +++- .../events/PaymentEventHandler.java | 31 +++++ .../service/PaymentApplicationService.java | 4 +- .../domain/event/IssuedRefundEvent.java | 2 + .../domain/event/PaymentCancelledEvent.java | 2 + .../domain/event/PaymentCapturedEvent.java | 2 + .../domain/event/PaymentCreatedEvent.java | 2 + .../domain/event/PaymentFailedEvent.java | 2 + .../domain/event/PaymentReservedEvent.java | 3 +- .../event/PaymentStatusChangedEvent.java | 2 + .../payment/domain/model/Payment.java | 75 ++++-------- .../domain/service/PaymentDomainService.java | 1 + .../domain/event/LargeStockIncreaseEvent.java | 3 +- .../product/domain/event/LowStockEvent.java | 3 +- .../domain/event/ProductCreatedEvent.java | 3 +- .../event/ProductPriceUpdatedEvent.java | 3 +- .../domain/event/StockCorrectionEvent.java | 3 +- .../product/domain/model/Product.java | 2 + .../DefaultProductInventoryPolicy.java | 6 +- .../shared/domain/event/DomainEvent.java | 23 +++- .../domain/event/DomainEventHandler.java | 4 +- .../service/DefaultDomainEventPublisher.java | 33 +++--- .../persistence/JpaDomainEventStore.java | 39 +++---- .../service/AggregateReferenceExtractor.java | 107 ++++++++++++++++++ .../service/EventSerializer.java | 26 ++--- .../service/UserApplicationService.java | 36 ++---- .../user/domain/event/UserCreatedEvent.java | 3 +- .../domain/event/UserStatusChangedEvent.java | 2 + .../exception/UserNotFoundException.java | 6 +- .../commercify/user/domain/model/User.java | 2 + .../domain/service/UserDomainService.java | 40 +++++-- src/main/resources/application.properties | 2 +- .../db/changelog/db.changelog-master.xml | 1 + .../migrations/250201235523-changelog.sql | 50 ++++++++ 40 files changed, 391 insertions(+), 181 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/AggregateReferenceExtractor.java create mode 100644 src/main/resources/db/changelog/migrations/250201235523-changelog.sql diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java index 3d0aad0..dd0a259 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java @@ -39,7 +39,7 @@ public AuthenticationResult authenticate(String email, String password) { // Publish domain event User user = userRepository.findByEmail(email) .orElseThrow(); // User must exist at this point - eventPublisher.publish(new UserAuthenticatedEvent(user.getId(), email)); + eventPublisher.publish(new UserAuthenticatedEvent(this, user.getId(), email)); return new AuthenticationResult(accessToken, refreshToken, authenticatedUser); } diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java b/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java index 3d8ffff..15f3caa 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java @@ -11,7 +11,8 @@ public class UserAuthenticatedEvent extends DomainEvent { private final UserId userId; private final String username; - public UserAuthenticatedEvent(UserId userId, String username) { + public UserAuthenticatedEvent(Object source, UserId userId, String username) { + super(source); this.userId = userId; this.username = username; } diff --git a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java index 8e3ecb3..1d66c9e 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java @@ -70,19 +70,19 @@ public OrderId createOrder(CreateOrderCommand command) { variants ); - // Save and publish events - Order savedOrder = orderRepository.save(order); - eventPublisher.publish(savedOrder.getDomainEvents()); - return savedOrder.getId(); + // publish events + eventPublisher.publish(order.getDomainEvents()); + + return order.getId(); } @Transactional public void updateOrderStatus(UpdateOrderStatusCommand command) { - Order order = orderRepository.findById(command.orderId()) - .orElseThrow(() -> new OrderNotFoundException(command.orderId())); + Order order = orderDomainService.getOrderById(command.orderId()); orderDomainService.updateOrderStatus(order, command.newStatus()); - orderRepository.save(order); + + // Save and publish events eventPublisher.publish(order.getDomainEvents()); } @@ -92,8 +92,6 @@ public void cancelOrder(CancelOrderCommand command) { .orElseThrow(() -> new OrderNotFoundException(command.orderId())); orderDomainService.updateOrderStatus(order, OrderStatus.CANCELLED); - orderRepository.save(order); - eventPublisher.publish(order.getDomainEvents()); } @Transactional(readOnly = true) diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java index 361d9e8..c2688c6 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderCreatedEvent.java @@ -16,7 +16,8 @@ public class OrderCreatedEvent extends DomainEvent { private final String currency; private final Instant createdAt; - public OrderCreatedEvent(OrderId orderId, UserId userId, String currency) { + public OrderCreatedEvent(Object source, OrderId orderId, UserId userId, String currency) { + super(source); this.orderId = orderId; this.userId = userId; this.currency = currency; diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java index 089ffca..fa2fae2 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java @@ -17,10 +17,12 @@ public class OrderStatusChangedEvent extends DomainEvent { private final Instant changedAt; public OrderStatusChangedEvent( + Object source, OrderId orderId, OrderStatus oldStatus, OrderStatus newStatus ) { + super(source); this.orderId = orderId; this.oldStatus = oldStatus; this.newStatus = newStatus; diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java index b16fc6a..d6ff992 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/Order.java @@ -97,6 +97,7 @@ public static Order create( // Register domain event order.registerEvent(new OrderCreatedEvent( + order, order.getId(), userId, order.getCurrency() @@ -122,6 +123,7 @@ public void updateStatus(OrderStatus newStatus) { this.status = newStatus; registerEvent(new OrderStatusChangedEvent( + this, this.id, oldStatus, newStatus diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index 54f35d4..47aa02b 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -1,17 +1,20 @@ package com.zenfulcode.commercify.order.domain.service; +import com.zenfulcode.commercify.order.domain.exception.OrderNotFoundException; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.order.domain.model.OrderLine; import com.zenfulcode.commercify.order.domain.model.OrderShippingInfo; import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.order.domain.repository.OrderRepository; import com.zenfulcode.commercify.order.domain.valueobject.OrderDetails; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.order.domain.valueobject.OrderLineDetails; import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.model.ProductVariant; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.domain.model.Money; -import com.zenfulcode.commercify.user.application.service.UserApplicationService; import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.service.UserDomainService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -25,7 +28,10 @@ public class OrderDomainService { private final OrderPricingStrategy pricingStrategy; private final OrderValidationService validationService; - private final UserApplicationService userApplicationService; + + private final OrderRepository orderRepository; + + private final UserDomainService userDomainService; public Order createOrder(OrderDetails orderDetails, List products, List variants) { // Create order with shipping info @@ -35,7 +41,7 @@ public Order createOrder(OrderDetails orderDetails, List products, List orderDetails.billingAddress() ); - User customer = userApplicationService.getUser(orderDetails.customerId()); + User customer = userDomainService.getUserById(orderDetails.customerId()); Order order = Order.create( customer.getId(), @@ -81,6 +87,8 @@ public Order createOrder(OrderDetails orderDetails, List products, List // Using validationService for order validation validationService.validateCreateOrder(order); + orderRepository.save(order); + return order; } @@ -95,6 +103,7 @@ private void applyPricing(Order order) { order.setTax(tax); order.updateTotal(); + orderRepository.save(order); } public void updateOrderStatus(Order order, OrderStatus newStatus) { @@ -108,5 +117,11 @@ public void updateOrderStatus(Order order, OrderStatus newStatus) { } order.updateStatus(newStatus); + orderRepository.save(order); + } + + public Order getOrderById(OrderId orderId) { + return orderRepository.findById(orderId) + .orElseThrow(() -> new OrderNotFoundException(orderId)); } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java b/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java new file mode 100644 index 0000000..0f821a7 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java @@ -0,0 +1,31 @@ +package com.zenfulcode.commercify.payment.application.events; + +import com.zenfulcode.commercify.order.application.command.UpdateOrderStatusCommand; +import com.zenfulcode.commercify.order.application.service.OrderApplicationService; +import com.zenfulcode.commercify.order.domain.model.OrderStatus; +import com.zenfulcode.commercify.payment.domain.event.PaymentCancelledEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Slf4j +@RequiredArgsConstructor +public class PaymentEventHandler { + private final OrderApplicationService orderApplicationService; + + @EventListener + @Transactional + public void handlePaymentCancelled(PaymentCancelledEvent event) { + log.info("Handling payment cancelled event for orderId: {}", event.getOrderId()); + + UpdateOrderStatusCommand command = new UpdateOrderStatusCommand( + event.getOrderId(), + OrderStatus.CANCELLED + ); + + orderApplicationService.updateOrderStatus(command); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java index 35fd508..030bf19 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java @@ -17,8 +17,8 @@ import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; import com.zenfulcode.commercify.payment.domain.valueobject.webhook.WebhookPayload; import com.zenfulcode.commercify.payment.infrastructure.webhook.WebhookHandler; -import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import com.zenfulcode.commercify.shared.domain.model.Money; +import com.zenfulcode.commercify.shared.domain.service.DefaultDomainEventPublisher; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,7 +30,7 @@ public class PaymentApplicationService { private final PaymentDomainService paymentDomainService; private final PaymentProviderFactory providerFactory; - private final DomainEventPublisher eventPublisher; + private final DefaultDomainEventPublisher eventPublisher; private final WebhookHandler webhookHandler; private final PaymentRepository paymentRepository; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java index 797c167..77f5cad 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java @@ -26,6 +26,7 @@ public class IssuedRefundEvent extends DomainEvent { private final Instant refundedAt; public IssuedRefundEvent( + Object source, PaymentId paymentId, OrderId orderId, Money refundAmount, @@ -33,6 +34,7 @@ public IssuedRefundEvent( String notes, boolean isFullRefund ) { + super(source); this.paymentId = paymentId; this.orderId = orderId; this.refundAmount = refundAmount; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java index 480a1de..985e8e0 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java @@ -21,10 +21,12 @@ public class PaymentCancelledEvent extends DomainEvent { private final Instant cancelledAt; public PaymentCancelledEvent( + Object source, PaymentId paymentId, OrderId orderId, String reason ) { + super(source); this.paymentId = paymentId; this.orderId = orderId; this.reason = reason; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java index 444e286..0f50f74 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java @@ -24,11 +24,13 @@ public class PaymentCapturedEvent extends DomainEvent { private final Instant capturedAt; public PaymentCapturedEvent( + Object source, PaymentId paymentId, OrderId orderId, Money amount, TransactionId transactionId ) { + super(source); this.paymentId = paymentId; this.orderId = orderId; this.amount = amount; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java index 3f4ae34..c783770 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java @@ -26,12 +26,14 @@ public class PaymentCreatedEvent extends DomainEvent { private final Instant createdAt; public PaymentCreatedEvent( + Object source, PaymentId paymentId, OrderId orderId, Money amount, PaymentMethod paymentMethod, PaymentProvider provider ) { + super(source); this.paymentId = paymentId; this.orderId = orderId; this.amount = amount; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java index 43a0558..ed2d6e4 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java @@ -21,10 +21,12 @@ public class PaymentFailedEvent extends DomainEvent { private final Instant failedAt; public PaymentFailedEvent( + Object source, PaymentId paymentId, OrderId orderId, String errorMessage ) { + super(source); this.paymentId = paymentId; this.orderId = orderId; this.errorMessage = errorMessage; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java index 7a4f2dd..6127a3f 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java @@ -12,7 +12,8 @@ public class PaymentReservedEvent extends DomainEvent { private final OrderId orderId; private final TransactionId transactionId; - public PaymentReservedEvent(PaymentId paymentId, OrderId orderId, TransactionId transactionId) { + public PaymentReservedEvent(Object source, PaymentId paymentId, OrderId orderId, TransactionId transactionId) { + super(source); this.paymentId = paymentId; this.orderId = orderId; this.transactionId = transactionId; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java index 14c77a8..f713c20 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java @@ -26,12 +26,14 @@ public class PaymentStatusChangedEvent extends DomainEvent { private final Instant changedAt; public PaymentStatusChangedEvent( + Object source, PaymentId paymentId, OrderId orderId, PaymentStatus oldStatus, PaymentStatus newStatus, TransactionId transactionId ) { + super(source); this.paymentId = paymentId; this.orderId = orderId; this.oldStatus = oldStatus; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java index 0dc078d..4caa127 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java @@ -105,21 +105,13 @@ public static Payment create( // Domain methods public void markAsCaptured(TransactionId transactionId, Money capturedAmount) { - PaymentStatus oldStatus = this.status; - - this.status = PaymentStatus.CAPTURED; this.transactionId = transactionId; this.completedAt = Instant.now(); - registerEvent(new PaymentStatusChangedEvent( - this.id, - order.getId(), - oldStatus, - PaymentStatus.CAPTURED, - transactionId - )); + updateStatus(PaymentStatus.CAPTURED); registerEvent(new PaymentCapturedEvent( + this, id, order.getId(), capturedAmount, @@ -128,21 +120,13 @@ public void markAsCaptured(TransactionId transactionId, Money capturedAmount) { } public void markAsFailed(String reason) { - PaymentStatus oldStatus = this.status; - - this.status = PaymentStatus.FAILED; this.errorMessage = reason; recordPaymentAttempt(false, reason); - registerEvent(new PaymentStatusChangedEvent( - this.id, - order.getId(), - oldStatus, - PaymentStatus.FAILED, - null - )); + updateStatus(PaymentStatus.FAILED); registerEvent(new PaymentFailedEvent( + this, this.id, order.getId(), reason @@ -150,24 +134,15 @@ public void markAsFailed(String reason) { } public void processRefund(Money refundAmount, RefundReason reason, String notes) { - PaymentStatus oldStatus = this.status; - // For full refunds if (refundAmount.equals(this.amount)) { - this.status = PaymentStatus.REFUNDED; + updateStatus(PaymentStatus.REFUNDED); } else { - this.status = PaymentStatus.PARTIALLY_REFUNDED; + updateStatus(PaymentStatus.PARTIALLY_REFUNDED); } - registerEvent(new PaymentStatusChangedEvent( - this.id, - order.getId(), - oldStatus, - this.status, - null - )); - registerEvent(new IssuedRefundEvent( + this, this.id, order.getId(), refundAmount, @@ -178,19 +153,10 @@ public void processRefund(Money refundAmount, RefundReason reason, String notes) } public void cancel() { - PaymentStatus oldStatus = this.status; - - this.status = PaymentStatus.CANCELLED; - - registerEvent(new PaymentStatusChangedEvent( - this.id, - order.getId(), - oldStatus, - PaymentStatus.CANCELLED, - null - )); + updateStatus(PaymentStatus.CANCELLED); registerEvent(new PaymentCancelledEvent( + this, this.id, order.getId(), "Payment cancelled" @@ -220,22 +186,27 @@ public void recordPaymentAttempt(boolean successful, String details) { } public void reserve() { - PaymentStatus oldStatus = this.status; - - this.status = PaymentStatus.RESERVED; + updateStatus(PaymentStatus.RESERVED); - registerEvent(new PaymentStatusChangedEvent( + registerEvent(new PaymentReservedEvent( + this, this.id, order.getId(), - oldStatus, - PaymentStatus.RESERVED, - null + transactionId )); + } - registerEvent(new PaymentReservedEvent( + private void updateStatus(PaymentStatus newStatus) { + PaymentStatus oldStatus = this.status; + this.status = newStatus; + + registerEvent(new PaymentStatusChangedEvent( + this, this.id, order.getId(), - transactionId + oldStatus, + status, + null )); } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java index 49a5bd7..057ec70 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java @@ -39,6 +39,7 @@ public Payment createPayment(Order order, PaymentMethod paymentMethod, PaymentPr payment.setOrder(order); payment.registerEvent(new PaymentCreatedEvent( + this, payment.getId(), payment.getOrder().getId(), payment.getAmount(), diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java index 3229b10..958a195 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java @@ -12,7 +12,8 @@ public class LargeStockIncreaseEvent extends DomainEvent { private final int quantity; private final String reason; - public LargeStockIncreaseEvent(ProductId productId, int quantity, String reason) { + public LargeStockIncreaseEvent(Object source, ProductId productId, int quantity, String reason) { + super(source); this.productId = productId; this.quantity = quantity; this.reason = reason; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java index f8f5ae5..b145b96 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java @@ -11,7 +11,8 @@ public class LowStockEvent extends DomainEvent { private final ProductId productId; private final int stockAmount; - public LowStockEvent(ProductId productId, int stockAmount) { + public LowStockEvent(Object source, ProductId productId, int stockAmount) { + super(source); this.productId = productId; this.stockAmount = stockAmount; } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java index d68a9dc..620cf7e 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java @@ -13,7 +13,8 @@ public class ProductCreatedEvent extends DomainEvent { private final String name; private final Money price; - public ProductCreatedEvent(ProductId productId, String name, Money price) { + public ProductCreatedEvent(Object source, ProductId productId, String name, Money price) { + super(source); this.productId = productId; this.name = name; this.price = price; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java index 8ca62ae..fc12ba9 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java @@ -13,7 +13,8 @@ public class ProductPriceUpdatedEvent extends DomainEvent { private final ProductId productId; private final Money newPrice; - public ProductPriceUpdatedEvent(ProductId productId, Money newPrice) { + public ProductPriceUpdatedEvent(Object source, ProductId productId, Money newPrice) { + super(source); this.productId = productId; this.newPrice = newPrice; } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java index 438c1b4..d984fb9 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java @@ -12,7 +12,8 @@ public class StockCorrectionEvent extends DomainEvent { private final int quantity; private final String reason; - public StockCorrectionEvent(ProductId productId, int quantity, String reason) { + public StockCorrectionEvent(Object source, ProductId productId, int quantity, String reason) { + super(source); this.productId = productId; this.quantity = quantity; this.reason = reason; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java index 712e089..9c18acf 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java @@ -77,6 +77,7 @@ public static Product create(String name, String description, int stock, Money m // Register domain event product.registerEvent(new ProductCreatedEvent( + product, product.getId(), product.getName(), product.getPrice() @@ -114,6 +115,7 @@ public void updatePrice(Money newPrice) { this.price = Objects.requireNonNull(newPrice, "Price cannot be null"); registerEvent(new ProductPriceUpdatedEvent( + this, id, newPrice )); diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java index 068f9c5..b6b3ae6 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/DefaultProductInventoryPolicy.java @@ -20,7 +20,7 @@ public class DefaultProductInventoryPolicy implements ProductInventoryPolicy { @Override public void initializeInventory(Product product) { if (product.getStock() <= REORDER_THRESHOLD) { - eventPublisher.publish(new LowStockEvent(product.getId(), product.getStock())); + eventPublisher.publish(new LowStockEvent(this, product.getId(), product.getStock())); } } @@ -28,6 +28,7 @@ public void initializeInventory(Product product) { public void handleStockIncrease(Product product, InventoryAdjustment adjustment) { if (adjustment.quantity() > 100) { eventPublisher.publish(new LargeStockIncreaseEvent( + this, product.getId(), adjustment.quantity(), adjustment.reason() @@ -38,13 +39,14 @@ public void handleStockIncrease(Product product, InventoryAdjustment adjustment) @Override public void handleStockDecrease(Product product, InventoryAdjustment adjustment) { if (product.getStock() <= LOW_STOCK_THRESHOLD) { - eventPublisher.publish(new LowStockEvent(product.getId(), product.getStock())); + eventPublisher.publish(new LowStockEvent(this, product.getId(), product.getStock())); } } @Override public void handleStockCorrection(Product product, InventoryAdjustment adjustment) { eventPublisher.publish(new StockCorrectionEvent( + this, product.getId(), adjustment.quantity(), adjustment.reason() diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java index 6518ed3..b931dae 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEvent.java @@ -1,21 +1,32 @@ package com.zenfulcode.commercify.shared.domain.event; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; +import org.springframework.context.ApplicationEvent; import java.time.Instant; import java.util.UUID; @Getter -public abstract class DomainEvent { +public abstract class DomainEvent extends ApplicationEvent { private final String eventId; - private final Instant occurredOn; + private final long occurredOn; private final String eventType; - private final int version; - protected DomainEvent() { + protected DomainEvent(Object source) { + super(source); this.eventId = UUID.randomUUID().toString(); - this.occurredOn = Instant.now(); + this.occurredOn = getTimestamp(); this.eventType = this.getClass().getSimpleName(); - this.version = 1; + } + + @Override + @JsonIgnore + public Object getSource() { + return super.getSource(); + } + + public Instant getOccurredOn() { + return Instant.ofEpochMilli(occurredOn); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java index c53f3e7..f95d9b5 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/event/DomainEventHandler.java @@ -2,6 +2,6 @@ public interface DomainEventHandler { void handle(DomainEvent event); - boolean canHandle(DomainEvent event); -} + boolean canHandle(DomainEvent event); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java b/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java index 877cf8b..cdb4370 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/service/DefaultDomainEventPublisher.java @@ -1,42 +1,45 @@ package com.zenfulcode.commercify.shared.domain.service; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; -import com.zenfulcode.commercify.shared.domain.event.DomainEventHandler; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import com.zenfulcode.commercify.shared.domain.event.DomainEventStore; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class DefaultDomainEventPublisher implements DomainEventPublisher { - private final ApplicationEventPublisher applicationEventPublisher; + private final ApplicationEventPublisher eventPublisher; private final DomainEventStore eventStore; - private final List eventHandlers; @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) public void publish(DomainEvent event) { - // Store event - eventStore.store(event); + try { + log.info("Publishing event: {}", event.getEventType()); - // Publish to Spring's event system - applicationEventPublisher.publishEvent(event); + // Store the event + eventStore.store(event); - // Handle event - handleEvent(event); + // Publish to Spring's event system + eventPublisher.publishEvent(event); + + log.info("Successfully published event: {}", event.getEventType()); + } catch (Exception e) { + log.error("Failed to publish event: {}", event.getEventType(), e); + throw e; + } } @Override public void publish(List events) { events.forEach(this::publish); } - - private void handleEvent(DomainEvent event) { - eventHandlers.stream() - .filter(handler -> handler.canHandle(event)) - .forEach(handler -> handler.handle(event)); - } } diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java index fbc93ac..e86a9e6 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/persistence/JpaDomainEventStore.java @@ -1,60 +1,53 @@ package com.zenfulcode.commercify.shared.infrastructure.persistence; -import com.fasterxml.jackson.databind.ObjectMapper; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.event.DomainEventStore; import com.zenfulcode.commercify.shared.domain.model.StoredEvent; import com.zenfulcode.commercify.shared.infrastructure.service.EventSerializer; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class JpaDomainEventStore implements DomainEventStore { private final EventStoreRepository repository; - private final ObjectMapper objectMapper; private final EventSerializer eventSerializer; + @Async + @EventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void handleDomainEvent(DomainEvent event) { + store(event); + } + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) public void store(DomainEvent event) { StoredEvent storedEvent = eventSerializer.serialize(event); repository.save(storedEvent); } @Override + @Transactional(readOnly = true) public List getEvents(String aggregateId, String aggregateType) { return repository.findByAggregateIdAndAggregateType(aggregateId, aggregateType) .stream() .map(eventSerializer::deserialize) - .collect(Collectors.toList()); + .toList(); } + @Transactional(readOnly = true) public List getEventsSince(Instant since) { return repository.findEventsSince(since) .stream() .map(eventSerializer::deserialize) - .collect(Collectors.toList()); - } - - public List getEventsByType(Class eventType) { - return repository.findByEventType(eventType.getName()) - .stream() - .map(event -> (T) eventSerializer.deserialize(event)) - .collect(Collectors.toList()); - } - - public Page getEventsByAggregateType(String aggregateType, Pageable pageable) { - return repository.findByAggregateType(aggregateType, pageable) - .map(eventSerializer::deserialize); - } - - public boolean hasEventOccurred(String aggregateId, String aggregateType, String eventType) { - return repository.hasEventType(aggregateId, aggregateType, eventType); + .toList(); } } diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/AggregateReferenceExtractor.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/AggregateReferenceExtractor.java new file mode 100644 index 0000000..cfb87c4 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/AggregateReferenceExtractor.java @@ -0,0 +1,107 @@ +package com.zenfulcode.commercify.shared.infrastructure.service; + +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; +import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Slf4j +public class AggregateReferenceExtractor { + + private static final List CONVENTIONAL_ID_NAMES = List.of( + "aggregateId", + "entityId", + "orderId", + "paymentId", + "productId", + "userId" + ); + + private static final List EVENT_SUFFIXES = List.of( + "Event", + "Created", + "Updated", + "Deleted", + "Changed", + "Cancelled", + "Completed", + "Started", + "Finished", + "Failed" + ); + + public static String extractAggregateId(DomainEvent event) { + // First try fields annotated with @AggregateId + Optional annotatedField = findAnnotatedIdField(event); + if (annotatedField.isPresent()) { + return extractFieldValue(annotatedField.get(), event); + } + + // Then try conventional ID field names + Optional conventionalField = findConventionalIdField(event); + if (conventionalField.isPresent()) { + return extractFieldValue(conventionalField.get(), event); + } + + // If no ID field is found, log a warning and use event ID + log.warn("No aggregate ID field found for event: {}", event.getClass().getSimpleName()); + return event.getEventId(); + } + + public static String extractAggregateType(DomainEvent event) { + // Remove common event-related suffixes + String aggregateType = event.getClass().getSimpleName(); + for (String suffix : EVENT_SUFFIXES) { + if (aggregateType.endsWith(suffix)) { + aggregateType = aggregateType.substring(0, + aggregateType.length() - suffix.length()); + } + } + + return aggregateType; + } + + private static Optional findAnnotatedIdField(DomainEvent event) { + List allFields = getAllFields(event.getClass()); + return allFields.stream() + .filter(field -> field.isAnnotationPresent(AggregateId.class)) + .findFirst(); + } + + private static Optional findConventionalIdField(DomainEvent event) { + List allFields = getAllFields(event.getClass()); + return allFields.stream() + .filter(field -> CONVENTIONAL_ID_NAMES.stream() + .anyMatch(name -> field.getName().equalsIgnoreCase(name))) + .findFirst(); + } + + private static List getAllFields(Class type) { + List fields = new ArrayList<>(); + Class currentClass = type; + + while (currentClass != null && currentClass != Object.class) { + fields.addAll(List.of(currentClass.getDeclaredFields())); + currentClass = currentClass.getSuperclass(); + } + + return fields; + } + + private static String extractFieldValue(Field field, DomainEvent event) { + try { + ReflectionUtils.makeAccessible(field); + Object value = field.get(event); + return value != null ? value.toString() : null; + } catch (IllegalAccessException e) { + log.error("Could not access field: {} in event: {}", + field.getName(), event.getClass().getSimpleName(), e); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java index 75a18f5..78a3c61 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java @@ -1,8 +1,6 @@ package com.zenfulcode.commercify.shared.infrastructure.service; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.zenfulcode.commercify.shared.domain.event.AggregateReference; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.exception.EventDeserializationException; import com.zenfulcode.commercify.shared.domain.exception.EventSerializationException; @@ -19,15 +17,16 @@ public class EventSerializer { public StoredEvent serialize(DomainEvent event) { try { String eventData = objectMapper.writeValueAsString(event); + return new StoredEvent( event.getEventId(), event.getClass().getName(), eventData, event.getOccurredOn(), - getAggregateId(event), - getAggregateType(event) + extractAggregateId(event), + extractAggregateType(event) ); - } catch (JsonProcessingException e) { + } catch (Exception e) { throw new EventSerializationException("Failed to serialize event", e); } } @@ -35,23 +34,18 @@ public StoredEvent serialize(DomainEvent event) { public DomainEvent deserialize(StoredEvent storedEvent) { try { Class eventClass = eventTypeResolver.resolveEventClass(storedEvent.getEventType()); - return (DomainEvent) objectMapper.readValue( - storedEvent.getEventData(), - eventClass - ); + return (DomainEvent) objectMapper.readValue(storedEvent.getEventData(), eventClass); } catch (Exception e) { throw new EventDeserializationException( - "Failed to deserialize event: " + storedEvent.getEventType(), - e - ); + "Failed to deserialize event: " + storedEvent.getEventType(), e); } } - private String getAggregateId(DomainEvent event) { - return AggregateReference.extractId(event); + private String extractAggregateId(DomainEvent event) { + return AggregateReferenceExtractor.extractAggregateId(event); } - private String getAggregateType(DomainEvent event) { - return AggregateReference.extractType(event); + private String extractAggregateType(DomainEvent event) { + return AggregateReferenceExtractor.extractAggregateType(event); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java index f04f839..79e6348 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java @@ -4,11 +4,9 @@ import com.zenfulcode.commercify.user.application.command.CreateUserCommand; import com.zenfulcode.commercify.user.application.command.UpdateUserCommand; import com.zenfulcode.commercify.user.application.command.UpdateUserStatusCommand; -import com.zenfulcode.commercify.user.domain.exception.UserNotFoundException; import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.model.UserRole; import com.zenfulcode.commercify.user.domain.model.UserStatus; -import com.zenfulcode.commercify.user.domain.repository.UserRepository; import com.zenfulcode.commercify.user.domain.service.UserDomainService; import com.zenfulcode.commercify.user.domain.valueobject.UserId; import com.zenfulcode.commercify.user.domain.valueobject.UserSpecification; @@ -25,7 +23,6 @@ @RequiredArgsConstructor public class UserApplicationService { private final UserDomainService userDomainService; - private final UserRepository userRepository; private final DomainEventPublisher eventPublisher; private final PasswordEncoder passwordEncoder; @@ -50,13 +47,9 @@ public UserId createUser(CreateUserCommand command) { // Create the user through domain service User user = userDomainService.createUser(userSpecification); - // Save the user - User savedUser = userRepository.save(user); - // Publish domain events eventPublisher.publish(user.getDomainEvents()); - - return savedUser.getId(); + return user.getId(); } @Transactional @@ -81,9 +74,6 @@ public void registerUser( // Create user through domain service User user = userDomainService.createUser(spec); - // Save user - userRepository.save(user); - // Publish domain event eventPublisher.publish(user.getDomainEvents()); } @@ -94,15 +84,11 @@ public void registerUser( @Transactional public void updateUser(UpdateUserCommand command) { // Retrieve user - User user = userRepository.findById(command.userId()) - .orElseThrow(() -> new UserNotFoundException(command.userId())); + User user = getUser(command.userId()); // Update through domain service userDomainService.updateUserInfo(user, command.userSpec()); - // Save changes - userRepository.save(user); - // Publish events eventPublisher.publish(user.getDomainEvents()); } @@ -112,11 +98,9 @@ public void updateUser(UpdateUserCommand command) { */ @Transactional public void updateUserStatus(UpdateUserStatusCommand command) { - User user = userRepository.findById(command.userId()) - .orElseThrow(() -> new UserNotFoundException(command.userId())); + User user = userDomainService.getUserById(command.userId()); userDomainService.updateUserStatus(user, command.newStatus()); - userRepository.save(user); eventPublisher.publish(user.getDomainEvents()); } @@ -141,8 +125,7 @@ public void deactivateUser(UserId userId) { */ @Transactional(readOnly = true) public User getUser(UserId userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new UserNotFoundException(userId)); + return userDomainService.getUserById(userId); } /** @@ -150,8 +133,7 @@ public User getUser(UserId userId) { */ @Transactional(readOnly = true) public User getUserByEmail(String email) { - return userRepository.findByEmail(email) - .orElseThrow(() -> new UserNotFoundException("User not found with email: " + email)); + return userDomainService.getUserByEmail(email); } /** @@ -159,7 +141,7 @@ public User getUserByEmail(String email) { */ @Transactional(readOnly = true) public Page getAllUsers(Pageable pageable) { - return userRepository.findAll(pageable); + return userDomainService.getAllUsers(pageable); } /** @@ -167,7 +149,7 @@ public Page getAllUsers(Pageable pageable) { */ @Transactional(readOnly = true) public Page getActiveUsers(Pageable pageable) { - return userRepository.findByStatus(UserStatus.ACTIVE, pageable); + return userDomainService.getUsersByStatus(UserStatus.ACTIVE, pageable); } /** @@ -175,7 +157,7 @@ public Page getActiveUsers(Pageable pageable) { */ @Transactional(readOnly = true) public boolean emailExists(String email) { - return userRepository.existsByEmail(email); + return userDomainService.emailExists(email); } /** @@ -194,7 +176,6 @@ public void changePassword(UserId userId, String currentPassword, String newPass String hashedPassword = passwordEncoder.encode(newPassword); userDomainService.changePassword(user, hashedPassword); - userRepository.save(user); eventPublisher.publish(user.getDomainEvents()); } @@ -208,7 +189,6 @@ public void resetPassword(UserId userId, String newPassword) { String hashedPassword = passwordEncoder.encode(newPassword); userDomainService.changePassword(user, hashedPassword); - userRepository.save(user); eventPublisher.publish(user.getDomainEvents()); } } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java index 4b8858b..8a25365 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java @@ -13,7 +13,8 @@ public class UserCreatedEvent extends DomainEvent { private final String email; private final UserStatus status; - public UserCreatedEvent(UserId userId, String email, UserStatus status) { + public UserCreatedEvent(Object source, UserId userId, String email, UserStatus status) { + super(source); this.userId = userId; this.email = email; this.status = status; diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java index 03c7018..d61ce64 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java @@ -17,10 +17,12 @@ public class UserStatusChangedEvent extends DomainEvent { private final Instant changedAt; public UserStatusChangedEvent( + Object source, UserId userId, UserStatus oldStatus, UserStatus newStatus ) { + super(source); this.userId = userId; this.oldStatus = oldStatus; this.newStatus = newStatus; diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java index c32d23d..b4b8f91 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java @@ -13,8 +13,8 @@ public UserNotFoundException(UserId userId) { this.identifier = userId; } - public UserNotFoundException(String message) { - super(message); - this.identifier = null; + public UserNotFoundException(String message, String identifier) { + super(message + ": " + identifier); + this.identifier = identifier; } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java index d376ae3..08cd206 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -90,6 +90,7 @@ public static User create( // Register domain event user.registerEvent(new UserCreatedEvent( + user, user.getId(), user.getEmail(), user.getStatus() @@ -125,6 +126,7 @@ public void updateStatus(UserStatus newStatus) { this.status = newStatus; registerEvent(new UserStatusChangedEvent( + this, this.id, oldStatus, newStatus diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java index aa3dc93..b075309 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java @@ -1,13 +1,16 @@ package com.zenfulcode.commercify.user.domain.service; -import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import com.zenfulcode.commercify.user.domain.exception.UserAlreadyExistsException; +import com.zenfulcode.commercify.user.domain.exception.UserNotFoundException; import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.model.UserStatus; import com.zenfulcode.commercify.user.domain.repository.UserRepository; import com.zenfulcode.commercify.user.domain.valueobject.UserDeletionValidation; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; import com.zenfulcode.commercify.user.domain.valueobject.UserSpecification; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -18,7 +21,6 @@ @RequiredArgsConstructor public class UserDomainService { private final UserValidationService validationService; - private final DomainEventPublisher eventPublisher; private final PasswordEncoder passwordEncoder; private final UserRepository userRepository; @@ -44,9 +46,8 @@ public User createUser(UserSpecification spec) { // Validate user validationService.validateCreateUser(user); - eventPublisher.publish(user.getDomainEvents()); - - return user; + // Save user + return userRepository.save(user); } /** @@ -62,8 +63,7 @@ public void updateUserStatus(User user, UserStatus newStatus) { } user.updateStatus(newStatus); - - eventPublisher.publish(user.getDomainEvents()); + userRepository.save(user); } /** @@ -85,6 +85,7 @@ public void updateUserInfo(User user, UserSpecification updateSpec) { // Validate updated user validationService.validateUpdateUser(user); + userRepository.save(user); } /** @@ -96,8 +97,7 @@ public void changePassword(User user, String newPassword) { // Update password user.updatePassword(passwordEncoder.encode(newPassword)); - - eventPublisher.publish(user.getDomainEvents()); + userRepository.save(user); } /** @@ -139,4 +139,26 @@ public void reactivateUser(User user) { } updateUserStatus(user, UserStatus.ACTIVE); } + + public User getUserById(UserId userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + } + + public User getUserByEmail(String email) { + return userRepository.findByEmail(email) + .orElseThrow(() -> new UserNotFoundException("User not found with email", email)); + } + + public Page getAllUsers(Pageable pageable) { + return userRepository.findAll(pageable); + } + + public Page getUsersByStatus(UserStatus userStatus, Pageable pageable) { + return userRepository.findByStatus(userStatus, pageable); + } + + public boolean emailExists(String email) { + return userRepository.existsByEmail(email); + } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e588514..b4e5d94 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -33,4 +33,4 @@ spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true # Application Configuration -#logging.level.org.springframework.security=debug \ No newline at end of file +logging.level.org.springframework.security=debug \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index d2e7805..5abd156 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -6,4 +6,5 @@ + \ No newline at end of file diff --git a/src/main/resources/db/changelog/migrations/250201235523-changelog.sql b/src/main/resources/db/changelog/migrations/250201235523-changelog.sql new file mode 100644 index 0000000..d52ebc2 --- /dev/null +++ b/src/main/resources/db/changelog/migrations/250201235523-changelog.sql @@ -0,0 +1,50 @@ +-- liquibase formatted sql + +-- changeset gkhaavik:1738450523221-1 +ALTER TABLE payments + MODIFY amount DECIMAL; + +-- changeset gkhaavik:1738450523221-3 +ALTER TABLE payments + MODIFY payment_method VARCHAR(255); + +-- changeset gkhaavik:1738450523221-5 +ALTER TABLE payments + MODIFY payment_provider VARCHAR(255); + +-- changeset gkhaavik:1738450523221-6 +ALTER TABLE orders + MODIFY shipping_cost DECIMAL; + +-- changeset gkhaavik:1738450523221-8 +ALTER TABLE orders + MODIFY status VARCHAR(255); + +-- changeset gkhaavik:1738450523221-10 +ALTER TABLE payments + MODIFY status VARCHAR(255); + +-- changeset gkhaavik:1738450523221-11 +ALTER TABLE orders + MODIFY subtotal DECIMAL; + +-- changeset gkhaavik:1738450523221-12 +ALTER TABLE orders + MODIFY tax DECIMAL; + +-- changeset gkhaavik:1738450523221-13 +ALTER TABLE orders + MODIFY total_amount DECIMAL; + +-- changeset gkhaavik:1738450523221-14 +ALTER TABLE order_lines + MODIFY unit_price DECIMAL; + +-- changeset gkhaavik:1738450523221-15 +ALTER TABLE product_variants + MODIFY unit_price DECIMAL; + +-- changeset gkhaavik:1738450523221-16 +ALTER TABLE products + MODIFY unit_price DECIMAL; + From 15e36af1b15584ee9ddfe8a2601a6b294be0181f Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 2 Feb 2025 01:40:00 +0100 Subject: [PATCH 33/57] Adding even types Fixing validateOrderForPayment method Updating order status whenever payment gets reserved or captured --- .../commercify/api/order/OrderController.java | 18 --- .../domain/event/UserAuthenticatedEvent.java | 5 + .../service/OrderApplicationService.java | 25 ++-- .../domain/service/OrderDomainService.java | 22 +++- .../service/OrderValidationService.java | 16 --- .../events/PaymentEventHandler.java | 28 ++++- .../domain/event/IssuedRefundEvent.java | 5 + .../domain/event/PaymentCancelledEvent.java | 5 + .../domain/event/PaymentCapturedEvent.java | 5 + .../domain/event/PaymentCreatedEvent.java | 5 + .../domain/event/PaymentFailedEvent.java | 5 + .../domain/event/PaymentReservedEvent.java | 4 + .../event/PaymentStatusChangedEvent.java | 5 + .../service/PaymentValidationService.java | 2 +- .../service/ProductApplicationService.java | 19 +++- .../domain/event/LargeStockIncreaseEvent.java | 5 + .../product/domain/event/LowStockEvent.java | 4 + .../domain/event/ProductCreatedEvent.java | 5 + .../event/ProductPriceUpdatedEvent.java | 5 + .../domain/event/StockCorrectionEvent.java | 5 + .../AggregateReference.java | 3 +- .../service/AggregateReferenceExtractor.java | 107 ------------------ .../service/EventSerializer.java | 5 +- .../exception/GlobalExceptionHandler.java | 19 ++-- .../user/domain/event/UserCreatedEvent.java | 5 + .../domain/event/UserStatusChangedEvent.java | 5 + .../commercify/user/domain/model/User.java | 3 +- src/main/resources/application.properties | 2 +- 28 files changed, 160 insertions(+), 182 deletions(-) rename src/main/java/com/zenfulcode/commercify/shared/domain/{event => service}/AggregateReference.java (94%) delete mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/AggregateReferenceExtractor.java diff --git a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java index 6c40b9e..922aa13 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java @@ -1,20 +1,17 @@ package com.zenfulcode.commercify.api.order; import com.zenfulcode.commercify.api.order.dto.request.CreateOrderRequest; -import com.zenfulcode.commercify.api.order.dto.request.UpdateOrderStatusRequest; import com.zenfulcode.commercify.api.order.dto.response.CreateOrderResponse; import com.zenfulcode.commercify.api.order.dto.response.OrderDetailsResponse; import com.zenfulcode.commercify.api.order.dto.response.PagedOrderResponse; import com.zenfulcode.commercify.api.order.mapper.OrderDtoMapper; import com.zenfulcode.commercify.order.application.command.CancelOrderCommand; import com.zenfulcode.commercify.order.application.command.CreateOrderCommand; -import com.zenfulcode.commercify.order.application.command.UpdateOrderStatusCommand; import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; import com.zenfulcode.commercify.order.application.service.OrderApplicationService; import com.zenfulcode.commercify.order.domain.model.Order; -import com.zenfulcode.commercify.order.domain.model.OrderStatus; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; import com.zenfulcode.commercify.user.domain.valueobject.UserId; @@ -83,21 +80,6 @@ public ResponseEntity> getAllOrders( return ResponseEntity.ok(ApiResponse.success(response)); } - @PutMapping("/{orderId}/status") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity> updateOrderStatus( - @PathVariable String orderId, - @RequestBody UpdateOrderStatusRequest request) { - - UpdateOrderStatusCommand command = new UpdateOrderStatusCommand( - OrderId.of(orderId), - OrderStatus.valueOf(request.status()) - ); - - orderApplicationService.updateOrderStatus(command); - return ResponseEntity.ok(ApiResponse.success("Order status updated successfully")); - } - @DeleteMapping("/{orderId}") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> cancelOrder(@PathVariable String orderId) { diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java b/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java index 15f3caa..7b3273a 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java @@ -16,4 +16,9 @@ public UserAuthenticatedEvent(Object source, UserId userId, String username) { this.userId = userId; this.username = username; } + + @Override + public String getEventType() { + return "USER_AUTHENTICATED"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java index 1d66c9e..924f45a 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java @@ -6,17 +6,15 @@ import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; -import com.zenfulcode.commercify.order.domain.exception.OrderNotFoundException; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.order.domain.model.OrderStatus; -import com.zenfulcode.commercify.order.domain.repository.OrderRepository; import com.zenfulcode.commercify.order.domain.service.OrderDomainService; import com.zenfulcode.commercify.order.domain.valueobject.OrderDetails; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.order.domain.valueobject.OrderLineDetails; +import com.zenfulcode.commercify.product.application.service.ProductApplicationService; import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.model.ProductVariant; -import com.zenfulcode.commercify.product.domain.repository.ProductRepository; import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.product.domain.valueobject.VariantId; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; @@ -34,9 +32,8 @@ @RequiredArgsConstructor public class OrderApplicationService { private final OrderDomainService orderDomainService; - private final OrderRepository orderRepository; - private final ProductRepository productRepository; private final DomainEventPublisher eventPublisher; + private final ProductApplicationService productApplicationService; @Transactional public OrderId createOrder(CreateOrderCommand command) { @@ -46,7 +43,7 @@ public OrderId createOrder(CreateOrderCommand command) { .map(OrderLineDetails::productId) .collect(Collectors.toList()); - List products = productRepository.findAllById(productIds); + List products = productApplicationService.findAllProducts(productIds); List variantIds = command.orderLines() .stream() @@ -54,7 +51,7 @@ public OrderId createOrder(CreateOrderCommand command) { .filter(Objects::nonNull) .collect(Collectors.toList()); - List variants = productRepository.findVariantsByIds(variantIds); + List variants = productApplicationService.findVariantsByIds(variantIds); // Create order through domain service Order order = orderDomainService.createOrder( @@ -88,20 +85,19 @@ public void updateOrderStatus(UpdateOrderStatusCommand command) { @Transactional public void cancelOrder(CancelOrderCommand command) { - Order order = orderRepository.findById(command.orderId()) - .orElseThrow(() -> new OrderNotFoundException(command.orderId())); - + Order order = orderDomainService.getOrderById(command.orderId()); orderDomainService.updateOrderStatus(order, OrderStatus.CANCELLED); + eventPublisher.publish(order.getDomainEvents()); } @Transactional(readOnly = true) public Page findOrdersByUserId(FindOrdersByUserIdQuery query) { - return orderRepository.findByUserId(query.userId(), query.pageRequest()); + return orderDomainService.findOrdersByUserId(query); } @Transactional(readOnly = true) public Page findAllOrders(FindAllOrdersQuery query) { - return orderRepository.findAll(query.pageRequest()); + return orderDomainService.findAllOrders(query); } @Transactional(readOnly = true) @@ -112,12 +108,11 @@ public OrderDetailsDTO getOrderDetailsById(OrderId orderId) { @Transactional(readOnly = true) public Order getOrderById(OrderId orderId) { - return orderRepository.findById(orderId) - .orElseThrow(() -> new OrderNotFoundException(orderId)); + return orderDomainService.getOrderById(orderId); } @Transactional(readOnly = true) public boolean isOrderOwnedByUser(OrderId orderId, UserId userId) { - return orderRepository.existsByIdAndUserId(orderId, userId); + return orderDomainService.isOrderOwnedByUser(orderId, userId); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index 47aa02b..56a1698 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -1,5 +1,7 @@ package com.zenfulcode.commercify.order.domain.service; +import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; +import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; import com.zenfulcode.commercify.order.domain.exception.OrderNotFoundException; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.order.domain.model.OrderLine; @@ -15,7 +17,9 @@ import com.zenfulcode.commercify.shared.domain.model.Money; import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.service.UserDomainService; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import java.util.List; @@ -110,12 +114,6 @@ public void updateOrderStatus(Order order, OrderStatus newStatus) { // Using validationService for status transition validation validationService.validateStatusTransition(order, newStatus); - if (newStatus == OrderStatus.CANCELLED) { - validationService.validateOrderCancellation(order); - } else if (newStatus == OrderStatus.COMPLETED) { - validationService.validateOrderCompletion(order); - } - order.updateStatus(newStatus); orderRepository.save(order); } @@ -124,4 +122,16 @@ public Order getOrderById(OrderId orderId) { return orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); } + + public Page findOrdersByUserId(FindOrdersByUserIdQuery query) { + return orderRepository.findByUserId(query.userId(), query.pageRequest()); + } + + public Page findAllOrders(FindAllOrdersQuery query) { + return orderRepository.findAll(query.pageRequest()); + } + + public boolean isOrderOwnedByUser(OrderId orderId, UserId userId) { + return orderRepository.existsByIdAndUserId(orderId, userId); + } } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java index 7fb5c0a..53ed349 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java @@ -74,22 +74,6 @@ public void validateStatusTransition(Order order, OrderStatus newStatus) { } } - public void validateOrderCancellation(Order order) { - if (order.isInTerminalState(stateFlow)) { - throw new OrderValidationException( - "Cannot cancel order in terminal status: " + order.getStatus() - ); - } - } - - public void validateOrderCompletion(Order order) { - if (order.getStatus() != OrderStatus.SHIPPED) { - throw new OrderValidationException( - "Cannot complete order that hasn't been shipped" - ); - } - } - public void validateStock(Product product, int requestedQuantity) { if (!product.hasEnoughStock(requestedQuantity)) { throw new InsufficientStockException( diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java b/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java index 0f821a7..ada4294 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java @@ -1,9 +1,12 @@ package com.zenfulcode.commercify.payment.application.events; +import com.zenfulcode.commercify.order.application.command.CancelOrderCommand; import com.zenfulcode.commercify.order.application.command.UpdateOrderStatusCommand; import com.zenfulcode.commercify.order.application.service.OrderApplicationService; import com.zenfulcode.commercify.order.domain.model.OrderStatus; import com.zenfulcode.commercify.payment.domain.event.PaymentCancelledEvent; +import com.zenfulcode.commercify.payment.domain.event.PaymentCapturedEvent; +import com.zenfulcode.commercify.payment.domain.event.PaymentReservedEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; @@ -21,9 +24,32 @@ public class PaymentEventHandler { public void handlePaymentCancelled(PaymentCancelledEvent event) { log.info("Handling payment cancelled event for orderId: {}", event.getOrderId()); + CancelOrderCommand command = new CancelOrderCommand(event.getOrderId()); + + orderApplicationService.cancelOrder(command); + } + + @EventListener + @Transactional + public void handlePaymentCaptured(PaymentCapturedEvent event) { + log.info("Handling payment captured event for orderId: {}", event.getOrderId()); + + UpdateOrderStatusCommand command = new UpdateOrderStatusCommand( + event.getOrderId(), + OrderStatus.COMPLETED + ); + + orderApplicationService.updateOrderStatus(command); + } + + @EventListener + @Transactional + public void handlePaymentReserved(PaymentReservedEvent event) { + log.info("Handling payment reserved event for orderId: {}", event.getOrderId()); + UpdateOrderStatusCommand command = new UpdateOrderStatusCommand( event.getOrderId(), - OrderStatus.CANCELLED + OrderStatus.PAID ); orderApplicationService.updateOrderStatus(command); diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java index 77f5cad..4a57c2f 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/IssuedRefundEvent.java @@ -43,4 +43,9 @@ public IssuedRefundEvent( this.isFullRefund = isFullRefund; this.refundedAt = Instant.now(); } + + @Override + public String getEventType() { + return "ISSUED_REFUND"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java index 985e8e0..f3da3c1 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCancelledEvent.java @@ -32,4 +32,9 @@ public PaymentCancelledEvent( this.reason = reason; this.cancelledAt = Instant.now(); } + + @Override + public String getEventType() { + return "PAYMENT_CANCELLED"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java index 0f50f74..a6c6cb6 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCapturedEvent.java @@ -37,4 +37,9 @@ public PaymentCapturedEvent( this.transactionId = transactionId; this.capturedAt = Instant.now(); } + + @Override + public String getEventType() { + return "PAYMENT_CAPTURED"; + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java index c783770..0c26f5c 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentCreatedEvent.java @@ -41,4 +41,9 @@ public PaymentCreatedEvent( this.provider = provider; this.createdAt = Instant.now(); } + + @Override + public String getEventType() { + return "PAYMENT_CREATED"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java index ed2d6e4..2f24761 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java @@ -32,4 +32,9 @@ public PaymentFailedEvent( this.errorMessage = errorMessage; this.failedAt = Instant.now(); } + + @Override + public String getEventType() { + return "PAYMENT_FAILED"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java index 6127a3f..d534913 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentReservedEvent.java @@ -19,4 +19,8 @@ public PaymentReservedEvent(Object source, PaymentId paymentId, OrderId orderId, this.transactionId = transactionId; } + @Override + public String getEventType() { + return "PAYMENT_RESERVED"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java index f713c20..c47d77b 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentStatusChangedEvent.java @@ -49,4 +49,9 @@ public boolean isCompletedTransition() { public boolean isFailedTransition() { return newStatus == PaymentStatus.FAILED; } + + @Override + public String getEventType() { + return "PAYMENT_STATUS_CHANGED"; + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java index ac448b6..0928b5c 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java @@ -127,7 +127,7 @@ private void validateOrderForPayment(Order order, List violations) { } // Validate order status allows payment - if (orderStateFlow.canTransition(order.getStatus(), OrderStatus.PAID)) { + if (!orderStateFlow.canTransition(order.getStatus(), OrderStatus.PAID)) { violations.add("Order status does not allow payment"); } } diff --git a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java index 8f98af0..f6b2bf6 100644 --- a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java @@ -5,12 +5,10 @@ import com.zenfulcode.commercify.product.domain.exception.ProductDeletionException; import com.zenfulcode.commercify.product.domain.exception.ProductNotFoundException; import com.zenfulcode.commercify.product.domain.model.Product; +import com.zenfulcode.commercify.product.domain.model.ProductVariant; import com.zenfulcode.commercify.product.domain.repository.ProductRepository; import com.zenfulcode.commercify.product.domain.service.ProductDomainService; -import com.zenfulcode.commercify.product.domain.valueobject.InventoryAdjustment; -import com.zenfulcode.commercify.product.domain.valueobject.ProductDeletionValidation; -import com.zenfulcode.commercify.product.domain.valueobject.ProductId; -import com.zenfulcode.commercify.product.domain.valueobject.ProductSpecification; +import com.zenfulcode.commercify.product.domain.valueobject.*; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -18,6 +16,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collection; +import java.util.List; + @Service @RequiredArgsConstructor public class ProductApplicationService { @@ -149,6 +150,11 @@ public void deleteProduct(DeleteProductCommand command) { eventPublisher.publish(product.getDomainEvents()); } + @Transactional(readOnly = true) + public List findAllProducts(Collection productIds) { + return productRepository.findAllById(productIds); + } + /** * Queries for products */ @@ -167,4 +173,9 @@ public Product getProduct(ProductId productId) { return productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException(productId)); } + + @Transactional(readOnly = true) + public List findVariantsByIds(List variantIds) { + return productRepository.findVariantsByIds(variantIds); + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java index 958a195..3ef868d 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/LargeStockIncreaseEvent.java @@ -18,4 +18,9 @@ public LargeStockIncreaseEvent(Object source, ProductId productId, int quantity, this.quantity = quantity; this.reason = reason; } + + @Override + public String getEventType() { + return "LARGE_STOCK_INCREASE"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java index b145b96..9640a27 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/LowStockEvent.java @@ -17,4 +17,8 @@ public LowStockEvent(Object source, ProductId productId, int stockAmount) { this.stockAmount = stockAmount; } + @Override + public String getEventType() { + return "LOW_STOCK"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java index 620cf7e..f93ae35 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductCreatedEvent.java @@ -19,4 +19,9 @@ public ProductCreatedEvent(Object source, ProductId productId, String name, Mone this.name = name; this.price = price; } + + @Override + public String getEventType() { + return "PRODUCT_CREATED"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java index fc12ba9..9abb48f 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/ProductPriceUpdatedEvent.java @@ -18,4 +18,9 @@ public ProductPriceUpdatedEvent(Object source, ProductId productId, Money newPri this.productId = productId; this.newPrice = newPrice; } + + @Override + public String getEventType() { + return "PRODUCT_PRICE_UPDATED"; + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java b/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java index d984fb9..cd895af 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/event/StockCorrectionEvent.java @@ -18,4 +18,9 @@ public StockCorrectionEvent(Object source, ProductId productId, int quantity, St this.quantity = quantity; this.reason = reason; } + + @Override + public String getEventType() { + return "STOCK_CORRECTION"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/event/AggregateReference.java b/src/main/java/com/zenfulcode/commercify/shared/domain/service/AggregateReference.java similarity index 94% rename from src/main/java/com/zenfulcode/commercify/shared/domain/event/AggregateReference.java rename to src/main/java/com/zenfulcode/commercify/shared/domain/service/AggregateReference.java index d7c0850..b4ccc28 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/domain/event/AggregateReference.java +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/service/AggregateReference.java @@ -1,5 +1,6 @@ -package com.zenfulcode.commercify.shared.domain.event; +package com.zenfulcode.commercify.shared.domain.service; +import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.model.AggregateRoot; import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/AggregateReferenceExtractor.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/AggregateReferenceExtractor.java deleted file mode 100644 index cfb87c4..0000000 --- a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/AggregateReferenceExtractor.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.zenfulcode.commercify.shared.infrastructure.service; - -import com.zenfulcode.commercify.shared.domain.event.DomainEvent; -import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.ReflectionUtils; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Slf4j -public class AggregateReferenceExtractor { - - private static final List CONVENTIONAL_ID_NAMES = List.of( - "aggregateId", - "entityId", - "orderId", - "paymentId", - "productId", - "userId" - ); - - private static final List EVENT_SUFFIXES = List.of( - "Event", - "Created", - "Updated", - "Deleted", - "Changed", - "Cancelled", - "Completed", - "Started", - "Finished", - "Failed" - ); - - public static String extractAggregateId(DomainEvent event) { - // First try fields annotated with @AggregateId - Optional annotatedField = findAnnotatedIdField(event); - if (annotatedField.isPresent()) { - return extractFieldValue(annotatedField.get(), event); - } - - // Then try conventional ID field names - Optional conventionalField = findConventionalIdField(event); - if (conventionalField.isPresent()) { - return extractFieldValue(conventionalField.get(), event); - } - - // If no ID field is found, log a warning and use event ID - log.warn("No aggregate ID field found for event: {}", event.getClass().getSimpleName()); - return event.getEventId(); - } - - public static String extractAggregateType(DomainEvent event) { - // Remove common event-related suffixes - String aggregateType = event.getClass().getSimpleName(); - for (String suffix : EVENT_SUFFIXES) { - if (aggregateType.endsWith(suffix)) { - aggregateType = aggregateType.substring(0, - aggregateType.length() - suffix.length()); - } - } - - return aggregateType; - } - - private static Optional findAnnotatedIdField(DomainEvent event) { - List allFields = getAllFields(event.getClass()); - return allFields.stream() - .filter(field -> field.isAnnotationPresent(AggregateId.class)) - .findFirst(); - } - - private static Optional findConventionalIdField(DomainEvent event) { - List allFields = getAllFields(event.getClass()); - return allFields.stream() - .filter(field -> CONVENTIONAL_ID_NAMES.stream() - .anyMatch(name -> field.getName().equalsIgnoreCase(name))) - .findFirst(); - } - - private static List getAllFields(Class type) { - List fields = new ArrayList<>(); - Class currentClass = type; - - while (currentClass != null && currentClass != Object.class) { - fields.addAll(List.of(currentClass.getDeclaredFields())); - currentClass = currentClass.getSuperclass(); - } - - return fields; - } - - private static String extractFieldValue(Field field, DomainEvent event) { - try { - ReflectionUtils.makeAccessible(field); - Object value = field.get(event); - return value != null ? value.toString() : null; - } catch (IllegalAccessException e) { - log.error("Could not access field: {} in event: {}", - field.getName(), event.getClass().getSimpleName(), e); - return null; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java index 78a3c61..837a0a4 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EventSerializer.java @@ -5,6 +5,7 @@ import com.zenfulcode.commercify.shared.domain.exception.EventDeserializationException; import com.zenfulcode.commercify.shared.domain.exception.EventSerializationException; import com.zenfulcode.commercify.shared.domain.model.StoredEvent; +import com.zenfulcode.commercify.shared.domain.service.AggregateReference; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -42,10 +43,10 @@ public DomainEvent deserialize(StoredEvent storedEvent) { } private String extractAggregateId(DomainEvent event) { - return AggregateReferenceExtractor.extractAggregateId(event); + return AggregateReference.extractId(event); } private String extractAggregateType(DomainEvent event) { - return AggregateReferenceExtractor.extractAggregateType(event); + return AggregateReference.extractType(event); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java b/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java index 3ee44d5..a6b510c 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java @@ -1,10 +1,10 @@ package com.zenfulcode.commercify.shared.interfaces.rest.exception; import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import com.zenfulcode.commercify.shared.domain.exception.DomainValidationException; import com.zenfulcode.commercify.shared.domain.exception.EntityNotFoundException; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -25,16 +25,17 @@ public ResponseEntity> handleDomainException(DomainException e return ResponseEntity.badRequest().body(response); } - @ExceptionHandler(MethodArgumentNotValidException.class) + @ExceptionHandler(DomainValidationException.class) public ResponseEntity> handleValidationException( - MethodArgumentNotValidException ex) { - List validationErrors = ex.getBindingResult() - .getFieldErrors() + DomainValidationException ex) { + List validationErrors = ex.getViolations() .stream() - .map(error -> new ApiResponse.ValidationError( - error.getField(), - error.getDefaultMessage() - )) + .map(error -> + new ApiResponse.ValidationError( + error, + "VALIDATION_ERROR" + ) + ) .collect(Collectors.toList()); ApiResponse response = ApiResponse.validationError(validationErrors); diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java index 8a25365..1207cce 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserCreatedEvent.java @@ -19,4 +19,9 @@ public UserCreatedEvent(Object source, UserId userId, String email, UserStatus s this.email = email; this.status = status; } + + @Override + public String getEventType() { + return "USER_CREATED"; + } } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java index d61ce64..3807827 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/event/UserStatusChangedEvent.java @@ -40,4 +40,9 @@ public boolean isSuspensionTransition() { public boolean isActivationTransition() { return newStatus == UserStatus.ACTIVE; } + + @Override + public String getEventType() { + return "USER_STATUS_CHANGED"; + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java index 08cd206..3f144a6 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -161,7 +161,8 @@ public boolean hasActiveOrders() { .anyMatch(order -> { OrderStatus status = order.getStatus(); return status == OrderStatus.PENDING || - status == OrderStatus.CONFIRMED || + status == OrderStatus.COMPLETED || + status == OrderStatus.PAID || status == OrderStatus.SHIPPED; }); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b4e5d94..e588514 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -33,4 +33,4 @@ spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true # Application Configuration -logging.level.org.springframework.security=debug \ No newline at end of file +#logging.level.org.springframework.security=debug \ No newline at end of file From 21ecd5c323a61165948019bd7ddcd227df7653e7 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 2 Feb 2025 01:41:45 +0100 Subject: [PATCH 34/57] Rethinking order and payment status's and their flow --- .../order/domain/model/OrderStatus.java | 16 +++++------ .../order/domain/service/OrderStateFlow.java | 23 +++++++++------- .../domain/service/PaymentStateFlow.java | 11 +++----- .../domain/valueobject/PaymentStatus.java | 27 +++++++------------ 4 files changed, 32 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java index 9561a1c..c331c7b 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java @@ -1,13 +1,11 @@ package com.zenfulcode.commercify.order.domain.model; public enum OrderStatus { - PENDING, // Order has been created but not yet confirmed - CONFIRMED, // Order has been confirmed by the customer - SHIPPED, // Order has been shipped - PAID, // Order has been paid - COMPLETED, // Order has been delivered - CANCELLED, // Order has been cancelled - FAILED, // Order has failed - REFUNDED, // Order has been refunded - RETURNED // Order has been returned + PENDING, // Order has been created + ABANDONED, // Order has been abandoned + PAID, // Order has been paid + SHIPPED, // Order has been shipped + COMPLETED, // Order has been delivered + CANCELLED, // Order has been cancelled + REFUNDED // Order has been refunded/returned } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderStateFlow.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderStateFlow.java index 799c50f..8765a0b 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderStateFlow.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderStateFlow.java @@ -13,28 +13,31 @@ public class OrderStateFlow { public OrderStateFlow() { validTransitions = new EnumMap<>(OrderStatus.class); - // Initial state -> Confirmed or Cancelled validTransitions.put(OrderStatus.PENDING, Set.of( - OrderStatus.CONFIRMED, - OrderStatus.CANCELLED + OrderStatus.PAID, + OrderStatus.ABANDONED + )); + + validTransitions.put(OrderStatus.ABANDONED, Set.of( + OrderStatus.PENDING )); - // Confirmed -> Shipped or Cancelled - validTransitions.put(OrderStatus.CONFIRMED, Set.of( + validTransitions.put(OrderStatus.PAID, Set.of( OrderStatus.SHIPPED, + OrderStatus.COMPLETED, OrderStatus.CANCELLED )); - // Shipped -> Completed or Returned validTransitions.put(OrderStatus.SHIPPED, Set.of( - OrderStatus.COMPLETED, - OrderStatus.RETURNED + OrderStatus.COMPLETED + )); + + validTransitions.put(OrderStatus.COMPLETED, Set.of( + OrderStatus.REFUNDED )); // Terminal states - validTransitions.put(OrderStatus.COMPLETED, Set.of()); validTransitions.put(OrderStatus.CANCELLED, Set.of()); - validTransitions.put(OrderStatus.RETURNED, Set.of()); validTransitions.put(OrderStatus.REFUNDED, Set.of()); } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java index 6836a7b..829eb26 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java @@ -13,13 +13,7 @@ public class PaymentStateFlow { public PaymentStateFlow() { this.validTransitions = new EnumMap<>(PaymentStatus.class); - initializeStateTransitions(); - } - /** - * Initialize valid state transitions - */ - private void initializeStateTransitions() { // Initial state -> PENDING validTransitions.put(PaymentStatus.PENDING, Set.of( PaymentStatus.FAILED, @@ -27,7 +21,6 @@ private void initializeStateTransitions() { PaymentStatus.CANCELLED )); - // RESERVED/PAID -> RESERVED or CANCELLED validTransitions.put(PaymentStatus.RESERVED, Set.of( PaymentStatus.CAPTURED, PaymentStatus.EXPIRED, @@ -36,6 +29,7 @@ private void initializeStateTransitions() { PaymentStatus.REFUNDED )); + // TODO: Unsure about this one // CAPTURED -> REFUNDED or PARTIALLY_REFUNDED validTransitions.put(PaymentStatus.CAPTURED, Set.of( PaymentStatus.REFUNDED, @@ -59,6 +53,7 @@ private void initializeStateTransitions() { validTransitions.put(PaymentStatus.EXPIRED, Set.of()); } + /** * Check if a state transition is valid */ @@ -78,7 +73,7 @@ private Set getValidTransitions(PaymentStatus currentState) { * Check if a state is terminal */ public boolean isTerminalState(PaymentStatus state) { - return state.isTerminalState(); + return validTransitions.get(state).isEmpty(); } /** diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java index 4ed26bd..ca7613b 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/valueobject/PaymentStatus.java @@ -1,23 +1,14 @@ package com.zenfulcode.commercify.payment.domain.valueobject; -import lombok.Getter; - -@Getter public enum PaymentStatus { - PENDING(false), // Payment has been initiated but not completed - RESERVED(false), // Payment has been reserved but not captured - CAPTURED(false), // Payment has been successfully captured - FAILED(false), // Payment attempt failed - CANCELLED(true), // Payment was cancelled - REFUNDED(true), // Payment was fully refunded - PARTIALLY_REFUNDED(false), // Payment was partially refunded - EXPIRED(true), // Payment expired before completion - TERMINATED(true); // Payment was terminated by the system - - private final boolean terminalState; - - PaymentStatus(boolean terminalState) { - this.terminalState = terminalState; - } + PENDING, // Payment has been initiated but not completed + RESERVED, // Payment has been reserved but not captured + CAPTURED, // Payment has been successfully captured + FAILED, // Payment attempt failed + CANCELLED, // Payment was cancelled + REFUNDED, // Payment was fully refunded + PARTIALLY_REFUNDED, // Payment was partially refunded + EXPIRED, // Payment expired before completion + TERMINATED // Payment was terminated by the system } From 40626ce144f85d26c682c3fac6978bdd7e4e6250 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 2 Feb 2025 13:31:40 +0100 Subject: [PATCH 35/57] Refactors payment and order state flows Fixing payment capture status updates --- .../service/OrderApplicationService.java | 4 ++ .../order/domain/model/OrderStatus.java | 3 +- .../domain/service/OrderDomainService.java | 6 --- .../service/OrderValidationService.java | 16 ------ .../events/PaymentEventHandler.java | 52 +++++++++++++++++++ .../service/PaymentApplicationService.java | 10 ++-- .../domain/event/PaymentFailedEvent.java | 11 ++-- .../payment/domain/model/FailureReason.java | 22 ++++++++ .../payment/domain/model/Payment.java | 11 ++-- .../domain/service/PaymentDomainService.java | 45 ++++++++-------- .../domain/service/PaymentStateFlow.java | 11 ++-- .../service/PaymentValidationService.java | 2 +- .../provider/MobilepayProviderService.java | 21 ++++---- .../commercify/user/domain/model/User.java | 2 +- src/main/resources/application.properties | 2 +- 15 files changed, 135 insertions(+), 83 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/payment/domain/model/FailureReason.java diff --git a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java index 1d66c9e..b72ed2e 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java @@ -80,6 +80,8 @@ public OrderId createOrder(CreateOrderCommand command) { public void updateOrderStatus(UpdateOrderStatusCommand command) { Order order = orderDomainService.getOrderById(command.orderId()); + System.out.println("Updating order status to: " + command.newStatus() + " for order: " + order.getId() + " with status: " + order.getStatus()); + orderDomainService.updateOrderStatus(order, command.newStatus()); // Save and publish events @@ -92,6 +94,8 @@ public void cancelOrder(CancelOrderCommand command) { .orElseThrow(() -> new OrderNotFoundException(command.orderId())); orderDomainService.updateOrderStatus(order, OrderStatus.CANCELLED); + + eventPublisher.publish(order.getDomainEvents()); } @Transactional(readOnly = true) diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java index c331c7b..38ba20c 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderStatus.java @@ -7,5 +7,6 @@ public enum OrderStatus { SHIPPED, // Order has been shipped COMPLETED, // Order has been delivered CANCELLED, // Order has been cancelled - REFUNDED // Order has been refunded/returned + REFUNDED, // Order has been refunded/returned + FAILED // Order has failed } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index 47aa02b..6ad9483 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -110,12 +110,6 @@ public void updateOrderStatus(Order order, OrderStatus newStatus) { // Using validationService for status transition validation validationService.validateStatusTransition(order, newStatus); - if (newStatus == OrderStatus.CANCELLED) { - validationService.validateOrderCancellation(order); - } else if (newStatus == OrderStatus.COMPLETED) { - validationService.validateOrderCompletion(order); - } - order.updateStatus(newStatus); orderRepository.save(order); } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java index 7fb5c0a..53ed349 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderValidationService.java @@ -74,22 +74,6 @@ public void validateStatusTransition(Order order, OrderStatus newStatus) { } } - public void validateOrderCancellation(Order order) { - if (order.isInTerminalState(stateFlow)) { - throw new OrderValidationException( - "Cannot cancel order in terminal status: " + order.getStatus() - ); - } - } - - public void validateOrderCompletion(Order order) { - if (order.getStatus() != OrderStatus.SHIPPED) { - throw new OrderValidationException( - "Cannot complete order that hasn't been shipped" - ); - } - } - public void validateStock(Product product, int requestedQuantity) { if (!product.hasEnoughStock(requestedQuantity)) { throw new InsufficientStockException( diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java b/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java index 0f821a7..606cfeb 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java @@ -4,6 +4,10 @@ import com.zenfulcode.commercify.order.application.service.OrderApplicationService; import com.zenfulcode.commercify.order.domain.model.OrderStatus; import com.zenfulcode.commercify.payment.domain.event.PaymentCancelledEvent; +import com.zenfulcode.commercify.payment.domain.event.PaymentCapturedEvent; +import com.zenfulcode.commercify.payment.domain.event.PaymentFailedEvent; +import com.zenfulcode.commercify.payment.domain.event.PaymentReservedEvent; +import com.zenfulcode.commercify.payment.domain.model.FailureReason; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; @@ -28,4 +32,52 @@ public void handlePaymentCancelled(PaymentCancelledEvent event) { orderApplicationService.updateOrderStatus(command); } + + @EventListener + @Transactional + public void handlePaymentReserved(PaymentReservedEvent event) { + log.info("Handling payment reserved event for orderId: {}", event.getOrderId()); + + UpdateOrderStatusCommand command = new UpdateOrderStatusCommand( + event.getOrderId(), + OrderStatus.PAID + ); + + orderApplicationService.updateOrderStatus(command); + } + + @EventListener + @Transactional + public void handlePaymentFailed(PaymentFailedEvent event) { + log.info("Handling payment failed event for orderId: {}", event.getOrderId()); + + UpdateOrderStatusCommand command = new UpdateOrderStatusCommand( + event.getOrderId(), + mapFailureReasonToOrderStatus(event.getFailureReason()) + ); + + orderApplicationService.updateOrderStatus(command); + } + + @EventListener + @Transactional + public void handlePaymentCaptured(PaymentCapturedEvent event) { + log.info("Handling payment captured event for orderId: {}", event.getOrderId()); + + UpdateOrderStatusCommand command = new UpdateOrderStatusCommand( + event.getOrderId(), + OrderStatus.COMPLETED + ); + + orderApplicationService.updateOrderStatus(command); + } + + private OrderStatus mapFailureReasonToOrderStatus(FailureReason failureReason) { + return switch (failureReason) { + case INSUFFICIENT_FUNDS, INVALID_PAYMENT_METHOD, PAYMENT_PROCESSING_ERROR, PAYMENT_METHOD_ERROR, + PAYMENT_PROVIDER_ERROR -> OrderStatus.FAILED; + case PAYMENT_EXPIRED, PAYMENT_TERMINATED -> OrderStatus.ABANDONED; + case UNKNOWN -> null; + }; + } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java index 030bf19..e3bc522 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java @@ -8,7 +8,6 @@ import com.zenfulcode.commercify.payment.domain.exception.PaymentProviderNotFoundException; import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; -import com.zenfulcode.commercify.payment.domain.repository.PaymentRepository; import com.zenfulcode.commercify.payment.domain.service.PaymentDomainService; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderFactory; import com.zenfulcode.commercify.payment.domain.service.PaymentProviderService; @@ -32,7 +31,6 @@ public class PaymentApplicationService { private final PaymentProviderFactory providerFactory; private final DefaultDomainEventPublisher eventPublisher; private final WebhookHandler webhookHandler; - private final PaymentRepository paymentRepository; @Transactional public InitializedPayment initiatePayment(InitiatePaymentCommand command) { @@ -57,9 +55,7 @@ public InitializedPayment initiatePayment(InitiatePaymentCommand command) { ); // Update payment with provider reference - payment.updateProviderReference(providerResponse.providerReference()); - - paymentRepository.save(payment); + paymentDomainService.updateProviderReference(payment, providerResponse.providerReference()); // Publish events eventPublisher.publish(payment.getDomainEvents()); @@ -76,7 +72,8 @@ public InitializedPayment initiatePayment(InitiatePaymentCommand command) { public void handlePaymentCallback(PaymentProvider provider, WebhookPayload payload) { Payment payment = paymentDomainService.getPaymentByProviderReference(payload.getPaymentReference()); webhookHandler.handleWebhook(provider, payload, payment); - paymentRepository.save(payment); + + eventPublisher.publish(payment.getDomainEvents()); } // TODO: Make sure the capture currency is the same as the payment currency @@ -87,7 +84,6 @@ public CapturedPayment capturePayment(CapturePaymentCommand command) { Money captureAmount = command.captureAmount() == null ? payment.getAmount() : command.captureAmount(); paymentDomainService.capturePayment(payment, TransactionId.generate(), captureAmount); - paymentRepository.save(payment); // Publish events eventPublisher.publish(payment.getDomainEvents()); diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java index ed2d6e4..4277231 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/event/PaymentFailedEvent.java @@ -1,7 +1,9 @@ package com.zenfulcode.commercify.payment.domain.event; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; +import com.zenfulcode.commercify.payment.domain.model.FailureReason; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; +import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; import com.zenfulcode.commercify.shared.domain.event.DomainEvent; import com.zenfulcode.commercify.shared.domain.valueobject.AggregateId; import lombok.Getter; @@ -17,19 +19,22 @@ public class PaymentFailedEvent extends DomainEvent { @AggregateId private final PaymentId paymentId; private final OrderId orderId; - private final String errorMessage; + private final FailureReason failureReason; + private final PaymentStatus status; private final Instant failedAt; public PaymentFailedEvent( Object source, PaymentId paymentId, OrderId orderId, - String errorMessage + FailureReason failureReason, + PaymentStatus status ) { super(source); this.paymentId = paymentId; this.orderId = orderId; - this.errorMessage = errorMessage; + this.failureReason = failureReason; + this.status = status; this.failedAt = Instant.now(); } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/FailureReason.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/FailureReason.java new file mode 100644 index 0000000..807449c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/FailureReason.java @@ -0,0 +1,22 @@ +package com.zenfulcode.commercify.payment.domain.model; + +import lombok.Getter; + +@Getter +public enum FailureReason { + INSUFFICIENT_FUNDS("Insufficient funds"), + INVALID_PAYMENT_METHOD("Invalid payment method"), + PAYMENT_PROVIDER_ERROR("Payment provider error"), + PAYMENT_METHOD_ERROR("Payment method error"), + PAYMENT_PROCESSING_ERROR("Payment processing error"), + PAYMENT_EXPIRED("Payment expired"), + PAYMENT_TERMINATED("Payment terminated"), + UNKNOWN("Unknown"); + + private final String reason; + + FailureReason(String reason) { + this.reason = reason; + } + +} diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java index 4caa127..bb56019 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/Payment.java @@ -119,17 +119,18 @@ public void markAsCaptured(TransactionId transactionId, Money capturedAmount) { )); } - public void markAsFailed(String reason) { - this.errorMessage = reason; - recordPaymentAttempt(false, reason); + public void markAsFailed(FailureReason reason, PaymentStatus status) { + this.errorMessage = reason.getReason(); + recordPaymentAttempt(false, reason.getReason()); - updateStatus(PaymentStatus.FAILED); + updateStatus(status); registerEvent(new PaymentFailedEvent( this, this.id, order.getId(), - reason + reason, + status )); } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java index 057ec70..6647ffd 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java @@ -3,6 +3,7 @@ import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.payment.domain.event.PaymentCreatedEvent; import com.zenfulcode.commercify.payment.domain.exception.PaymentNotFoundException; +import com.zenfulcode.commercify.payment.domain.model.FailureReason; import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; @@ -11,7 +12,6 @@ import com.zenfulcode.commercify.payment.domain.valueobject.PaymentStatus; import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; import com.zenfulcode.commercify.payment.domain.valueobject.refund.RefundRequest; -import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; import com.zenfulcode.commercify.shared.domain.model.Money; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,7 +20,6 @@ @RequiredArgsConstructor public class PaymentDomainService { private final PaymentValidationService validationService; - private final DomainEventPublisher eventPublisher; private final PaymentRepository paymentRepository; /** @@ -58,25 +57,19 @@ public void capturePayment(Payment payment, TransactionId transactionId, Money c payment.markAsCaptured(transactionId, capturedAmount); - // Publish payment captured event - eventPublisher.publish(payment.getDomainEvents()); + paymentRepository.save(payment); } /** * Handles payment failures */ - public void failPayment(Payment payment, String failureReason) { - failPayment(payment, failureReason, PaymentStatus.FAILED); - } - - public void failPayment(Payment payment, String failureReason, PaymentStatus status) { + public void failPayment(Payment payment, FailureReason failureReason, PaymentStatus status) { // Validate current state validationService.validateStatusTransition(payment, status); - payment.markAsFailed(failureReason); + payment.markAsFailed(failureReason, status); - // Publish payment failed event - eventPublisher.publish(payment.getDomainEvents()); + paymentRepository.save(payment); } /** @@ -93,12 +86,11 @@ public void refundPayment(Payment payment, RefundRequest refundRequest) { refundRequest.notes() ); - // Publish refund event - eventPublisher.publish(payment.getDomainEvents()); + paymentRepository.save(payment); } /** - * Cancels a pending payment + * Cancels a reserved payment */ public void cancelPayment(Payment payment) { // Validate cancellation @@ -106,7 +98,20 @@ public void cancelPayment(Payment payment) { payment.cancel(); - eventPublisher.publish(payment.getDomainEvents()); + paymentRepository.save(payment); + } + + public void authorizePayment(Payment payment) { + validationService.validateStatusTransition(payment, PaymentStatus.RESERVED); + + payment.reserve(); + + paymentRepository.save(payment); + } + + public void updateProviderReference(Payment payment, String providerReference) { + payment.updateProviderReference(providerReference); + paymentRepository.save(payment); } /** @@ -121,12 +126,4 @@ public Payment getPaymentByProviderReference(String providerReference) { return paymentRepository.findByProviderReference(providerReference) .orElseThrow(() -> new PaymentNotFoundException(providerReference)); } - - public void authorizePayment(Payment payment) { - validationService.validateStatusTransition(payment, PaymentStatus.RESERVED); - - payment.reserve(); - - eventPublisher.publish(payment.getDomainEvents()); - } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java index 829eb26..a6a8596 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentStateFlow.java @@ -18,11 +18,12 @@ public PaymentStateFlow() { validTransitions.put(PaymentStatus.PENDING, Set.of( PaymentStatus.FAILED, PaymentStatus.RESERVED, - PaymentStatus.CANCELLED + PaymentStatus.TERMINATED )); validTransitions.put(PaymentStatus.RESERVED, Set.of( PaymentStatus.CAPTURED, + PaymentStatus.CANCELLED, PaymentStatus.EXPIRED, PaymentStatus.FAILED, PaymentStatus.PARTIALLY_REFUNDED, @@ -36,12 +37,6 @@ public PaymentStateFlow() { PaymentStatus.PARTIALLY_REFUNDED )); - // FAILED -> Can retry (go back to PENDING) or CANCELLED - validTransitions.put(PaymentStatus.FAILED, Set.of( - PaymentStatus.PENDING, - PaymentStatus.CANCELLED - )); - // PARTIALLY_REFUNDED -> REFUNDED validTransitions.put(PaymentStatus.PARTIALLY_REFUNDED, Set.of( PaymentStatus.REFUNDED @@ -51,6 +46,8 @@ public PaymentStateFlow() { validTransitions.put(PaymentStatus.REFUNDED, Set.of()); validTransitions.put(PaymentStatus.CANCELLED, Set.of()); validTransitions.put(PaymentStatus.EXPIRED, Set.of()); + validTransitions.put(PaymentStatus.TERMINATED, Set.of()); + validTransitions.put(PaymentStatus.FAILED, Set.of()); } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java index ac448b6..0928b5c 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentValidationService.java @@ -127,7 +127,7 @@ private void validateOrderForPayment(Order order, List violations) { } // Validate order status allows payment - if (orderStateFlow.canTransition(order.getStatus(), OrderStatus.PAID)) { + if (!orderStateFlow.canTransition(order.getStatus(), OrderStatus.PAID)) { violations.add("Order status does not allow payment"); } } diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java index 706cfe3..9b8d9bf 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/provider/MobilepayProviderService.java @@ -3,6 +3,7 @@ import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.payment.domain.exception.PaymentValidationException; import com.zenfulcode.commercify.payment.domain.exception.WebhookProcessingException; +import com.zenfulcode.commercify.payment.domain.model.FailureReason; import com.zenfulcode.commercify.payment.domain.model.Payment; import com.zenfulcode.commercify.payment.domain.model.PaymentMethod; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; @@ -14,7 +15,6 @@ import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayCreatePaymentRequest; import com.zenfulcode.commercify.payment.infrastructure.gateway.MobilepayPaymentResponse; import com.zenfulcode.commercify.payment.infrastructure.gateway.client.MobilepayClient; -import com.zenfulcode.commercify.shared.domain.model.Money; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -67,24 +67,23 @@ public void handleCallback(Payment payment, WebhookPayload payload) { switch (payload.getEventType()) { case "CREATED": - log.info("Payment created: {}", payment.getId()); break; case "AUTHORIZED": paymentService.authorizePayment(payment); break; - case "ABORTED", "CANCELLED": // ABORTED = When user clicks cancel in checkout - paymentService.cancelPayment(payment); + case "ABORTED", "TERMINATED": + paymentService.failPayment(payment, FailureReason.PAYMENT_TERMINATED, PaymentStatus.TERMINATED); break; - case "EXPIRED": - paymentService.failPayment(payment, "Payment expired", PaymentStatus.EXPIRED); + case "CANCELLED": +// paymentService.cancelPayment(payment); break; - case "TERMINATED": - paymentService.failPayment(payment, "Payment terminated", PaymentStatus.TERMINATED); + case "EXPIRED": + paymentService.failPayment(payment, FailureReason.PAYMENT_EXPIRED, PaymentStatus.EXPIRED); break; case "CAPTURED": - double amount = webhookPayload.amount().value() / 100.0; - Money captureAmount = Money.of(amount, webhookPayload.amount().currency()); - paymentService.capturePayment(payment, TransactionId.generate(), captureAmount); +// double amount = webhookPayload.amount().value() / 100.0; +// Money captureAmount = Money.of(amount, webhookPayload.amount().currency()); +// paymentService.capturePayment(payment, TransactionId.generate(), captureAmount); break; case "REFUNDED": log.info("Handle refund: {}", payment.getId()); diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java index 08cd206..f03bdcb 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/model/User.java @@ -161,7 +161,7 @@ public boolean hasActiveOrders() { .anyMatch(order -> { OrderStatus status = order.getStatus(); return status == OrderStatus.PENDING || - status == OrderStatus.CONFIRMED || + status == OrderStatus.PAID || status == OrderStatus.SHIPPED; }); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b4e5d94..e588514 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -33,4 +33,4 @@ spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true # Application Configuration -logging.level.org.springframework.security=debug \ No newline at end of file +#logging.level.org.springframework.security=debug \ No newline at end of file From 9f9c41e390105a42f6fde2e0d0faef0d0f6ace99 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 2 Feb 2025 13:33:31 +0100 Subject: [PATCH 36/57] Removes debugging println --- .../order/application/service/OrderApplicationService.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java index b72ed2e..c8dbf48 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java @@ -79,9 +79,6 @@ public OrderId createOrder(CreateOrderCommand command) { @Transactional public void updateOrderStatus(UpdateOrderStatusCommand command) { Order order = orderDomainService.getOrderById(command.orderId()); - - System.out.println("Updating order status to: " + command.newStatus() + " for order: " + order.getId() + " with status: " + order.getStatus()); - orderDomainService.updateOrderStatus(order, command.newStatus()); // Save and publish events @@ -90,9 +87,7 @@ public void updateOrderStatus(UpdateOrderStatusCommand command) { @Transactional public void cancelOrder(CancelOrderCommand command) { - Order order = orderRepository.findById(command.orderId()) - .orElseThrow(() -> new OrderNotFoundException(command.orderId())); - + Order order = orderDomainService.getOrderById(command.orderId()); orderDomainService.updateOrderStatus(order, OrderStatus.CANCELLED); eventPublisher.publish(order.getDomainEvents()); From 9e870247e51935f1210b8119f812c1aa61481a4b Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 2 Feb 2025 13:43:14 +0100 Subject: [PATCH 37/57] Minor cleanup --- .../payment/application/events/PaymentEventHandler.java | 1 + .../commercify/payment/domain/model/FailureReason.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java b/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java index b7fa0d8..00f0b86 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java @@ -8,6 +8,7 @@ import com.zenfulcode.commercify.payment.domain.event.PaymentCapturedEvent; import com.zenfulcode.commercify.payment.domain.event.PaymentFailedEvent; import com.zenfulcode.commercify.payment.domain.event.PaymentReservedEvent; +import com.zenfulcode.commercify.payment.domain.model.FailureReason; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/model/FailureReason.java b/src/main/java/com/zenfulcode/commercify/payment/domain/model/FailureReason.java index 807449c..20fa663 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/model/FailureReason.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/model/FailureReason.java @@ -18,5 +18,4 @@ public enum FailureReason { FailureReason(String reason) { this.reason = reason; } - } From f0c4cf1598e386ae0f93ad15b30e8fa79e084b05 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 2 Feb 2025 21:24:46 +0100 Subject: [PATCH 38/57] Successfully sending order confirmation when order has been paid for --- .../application/events/OrderEmailHandler.java | 56 +++++++ .../domain/event/OrderStatusChangedEvent.java | 4 + .../order/domain/model/OrderShippingInfo.java | 2 +- .../service/OrderNotificationService.java | 11 ++ .../OrderEmailNotificationService.java | 145 ++++++++++++++++++ .../exception/EmailSendingException.java | 7 + .../infrastructure/config/MailConfig.java | 48 ++++++ .../infrastructure/service/EmailService.java | 27 ++++ .../templates/confirmation-email.html | 16 -- .../templates/order-confirmation-email.html | 86 ----------- .../templates/order/confirmation-email.html | 118 ++++++++++++++ .../templates/order/shipping-email.html | 119 ++++++++++++++ .../templates/order/status-update-email.html | 87 +++++++++++ 13 files changed, 623 insertions(+), 103 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/service/OrderNotificationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/EmailSendingException.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/config/MailConfig.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EmailService.java delete mode 100644 src/main/resources/templates/confirmation-email.html delete mode 100644 src/main/resources/templates/order-confirmation-email.html create mode 100644 src/main/resources/templates/order/confirmation-email.html create mode 100644 src/main/resources/templates/order/shipping-email.html create mode 100644 src/main/resources/templates/order/status-update-email.html diff --git a/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java b/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java new file mode 100644 index 0000000..b55f498 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java @@ -0,0 +1,56 @@ +package com.zenfulcode.commercify.order.application.events; + +import com.zenfulcode.commercify.order.application.service.OrderApplicationService; +import com.zenfulcode.commercify.order.domain.event.OrderStatusChangedEvent; +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.infrastructure.notification.OrderEmailNotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OrderEmailHandler { + private final OrderApplicationService orderService; + private final OrderEmailNotificationService notificationService; + +// @Async +// @EventListener +// @Transactional(readOnly = true) +// public void handleOrderCreated(OrderCreatedEvent event) { +// try { +// log.info("Sending order confirmation notification for order: {}", event.getOrderId()); +// Order order = orderService.getOrderById(event.getOrderId()); +// notificationService.sendOrderConfirmation(order); +// } catch (Exception e) { +// log.error("Failed to send order confirmation notification", e); +// } +// } + + @Async + @EventListener + @Transactional(readOnly = true) + public void handleOrderStatusChanged(OrderStatusChangedEvent event) { + log.info("Sending email confirmation notification for order: {}", event.getOrderId()); + try { + Order order = orderService.getOrderById(event.getOrderId()); + + if (event.isPaidTransition()) { + log.info("Sending order confirmation email for order: {}", order.getId()); + notificationService.sendOrderConfirmation(order); + } else if (event.isShippingTransition()) { + log.info("Sending shipping confirmation email for order: {}", order.getId()); + notificationService.sendShippingConfirmation(order); + } else if (event.isCompletedTransition()) { + log.info("Sending order status update email for order: {}", order.getId()); + notificationService.sendOrderStatusUpdate(order); + } + } catch (Exception e) { + log.error("Failed to send order status update notification", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java index fa2fae2..22cb98c 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/event/OrderStatusChangedEvent.java @@ -34,6 +34,10 @@ public String getEventType() { return "ORDER_STATUS_CHANGED"; } + public boolean isPaidTransition() { + return newStatus == OrderStatus.PAID; + } + public boolean isCompletedTransition() { return newStatus == OrderStatus.COMPLETED; } diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java index 1453411..d932eb4 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java @@ -113,7 +113,7 @@ public Address toShippingAddress() { public Address toBillingAddress() { if (!hasBillingAddress()) { - return null; + return toShippingAddress(); } return new Address( billingStreet, diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderNotificationService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderNotificationService.java new file mode 100644 index 0000000..5d67cf0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderNotificationService.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.order.domain.service; + +import com.zenfulcode.commercify.order.domain.model.Order; + +public interface OrderNotificationService { + void sendOrderConfirmation(Order order); + + void sendOrderStatusUpdate(Order order); + + void sendShippingConfirmation(Order order); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java new file mode 100644 index 0000000..ba173b5 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java @@ -0,0 +1,145 @@ +package com.zenfulcode.commercify.order.infrastructure.notification; + +import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.model.OrderLine; +import com.zenfulcode.commercify.order.domain.service.OrderNotificationService; +import com.zenfulcode.commercify.shared.domain.exception.EmailSendingException; +import com.zenfulcode.commercify.shared.infrastructure.service.EmailService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +@Service +@Slf4j +@RequiredArgsConstructor +public class OrderEmailNotificationService implements OrderNotificationService { + private final EmailService emailService; + private final TemplateEngine templateEngine; + private static final String ORDER_CONFIRMATION_TEMPLATE = "order/confirmation-email"; + private static final String ORDER_STATUS_UPDATE_TEMPLATE = "order/status-update-email"; + private static final String ORDER_SHIPPING_TEMPLATE = "order/shipping-email"; + + @Override + public void sendOrderConfirmation(Order order) { + try { + Context context = createOrderContext(order); + String emailContent = templateEngine.process(ORDER_CONFIRMATION_TEMPLATE, context); + emailService.sendEmail( + order.getOrderShippingInfo().toCustomerDetails().email(), + "Order Confirmation - #" + order.getId(), + emailContent, true + ); + log.info("Order confirmation email sent for order: {}", order.getId()); + } catch (Exception e) { + log.error("Failed to send order confirmation email", e); + throw new EmailSendingException("Failed to send order confirmation email: " + e.getMessage()); + } + } + + @Override + public void sendOrderStatusUpdate(Order order) { + try { + Context context = createOrderContext(order); + String emailContent = templateEngine.process(ORDER_STATUS_UPDATE_TEMPLATE, context); + emailService.sendEmail( + order.getOrderShippingInfo().toCustomerDetails().email(), + "Order Status Update - #" + order.getId(), + emailContent, true + ); + log.info("Order status update email sent for order: {}", order.getId()); + } catch (Exception e) { + log.error("Failed to send order status update email", e); + throw new EmailSendingException("Failed to send order status update email: " + e.getMessage()); + } + } + + @Override + public void sendShippingConfirmation(Order order) { + try { + Context context = createOrderContext(order); + String emailContent = templateEngine.process(ORDER_SHIPPING_TEMPLATE, context); + emailService.sendEmail( + order.getOrderShippingInfo().toCustomerDetails().email(), + "Order Shipped - #" + order.getId(), + emailContent, true + ); + log.info("Order shipping confirmation email sent for order: {}", order.getId()); + } catch (Exception e) { + log.error("Failed to send shipping confirmation email", e); + throw new EmailSendingException("Failed to send shipping confirmation email: " + e.getMessage()); + } + } + + private Context createOrderContext(Order order) { + Context context = new Context(Locale.getDefault()); + + List> orderItems = order.getOrderLines().stream() + .map(this::createOrderItemMap) + .toList(); + + Map shippingAddress = new HashMap<>(); + shippingAddress.put("city", order.getOrderShippingInfo().toShippingAddress().city()); + shippingAddress.put("zipCode", order.getOrderShippingInfo().toShippingAddress().zipCode()); + shippingAddress.put("country", order.getOrderShippingInfo().toShippingAddress().country()); + shippingAddress.put("street", order.getOrderShippingInfo().toShippingAddress().street()); + shippingAddress.put("state", order.getOrderShippingInfo().toShippingAddress().state()); + context.setVariable("shippingAddress", shippingAddress); + + Map billingAddress = new HashMap<>(); + billingAddress.put("city", order.getOrderShippingInfo().toBillingAddress().city()); + billingAddress.put("zipCode", order.getOrderShippingInfo().toBillingAddress().zipCode()); + billingAddress.put("country", order.getOrderShippingInfo().toBillingAddress().country()); + billingAddress.put("street", order.getOrderShippingInfo().toBillingAddress().street()); + billingAddress.put("state", order.getOrderShippingInfo().toBillingAddress().state()); + context.setVariable("billingAddress", billingAddress); + + final String username = order.getOrderShippingInfo().toCustomerDetails().firstName() + " " + + order.getOrderShippingInfo().toCustomerDetails().lastName(); + + Map details = new HashMap<>(); + details.put("id", order.getId().toString()); + details.put("firstName", order.getOrderShippingInfo().toCustomerDetails().firstName()); + details.put("lastName", order.getOrderShippingInfo().toCustomerDetails().lastName()); + details.put("username", username); + details.put("phone", order.getOrderShippingInfo().toCustomerDetails().phone()); + details.put("email", order.getOrderShippingInfo().toCustomerDetails().email()); + details.put("orderNumber", order.getId().toString()); + details.put("status", order.getStatus().toString()); + details.put("createdAt", order.getCreatedAt()); + details.put("currency", order.getCurrency()); + details.put("totalAmount", order.getTotalAmount().getAmount().doubleValue()); + details.put("items", orderItems); + context.setVariable("order", details); + +// Map.of( +// "id", order.getId().toString(), +// "userName", order.getOrderShippingInfo().toCustomerDetails().firstName(), +// "status", order.getStatus().toString(), +// "createdAt", order.getCreatedAt(), +// "currency", order.getCurrency(), +// "totalAmount", order.getTotalAmount().getAmount().doubleValue(), +// "items", orderItems +// ) + + return context; + } + + private Map createOrderItemMap(OrderLine orderLine) { + Map items = new HashMap<>(); + + items.put("name", orderLine.getProduct().getName()); + items.put("variant", orderLine.getProductVariant() != null ? + orderLine.getProductVariant().getSku() : ""); + items.put("quantity", orderLine.getQuantity()); + items.put("unitPrice", orderLine.getUnitPrice().getAmount().doubleValue()); + items.put("total", orderLine.getTotal().getAmount().doubleValue()); + return items; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EmailSendingException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EmailSendingException.java new file mode 100644 index 0000000..80d3b84 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/EmailSendingException.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +public class EmailSendingException extends DomainException { + public EmailSendingException(String message) { + super(message); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/config/MailConfig.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/config/MailConfig.java new file mode 100644 index 0000000..f62563c --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/config/MailConfig.java @@ -0,0 +1,48 @@ +package com.zenfulcode.commercify.shared.infrastructure.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class MailConfig { + + @Value("${spring.mail.host}") + private String host; + + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.username}") + private String username; + + @Value("${spring.mail.password}") + private String password; + + @Value("${spring.mail.properties.mail.smtp.auth}") + private boolean auth; + + @Value("${spring.mail.properties.mail.smtp.starttls.enable}") + private boolean starttls; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", auth); + props.put("mail.smtp.starttls.enable", starttls); + props.put("mail.debug", "false"); // Set to true for debugging + + return mailSender; + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EmailService.java b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EmailService.java new file mode 100644 index 0000000..25d250e --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/infrastructure/service/EmailService.java @@ -0,0 +1,27 @@ +package com.zenfulcode.commercify.shared.infrastructure.service; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class EmailService { + private final JavaMailSender mailSender; + + public void sendEmail(String to, String subject, String content, boolean isHtml) throws MessagingException { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); + + helper.setTo(to); + helper.setSubject(subject); + helper.setText(content, isHtml); + + mailSender.send(mimeMessage); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/confirmation-email.html b/src/main/resources/templates/confirmation-email.html deleted file mode 100644 index 8d735fb..0000000 --- a/src/main/resources/templates/confirmation-email.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - Confirm your email - - - -

Welcome to Commercify!

-

Thank you for registering. Please click the link below to confirm your email address:

-

- Confirmation Link -

-

This link will expire in 24 hours.

-

If you did not create an account, please ignore this email.

- - \ No newline at end of file diff --git a/src/main/resources/templates/order-confirmation-email.html b/src/main/resources/templates/order-confirmation-email.html deleted file mode 100644 index c3af9a6..0000000 --- a/src/main/resources/templates/order-confirmation-email.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - Order Confirmation - - - - -

Order Confirmation - #123

- -

Dear Customer,

- -

Thank you for your order. Here are your order details:

- -
-

Order Status: PENDING

-

Order Date: 01-01-2024

- - - - - - - - - - - - - - - - - - -
ProductQuantityPriceTotal
- Product Name -
Variant Details
-
1$99.99$99.99
- -

Total Amount: $99.99

-
- -

- Your order has been received and is being processed. We'll notify you when there are any updates. -

- -

- Your order has been confirmed and is being prepared for shipping. -

- -

- Your order has been shipped! You can track your order using the following link: - Tracking Link -

- -

If you have any questions about your order, please contact our customer service.

- -

Best regards,
Commercify Team

- - \ No newline at end of file diff --git a/src/main/resources/templates/order/confirmation-email.html b/src/main/resources/templates/order/confirmation-email.html new file mode 100644 index 0000000..9a61434 --- /dev/null +++ b/src/main/resources/templates/order/confirmation-email.html @@ -0,0 +1,118 @@ + + + + Order Confirmation + + + + +
+

Order Confirmation

+

Order #123

+
+ +

Dear Customer,

+ +

Thank you for your order! We're pleased to confirm that we've received your order and it's being processed.

+ +
+

Order Details

+

Order Date: 01-01-2024

+ + + + + + + + + + + + + + + + + + +
ProductQuantityPriceTotal
+ Product Name +
Variant Details
+
1$99.99$99.99
+ +
+ Total Amount: $99.99 +
+
+ +

What happens next?

+
    +
  • We will prepare your order for shipping
  • +
  • You'll receive a shipping confirmation email when your order is on its way
  • +
  • You can track your order status by logging into your account
  • +
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/order/shipping-email.html b/src/main/resources/templates/order/shipping-email.html new file mode 100644 index 0000000..48ff278 --- /dev/null +++ b/src/main/resources/templates/order/shipping-email.html @@ -0,0 +1,119 @@ + + + + Order Shipped + + + + +
+

Your Order Has Been Shipped!

+

Order #123

+
+ +

Dear Customer,

+ +

Great news! Your order is on its way to you.

+ +
+

Shipping Information

+

Shipping Address:

+

123 Main St

+

City, State ZIP

+

Country

+
+ +
+

Order Summary

+ + + + + + + + + + + + + +
ProductQuantity
+ Product Name +
Variant Details
+
1
+
+ +

What to expect:

+
    +
  • Your order is expected to arrive within 3-5 business days
  • +
  • You'll receive a delivery confirmation email once your package has been delivered
  • +
  • If you have any questions about your delivery, please contact our customer service team
  • +
+ + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/order/status-update-email.html b/src/main/resources/templates/order/status-update-email.html new file mode 100644 index 0000000..280dfbf --- /dev/null +++ b/src/main/resources/templates/order/status-update-email.html @@ -0,0 +1,87 @@ + + + + Order Status Update + + + + +
+

Order Status Update

+

Order #123

+
+ +

Dear Customer,

+ +
+

Your order status has been updated to:

+
COMPLETED
+
+ +
+

Order Summary

+

Order Date: 01-01-2024

+

Total Amount: $99.99

+
+ +
+
+

Your order has been successfully completed! We hope you enjoy your purchase.

+

If you have any feedback about our products or service, we'd love to hear from you.

+
+
+

Your order has been cancelled. If you did not request this cancellation, please contact our customer service immediately.

+
+
+

If you have any questions about your order, please don't hesitate to contact our customer service team.

+
+
+ + + + \ No newline at end of file From 9aacf584be363504e2e60b46d067fef4216118b7 Mon Sep 17 00:00:00 2001 From: GustavH Date: Tue, 4 Feb 2025 13:36:02 +0100 Subject: [PATCH 39/57] adding new order notification for admin --- example.env | 20 +++ .../infrastructure/config/SecurityConfig.java | 2 +- .../application/events/OrderEmailHandler.java | 2 + .../order/domain/model/OrderShippingInfo.java | 4 + .../service/OrderNotificationService.java | 2 + .../OrderEmailNotificationService.java | 54 +++++--- src/main/resources/application.properties | 1 + .../order/admin-order-notification.html | 127 ++++++++++++++++++ .../templates/order/confirmation-email.html | 2 +- .../templates/order/shipping-email.html | 2 +- .../templates/order/status-update-email.html | 2 +- 11 files changed, 196 insertions(+), 22 deletions(-) create mode 100644 example.env create mode 100644 src/main/resources/templates/order/admin-order-notification.html diff --git a/example.env b/example.env new file mode 100644 index 0000000..96c1b2e --- /dev/null +++ b/example.env @@ -0,0 +1,20 @@ +DATASOURCE_URL=jdbc:mysql://localhost/commercifydb?createDatabaseIfNotExist=true +DATASOURCE_USERNAME= +DATASOURCE_PASSWORD= +STRIPE_SECRET_TEST_KEY= +STRIPE_WEBHOOK_SECRET= +JWT_SECRET_KEY=7581e8477a88733917bc3b48f683a827935a492a0bd976a59429a72f28c71fd3 +ADMIN_EMAIL= +ADMIN_PASSWORD=commercifyadmin123! +ADMIN_ORDER_DASHBOARD=https:///admin/orders +MOBILEPAY_CLIENT_ID= +MOBILEPAY_CLIENT_SECRET= +MOBILEPAY_SUBSCRIPTION_KEY= +MOBILEPAY_MERCHANT_ID= +MOBILEPAY_API_URL= +MOBILEPAY_SYSTEM_NAME= +MOBILEPAY_WEBHOOK_CALLBACK=https:///api/v2/payments/webhooks/mobilepay/callback +MAIL_HOST=smtp.gmail.com +MAIL_PORT= +MAIL_USERNAME= +MAIL_PASSWORD= \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index bfb00b5..f442718 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -71,6 +71,6 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c @Bean public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + return new BCryptPasswordEncoder(15); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java b/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java index b55f498..c012bd1 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java @@ -42,6 +42,8 @@ public void handleOrderStatusChanged(OrderStatusChangedEvent event) { if (event.isPaidTransition()) { log.info("Sending order confirmation email for order: {}", order.getId()); notificationService.sendOrderConfirmation(order); + notificationService.notifyAdminNewOrder(order); + } else if (event.isShippingTransition()) { log.info("Sending shipping confirmation email for order: {}", order.getId()); notificationService.sendShippingConfirmation(order); diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java index d932eb4..6006006 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/model/OrderShippingInfo.java @@ -92,6 +92,10 @@ public static OrderShippingInfo create( return info; } + public String getCustomerName() { + return customerFirstName + " " + customerLastName; + } + public CustomerDetails toCustomerDetails() { return new CustomerDetails( customerFirstName, diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderNotificationService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderNotificationService.java index 5d67cf0..be03a41 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderNotificationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderNotificationService.java @@ -8,4 +8,6 @@ public interface OrderNotificationService { void sendOrderStatusUpdate(Order order); void sendShippingConfirmation(Order order); + + void notifyAdminNewOrder(Order order); } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java index ba173b5..16282ed 100644 --- a/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java @@ -7,6 +7,7 @@ import com.zenfulcode.commercify.shared.infrastructure.service.EmailService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; @@ -22,9 +23,17 @@ public class OrderEmailNotificationService implements OrderNotificationService { private final EmailService emailService; private final TemplateEngine templateEngine; + + @Value("${admin.email}") + private String adminEmail; + + @Value("${admin.order-dashboard") + private String orderDashboard; + private static final String ORDER_CONFIRMATION_TEMPLATE = "order/confirmation-email"; private static final String ORDER_STATUS_UPDATE_TEMPLATE = "order/status-update-email"; private static final String ORDER_SHIPPING_TEMPLATE = "order/shipping-email"; + private static final String ADMIN_ORDER_TEMPLATE = "order/admin-order-notification"; @Override public void sendOrderConfirmation(Order order) { @@ -77,6 +86,23 @@ public void sendShippingConfirmation(Order order) { } } + @Override + public void notifyAdminNewOrder(Order order) { + try { + Context context = createAdminOrderContext(order); + String emailContent = templateEngine.process(ADMIN_ORDER_TEMPLATE, context); + emailService.sendEmail( + adminEmail, + "New Order Received - #" + order.getId(), + emailContent, true + ); + log.info("Admin notification sent for order: {}", order.getId()); + } catch (Exception e) { + log.error("Failed to send admin notification", e); + throw new EmailSendingException("Failed to send admin notification: " + e.getMessage()); + } + } + private Context createOrderContext(Order order) { Context context = new Context(Locale.getDefault()); @@ -100,16 +126,11 @@ private Context createOrderContext(Order order) { billingAddress.put("state", order.getOrderShippingInfo().toBillingAddress().state()); context.setVariable("billingAddress", billingAddress); - final String username = order.getOrderShippingInfo().toCustomerDetails().firstName() + " " + - order.getOrderShippingInfo().toCustomerDetails().lastName(); - Map details = new HashMap<>(); details.put("id", order.getId().toString()); - details.put("firstName", order.getOrderShippingInfo().toCustomerDetails().firstName()); - details.put("lastName", order.getOrderShippingInfo().toCustomerDetails().lastName()); - details.put("username", username); - details.put("phone", order.getOrderShippingInfo().toCustomerDetails().phone()); - details.put("email", order.getOrderShippingInfo().toCustomerDetails().email()); + details.put("customerName", order.getOrderShippingInfo().getCustomerName()); + details.put("customerPhone", order.getOrderShippingInfo().getCustomerPhone()); + details.put("customerEmail", order.getOrderShippingInfo().getCustomerEmail()); details.put("orderNumber", order.getId().toString()); details.put("status", order.getStatus().toString()); details.put("createdAt", order.getCreatedAt()); @@ -117,17 +138,12 @@ private Context createOrderContext(Order order) { details.put("totalAmount", order.getTotalAmount().getAmount().doubleValue()); details.put("items", orderItems); context.setVariable("order", details); + return context; + } -// Map.of( -// "id", order.getId().toString(), -// "userName", order.getOrderShippingInfo().toCustomerDetails().firstName(), -// "status", order.getStatus().toString(), -// "createdAt", order.getCreatedAt(), -// "currency", order.getCurrency(), -// "totalAmount", order.getTotalAmount().getAmount().doubleValue(), -// "items", orderItems -// ) - + private Context createAdminOrderContext(Order order) { + Context context = createOrderContext(order); + context.setVariable("adminOrderUrl", String.format("%s/%s", orderDashboard, order.getId().toString())); return context; } @@ -138,6 +154,8 @@ private Map createOrderItemMap(OrderLine orderLine) { items.put("variant", orderLine.getProductVariant() != null ? orderLine.getProductVariant().getSku() : ""); items.put("quantity", orderLine.getQuantity()); + items.put("sku", orderLine.getProductVariant() != null ? + orderLine.getProductVariant().getSku() : orderLine.getProduct().getId().toString()); items.put("unitPrice", orderLine.getUnitPrice().getAmount().doubleValue()); items.put("total", orderLine.getTotal().getAmount().doubleValue()); return items; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e588514..ba57a59 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,6 +17,7 @@ security.jwt.refresh-token-expiration=86400000 # Admin Configuration admin.email=${ADMIN_EMAIL} admin.password=${ADMIN_PASSWORD} +admin.order-dashboard=${ADMIN_ORDER_DASHBOARD} # MobilePay Configuration integration.payments.mobilepay.client-id=${MOBILEPAY_CLIENT_ID} integration.payments.mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID} diff --git a/src/main/resources/templates/order/admin-order-notification.html b/src/main/resources/templates/order/admin-order-notification.html new file mode 100644 index 0000000..7282681 --- /dev/null +++ b/src/main/resources/templates/order/admin-order-notification.html @@ -0,0 +1,127 @@ + + + + New Order Received + + + + +
+

New Order Received

+

Order #123

+
+ +
+

Order Information

+

Date: 01-01-2024

+

Status: PENDING

+
+ +
+

Customer Details

+

Name: John Doe

+

Email: john@example.com

+

Phone: +1234567890

+
+ +
+

Order Details

+ + + + + + + + + + + + + + + + + + + +
ProductSKUQuantityPriceTotal
+ Product Name +
Variant Details
+
SKU1231$99.99$99.99
+ +
+ Total Amount: $99.99 +
+
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/order/confirmation-email.html b/src/main/resources/templates/order/confirmation-email.html index 9a61434..ac6e5ab 100644 --- a/src/main/resources/templates/order/confirmation-email.html +++ b/src/main/resources/templates/order/confirmation-email.html @@ -66,7 +66,7 @@

Order Confirmation

Order #123

-

Dear Customer,

+

Dear Customer,

Thank you for your order! We're pleased to confirm that we've received your order and it's being processed.

diff --git a/src/main/resources/templates/order/shipping-email.html b/src/main/resources/templates/order/shipping-email.html index 48ff278..45f4e79 100644 --- a/src/main/resources/templates/order/shipping-email.html +++ b/src/main/resources/templates/order/shipping-email.html @@ -67,7 +67,7 @@

Your Order Has Been Shipped!

Order #123

-

Dear Customer,

+

Dear Customer,

Great news! Your order is on its way to you.

diff --git a/src/main/resources/templates/order/status-update-email.html b/src/main/resources/templates/order/status-update-email.html index 280dfbf..10a5005 100644 --- a/src/main/resources/templates/order/status-update-email.html +++ b/src/main/resources/templates/order/status-update-email.html @@ -53,7 +53,7 @@

Order Status Update

Order #123

-

Dear Customer,

+

Dear Customer,

Your order status has been updated to:

From 64ba0dec03cdf1dfcff70892da78f21ad4eef6f0 Mon Sep 17 00:00:00 2001 From: GustavH Date: Tue, 4 Feb 2025 15:34:01 +0100 Subject: [PATCH 40/57] Fixing all authenticated users could create orders for other users --- .../commercify/api/order/OrderController.java | 20 ++++++++++++++++++- .../auth/domain/model/AuthenticatedUser.java | 4 ++++ .../UnauthorizedOrderCreationException.java | 9 +++++++++ .../exception/DomainForbiddenException.java | 7 +++++++ .../exception/GlobalExceptionHandler.java | 12 +++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderCreationException.java create mode 100644 src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainForbiddenException.java diff --git a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java index 922aa13..e56f3e2 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java @@ -5,12 +5,14 @@ import com.zenfulcode.commercify.api.order.dto.response.OrderDetailsResponse; import com.zenfulcode.commercify.api.order.dto.response.PagedOrderResponse; import com.zenfulcode.commercify.api.order.mapper.OrderDtoMapper; +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; import com.zenfulcode.commercify.order.application.command.CancelOrderCommand; import com.zenfulcode.commercify.order.application.command.CreateOrderCommand; import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; import com.zenfulcode.commercify.order.application.service.OrderApplicationService; +import com.zenfulcode.commercify.order.domain.exception.UnauthorizedOrderCreationException; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; @@ -20,6 +22,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @RestController @@ -31,10 +34,25 @@ public class OrderController { @PostMapping public ResponseEntity> createOrder( - @RequestBody CreateOrderRequest request) { + @RequestBody CreateOrderRequest request, + Authentication authentication) { + AuthenticatedUser user = (AuthenticatedUser) authentication.getPrincipal(); + + // Check if user is authorized to create order + System.out.println(authentication.getName()); + System.out.println(user.getUserId()); + + if (!request.userId().equals(user.getUserId()) && !user.isAdmin()) { + throw new UnauthorizedOrderCreationException(request.userId()); + } + + // Convert request to command CreateOrderCommand command = orderDtoMapper.toCommand(request); + + // Create order through application service OrderId orderId = orderApplicationService.createOrder(command); + // Create and return response CreateOrderResponse response = new CreateOrderResponse( orderId.toString(), "Order created successfully" diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java index 1d56589..e2fa5a9 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java @@ -79,6 +79,10 @@ public Collection getAuthorities() { .collect(Collectors.toList()); } + public boolean isAdmin() { + return roles.contains(UserRole.ROLE_ADMIN); + } + @Override public String getPassword() { return password; diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderCreationException.java b/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderCreationException.java new file mode 100644 index 0000000..aa490ac --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderCreationException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.order.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainForbiddenException; + +public class UnauthorizedOrderCreationException extends DomainForbiddenException { + public UnauthorizedOrderCreationException(String userId) { + super("User is not authorized to create order for user ID: " + userId); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainForbiddenException.java b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainForbiddenException.java new file mode 100644 index 0000000..fa9f886 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/shared/domain/exception/DomainForbiddenException.java @@ -0,0 +1,7 @@ +package com.zenfulcode.commercify.shared.domain.exception; + +public class DomainForbiddenException extends DomainException { + public DomainForbiddenException(String message) { + super(message); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java b/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java index a6b510c..ac84d41 100644 --- a/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/zenfulcode/commercify/shared/interfaces/rest/exception/GlobalExceptionHandler.java @@ -1,9 +1,11 @@ package com.zenfulcode.commercify.shared.interfaces.rest.exception; import com.zenfulcode.commercify.shared.domain.exception.DomainException; +import com.zenfulcode.commercify.shared.domain.exception.DomainForbiddenException; import com.zenfulcode.commercify.shared.domain.exception.DomainValidationException; import com.zenfulcode.commercify.shared.domain.exception.EntityNotFoundException; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -53,4 +55,14 @@ public ResponseEntity> handleNotFoundException( return ResponseEntity.status(404).body(response); } + + @ExceptionHandler(DomainForbiddenException.class) + public ResponseEntity> handleUnauthorizedOrderCreation(DomainForbiddenException ex) { + ApiResponse response = ApiResponse.error( + ex.getMessage(), + "UNAUTHORIZED_ORDER_CREATION", + 403 + ); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); + } } From 3860d6966b6793dec0ade1b1a4d64f3e03405c10 Mon Sep 17 00:00:00 2001 From: GustavH Date: Thu, 6 Feb 2025 08:41:42 +0100 Subject: [PATCH 41/57] cleaning up unauthorized error handling for OrdersController --- .../api/auth/dto/response/AuthResponse.java | 2 +- .../commercify/api/order/OrderController.java | 38 +++++++++++++------ .../order/dto/request/CreateOrderRequest.java | 5 +++ .../auth/domain/model/AuthenticatedUser.java | 5 +++ .../infrastructure/security/TokenService.java | 2 +- .../UnauthorizedOrderCreationException.java | 3 +- .../UnauthorizedOrderFetchingException.java | 9 +++++ 7 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderFetchingException.java diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/AuthResponse.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/AuthResponse.java index 2bd36d5..c299a80 100644 --- a/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/AuthResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/AuthResponse.java @@ -24,7 +24,7 @@ public static AuthResponse from(AuthenticationResult result) { result.accessToken(), result.refreshToken(), "Bearer", - result.user().getUserId(), + result.user().getUserId().toString(), result.user().getUsername(), result.user().getEmail(), roles diff --git a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java index e56f3e2..36fe57f 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java @@ -13,6 +13,7 @@ import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; import com.zenfulcode.commercify.order.application.service.OrderApplicationService; import com.zenfulcode.commercify.order.domain.exception.UnauthorizedOrderCreationException; +import com.zenfulcode.commercify.order.domain.exception.UnauthorizedOrderFetchingException; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; @@ -37,13 +38,8 @@ public ResponseEntity> createOrder( @RequestBody CreateOrderRequest request, Authentication authentication) { AuthenticatedUser user = (AuthenticatedUser) authentication.getPrincipal(); - - // Check if user is authorized to create order - System.out.println(authentication.getName()); - System.out.println(user.getUserId()); - - if (!request.userId().equals(user.getUserId()) && !user.isAdmin()) { - throw new UnauthorizedOrderCreationException(request.userId()); + if (isNotUserAuthorized(user, request.getUserId().getId())) { + throw new UnauthorizedOrderCreationException(request.getUserId()); } // Convert request to command @@ -63,18 +59,30 @@ public ResponseEntity> createOrder( @GetMapping("/{orderId}") public ResponseEntity> getOrder( - @PathVariable String orderId) { + @PathVariable String orderId, + Authentication authentication) { OrderDetailsDTO order = orderApplicationService.getOrderDetailsById(OrderId.of(orderId)); + AuthenticatedUser user = (AuthenticatedUser) authentication.getPrincipal(); + + if (isNotUserAuthorized(user, order.userId().getId())) { + throw new UnauthorizedOrderFetchingException(user.getUserId().getId()); + } + OrderDetailsResponse response = orderDtoMapper.toResponse(order); return ResponseEntity.ok(ApiResponse.success(response)); } @GetMapping("/user/{userId}") - @PreAuthorize("hasRole('USER') and #userId == authentication.principal.id or hasRole('ADMIN')") public ResponseEntity> getOrdersByUserId( @PathVariable String userId, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { + @RequestParam(defaultValue = "10") int size, + Authentication authentication) { + AuthenticatedUser user = (AuthenticatedUser) authentication.getPrincipal(); + + if (isNotUserAuthorized(user, userId)) { + throw new UnauthorizedOrderFetchingException(userId); + } FindOrdersByUserIdQuery query = new FindOrdersByUserIdQuery( UserId.of(userId), @@ -91,10 +99,11 @@ public ResponseEntity> getOrdersByUserId( public ResponseEntity> getAllOrders( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { - FindAllOrdersQuery query = new FindAllOrdersQuery(PageRequest.of(page, size)); + Page orders = orderApplicationService.findAllOrders(query); PagedOrderResponse response = orderDtoMapper.toPagedResponse(orders); + return ResponseEntity.ok(ApiResponse.success(response)); } @@ -102,7 +111,14 @@ public ResponseEntity> getAllOrders( @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> cancelOrder(@PathVariable String orderId) { CancelOrderCommand command = new CancelOrderCommand(OrderId.of(orderId)); + orderApplicationService.cancelOrder(command); + return ResponseEntity.ok(ApiResponse.success("Order cancelled successfully")); } + + + private boolean isNotUserAuthorized(AuthenticatedUser user, String userId) { + return !user.getUserId().getId().equals(userId) && !user.isAdmin(); + } } diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderRequest.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderRequest.java index 23012a7..7f24ff6 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/request/CreateOrderRequest.java @@ -1,5 +1,7 @@ package com.zenfulcode.commercify.api.order.dto.request; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; + import java.util.List; public record CreateOrderRequest( @@ -10,4 +12,7 @@ public record CreateOrderRequest( AddressRequest billingAddress, List orderLines ) { + public UserId getUserId() { + return UserId.of(userId); + } } diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java index e2fa5a9..8241234 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/model/AuthenticatedUser.java @@ -1,5 +1,6 @@ package com.zenfulcode.commercify.auth.domain.model; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -72,6 +73,10 @@ public static AuthenticatedUser create( ); } + public UserId getUserId() { + return UserId.of(userId); + } + @Override public Collection getAuthorities() { return roles.stream() diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/TokenService.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/TokenService.java index ecb3fa1..e59c0e9 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/TokenService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/TokenService.java @@ -46,7 +46,7 @@ private String buildToken(AuthenticatedUser user, long expiration) { Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() - .subject(user.getUserId()) + .subject(user.getUserId().toString()) .claim("username", user.getUsername()) .claim("email", user.getEmail()) .claim("roles", user.getRoles()) diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderCreationException.java b/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderCreationException.java index aa490ac..844c6d7 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderCreationException.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderCreationException.java @@ -1,9 +1,10 @@ package com.zenfulcode.commercify.order.domain.exception; import com.zenfulcode.commercify.shared.domain.exception.DomainForbiddenException; +import com.zenfulcode.commercify.user.domain.valueobject.UserId; public class UnauthorizedOrderCreationException extends DomainForbiddenException { - public UnauthorizedOrderCreationException(String userId) { + public UnauthorizedOrderCreationException(UserId userId) { super("User is not authorized to create order for user ID: " + userId); } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderFetchingException.java b/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderFetchingException.java new file mode 100644 index 0000000..a75b6b0 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/domain/exception/UnauthorizedOrderFetchingException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.order.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainForbiddenException; + +public class UnauthorizedOrderFetchingException extends DomainForbiddenException { + public UnauthorizedOrderFetchingException(String userId) { + super("User is not authorized to fetch order for user ID: " + userId); + } +} \ No newline at end of file From fb26c13e4e6ea7513be5a9a5098e73fcd547b9b5 Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 9 Feb 2025 00:24:39 +0100 Subject: [PATCH 42/57] cleaning up and refactoring consider vulnerable dependencies --- Dockerfile | 1 + pom.xml | 24 ++++++++++++++++- .../api/order/dto/response/MoneyResponse.java | 7 ----- .../dto/response/OrderDetailsResponse.java | 8 +++--- .../order/dto/response/OrderLineResponse.java | 5 ++-- .../dto/response/OrderSummaryResponse.java | 6 ++++- .../api/order/mapper/OrderDtoMapper.java | 23 +++++----------- .../api/payment/PaymentController.java | 4 +-- .../api/payment/PaymentWebhookController.java | 2 +- .../request/InitiatePaymentRequest.java | 7 +++-- .../MobilepayWebhookRegistrationRequest.java | 2 +- .../request/PaymentDetailsRequest.java | 2 +- .../{ => dto}/response/PaymentResponse.java | 2 +- .../api/payment/mapper/PaymentDtoMapper.java | 8 +++--- .../api/product/ProductController.java | 2 +- .../dto/request/AdjustInventoryRequest.java | 2 +- .../dto/request/CreateProductRequest.java | 6 +++-- .../dto/request/CreateVariantRequest.java | 4 ++- .../api/product/dto/request/PriceRequest.java | 7 ----- .../dto/request/UpdateProductRequest.java | 4 ++- .../request/VariantPriceUpdateRequest.java | 4 ++- .../dto/response/CreateProductResponse.java | 5 +++- .../dto/response/ProductDetailResponse.java | 8 +++--- .../dto/response/ProductSummaryResponse.java | 17 +++--------- .../ProductVariantSummaryResponse.java | 4 ++- .../api/product/mapper/ProductDtoMapper.java | 27 ++++++------------- .../product/mapper/ProductResponseMapper.java | 19 ++----------- .../infrastructure/config/SecurityConfig.java | 25 +++++++++++++++-- .../messaging}/events/OrderEmailHandler.java | 15 +---------- .../OrderEmailNotificationService.java | 2 +- .../message}/events/PaymentEventHandler.java | 2 +- src/main/resources/application.properties | 1 + 32 files changed, 125 insertions(+), 130 deletions(-) delete mode 100644 src/main/java/com/zenfulcode/commercify/api/order/dto/response/MoneyResponse.java rename src/main/java/com/zenfulcode/commercify/api/payment/{ => dto}/request/InitiatePaymentRequest.java (53%) rename src/main/java/com/zenfulcode/commercify/api/payment/{ => dto}/request/MobilepayWebhookRegistrationRequest.java (55%) rename src/main/java/com/zenfulcode/commercify/api/payment/{ => dto}/request/PaymentDetailsRequest.java (76%) rename src/main/java/com/zenfulcode/commercify/api/payment/{ => dto}/response/PaymentResponse.java (72%) delete mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/PriceRequest.java rename src/main/java/com/zenfulcode/commercify/order/{application => infrastructure/messaging}/events/OrderEmailHandler.java (77%) rename src/main/java/com/zenfulcode/commercify/payment/{application => infrastructure/message}/events/PaymentEventHandler.java (97%) diff --git a/Dockerfile b/Dockerfile index c707155..6021617 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY --from=build /app/target/*.jar app.jar COPY --from=build /app/src/main/resources/db/changelog /app/resources/db/changelog +COPY --from=build /app/src/main/resources/templates /app/resources/templates EXPOSE 6091 ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/pom.xml b/pom.xml index b65ca66..cd07427 100644 --- a/pom.xml +++ b/pom.xml @@ -102,7 +102,7 @@ com.stripe stripe-java - 26.11.0 + 28.3.0 compile @@ -172,6 +172,28 @@ + + + cz.habarta.typescript-generator + typescript-generator-maven-plugin + 3.2.1263 + + + generate + + generate + + process-classes + + + + jackson2 + module + + com.zenfulcode.commercify.api.**.dto.** + + + diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/MoneyResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/MoneyResponse.java deleted file mode 100644 index 1ba0f0f..0000000 --- a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/MoneyResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.api.order.dto.response; - -public record MoneyResponse( - double amount, - String currency -) { -} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderDetailsResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderDetailsResponse.java index 2e524f2..0e53c6b 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderDetailsResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderDetailsResponse.java @@ -1,5 +1,7 @@ package com.zenfulcode.commercify.api.order.dto.response; +import com.zenfulcode.commercify.shared.domain.model.Money; + import java.time.Instant; import java.util.List; @@ -7,11 +9,11 @@ public record OrderDetailsResponse( String id, String userId, String status, - String currency, - MoneyResponse totalAmount, + Money totalAmount, List orderLines, CustomerDetailsResponse customerDetails, AddressResponse shippingAddress, AddressResponse billingAddress, Instant createdAt -) {} +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java index 0146e11..29355c0 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderLineResponse.java @@ -2,13 +2,14 @@ import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.product.domain.valueobject.VariantId; +import com.zenfulcode.commercify.shared.domain.model.Money; public record OrderLineResponse( String id, ProductId productId, VariantId variantId, int quantity, - MoneyResponse unitPrice, - MoneyResponse total + Money unitPrice, + Money total ) { } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderSummaryResponse.java b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderSummaryResponse.java index c6dd019..9346994 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderSummaryResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/dto/response/OrderSummaryResponse.java @@ -1,12 +1,16 @@ package com.zenfulcode.commercify.api.order.dto.response; +import com.zenfulcode.commercify.shared.domain.model.Money; + import java.time.Instant; public record OrderSummaryResponse( String id, String userId, + String customerName, String status, - MoneyResponse totalAmount, + int orderLineAmount, + Money totalAmount, Instant createdAt ) { } diff --git a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java index 56fde45..baa1273 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java @@ -73,11 +73,7 @@ public OrderDetailsResponse toResponse(OrderDetailsDTO dto) { dto.id().toString(), dto.userId().toString(), dto.status().toString(), - dto.currency(), - new MoneyResponse( - dto.totalAmount().getAmount().doubleValue(), - dto.totalAmount().getCurrency() - ), + dto.totalAmount(), dto.orderLines().stream() .map(this::toOrderLineResponse) .collect(Collectors.toList()), @@ -109,11 +105,10 @@ private OrderSummaryResponse toSummaryResponse(Order order) { return new OrderSummaryResponse( order.getId().toString(), order.getUser().getId().toString(), + order.getOrderShippingInfo().getCustomerName(), order.getStatus().toString(), - new MoneyResponse( - order.getTotalAmount().getAmount().doubleValue(), - order.getTotalAmount().getCurrency() - ), + order.getOrderLines().size(), + order.getTotalAmount(), order.getCreatedAt() ); } @@ -124,14 +119,8 @@ private OrderLineResponse toOrderLineResponse(OrderLineDTO line) { line.productId(), line.variantId(), line.quantity(), - new MoneyResponse( - line.unitPrice().getAmount().doubleValue(), - line.unitPrice().getCurrency() - ), - new MoneyResponse( - line.total().getAmount().doubleValue(), - line.total().getCurrency() - ) + line.unitPrice(), + line.total() ); } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentController.java index 01aa75c..da265a0 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentController.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentController.java @@ -1,8 +1,8 @@ package com.zenfulcode.commercify.api.payment; import com.zenfulcode.commercify.api.payment.mapper.PaymentDtoMapper; -import com.zenfulcode.commercify.api.payment.request.InitiatePaymentRequest; -import com.zenfulcode.commercify.api.payment.response.PaymentResponse; +import com.zenfulcode.commercify.api.payment.dto.request.InitiatePaymentRequest; +import com.zenfulcode.commercify.api.payment.dto.response.PaymentResponse; import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java index 2acd971..13a3f3d 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentWebhookController.java @@ -1,6 +1,6 @@ package com.zenfulcode.commercify.api.payment; -import com.zenfulcode.commercify.api.payment.request.MobilepayWebhookRegistrationRequest; +import com.zenfulcode.commercify.api.payment.dto.request.MobilepayWebhookRegistrationRequest; import com.zenfulcode.commercify.payment.application.service.MobilepayWebhookService; import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; import com.zenfulcode.commercify.payment.domain.model.PaymentProvider; diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java b/src/main/java/com/zenfulcode/commercify/api/payment/dto/request/InitiatePaymentRequest.java similarity index 53% rename from src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java rename to src/main/java/com/zenfulcode/commercify/api/payment/dto/request/InitiatePaymentRequest.java index be949a3..304e864 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/request/InitiatePaymentRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/dto/request/InitiatePaymentRequest.java @@ -1,10 +1,13 @@ -package com.zenfulcode.commercify.api.payment.request; +package com.zenfulcode.commercify.api.payment.dto.request; import com.zenfulcode.commercify.order.domain.valueobject.OrderId; public record InitiatePaymentRequest( - OrderId orderId, + String orderId, String provider, PaymentDetailsRequest paymentDetails ) { + public OrderId getOrderId() { + return OrderId.of(orderId); + } } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/request/MobilepayWebhookRegistrationRequest.java b/src/main/java/com/zenfulcode/commercify/api/payment/dto/request/MobilepayWebhookRegistrationRequest.java similarity index 55% rename from src/main/java/com/zenfulcode/commercify/api/payment/request/MobilepayWebhookRegistrationRequest.java rename to src/main/java/com/zenfulcode/commercify/api/payment/dto/request/MobilepayWebhookRegistrationRequest.java index 84d270d..d84aa39 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/request/MobilepayWebhookRegistrationRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/dto/request/MobilepayWebhookRegistrationRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.api.payment.request; +package com.zenfulcode.commercify.api.payment.dto.request; public record MobilepayWebhookRegistrationRequest(String callbackUrl) { } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java b/src/main/java/com/zenfulcode/commercify/api/payment/dto/request/PaymentDetailsRequest.java similarity index 76% rename from src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java rename to src/main/java/com/zenfulcode/commercify/api/payment/dto/request/PaymentDetailsRequest.java index 9eca57c..a4b1ad0 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/request/PaymentDetailsRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/dto/request/PaymentDetailsRequest.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.api.payment.request; +package com.zenfulcode.commercify.api.payment.dto.request; import java.util.Map; diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/response/PaymentResponse.java b/src/main/java/com/zenfulcode/commercify/api/payment/dto/response/PaymentResponse.java similarity index 72% rename from src/main/java/com/zenfulcode/commercify/api/payment/response/PaymentResponse.java rename to src/main/java/com/zenfulcode/commercify/api/payment/dto/response/PaymentResponse.java index 2a24e82..00123c5 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/response/PaymentResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/dto/response/PaymentResponse.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.api.payment.response; +package com.zenfulcode.commercify.api.payment.dto.response; import java.util.Map; diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java index f4e20be..8edf8fb 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.zenfulcode.commercify.api.payment.request.InitiatePaymentRequest; -import com.zenfulcode.commercify.api.payment.request.PaymentDetailsRequest; -import com.zenfulcode.commercify.api.payment.response.PaymentResponse; +import com.zenfulcode.commercify.api.payment.dto.request.InitiatePaymentRequest; +import com.zenfulcode.commercify.api.payment.dto.request.PaymentDetailsRequest; +import com.zenfulcode.commercify.api.payment.dto.response.PaymentResponse; import com.zenfulcode.commercify.order.application.service.OrderApplicationService; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; @@ -30,7 +30,7 @@ public class PaymentDtoMapper { private final ObjectMapper objectMapper; public InitiatePaymentCommand toCommand(InitiatePaymentRequest request) { - Order order = orderService.getOrderById(request.orderId()); + Order order = orderService.getOrderById(request.getOrderId()); return new InitiatePaymentCommand( order, diff --git a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java index 5b94515..f383220 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java @@ -43,7 +43,7 @@ public ResponseEntity> createProduct( // Return response CreateProductResponse response = new CreateProductResponse( - productId, + productId.getId(), "Product created successfully" ); diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AdjustInventoryRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AdjustInventoryRequest.java index 5c604c8..c3ec0d1 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AdjustInventoryRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/AdjustInventoryRequest.java @@ -2,7 +2,7 @@ public record AdjustInventoryRequest( String type, - int quantity, + Integer quantity, String reason ) { } diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java index 0e0c492..e28fe76 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java @@ -1,12 +1,14 @@ package com.zenfulcode.commercify.api.product.dto.request; +import com.zenfulcode.commercify.shared.domain.model.Money; + import java.util.List; public record CreateProductRequest( String name, String description, - int initialStock, - PriceRequest price, + Integer initialStock, + Money price, List variants ) { } diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateVariantRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateVariantRequest.java index 6b27e83..79a7af9 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateVariantRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateVariantRequest.java @@ -1,10 +1,12 @@ package com.zenfulcode.commercify.api.product.dto.request; +import com.zenfulcode.commercify.shared.domain.model.Money; + import java.util.List; public record CreateVariantRequest( Integer stock, - PriceRequest price, + Money price, String imageUrl, List options ) { diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/PriceRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/PriceRequest.java deleted file mode 100644 index 67028bf..0000000 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/PriceRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.zenfulcode.commercify.api.product.dto.request; - -public record PriceRequest( - double amount, - String currency -) { -} diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateProductRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateProductRequest.java index 11a78a0..df32707 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateProductRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/UpdateProductRequest.java @@ -1,10 +1,12 @@ package com.zenfulcode.commercify.api.product.dto.request; +import com.zenfulcode.commercify.shared.domain.model.Money; + public record UpdateProductRequest( String name, String description, Integer stock, - PriceRequest price, + Money price, Boolean active ) { } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantPriceUpdateRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantPriceUpdateRequest.java index 9e85e20..2c6de5e 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantPriceUpdateRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/VariantPriceUpdateRequest.java @@ -1,7 +1,9 @@ package com.zenfulcode.commercify.api.product.dto.request; +import com.zenfulcode.commercify.shared.domain.model.Money; + public record VariantPriceUpdateRequest( String sku, - PriceRequest price + Money price ) { } diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/CreateProductResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/CreateProductResponse.java index 0000ae2..bff9ea0 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/CreateProductResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/CreateProductResponse.java @@ -2,5 +2,8 @@ import com.zenfulcode.commercify.product.domain.valueobject.ProductId; -public record CreateProductResponse(ProductId productId, String message) { +public record CreateProductResponse(String productId, String message) { + public ProductId getProductId() { + return ProductId.of(productId); + } } diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java index 72088a0..d0e9c47 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductDetailResponse.java @@ -1,16 +1,18 @@ package com.zenfulcode.commercify.api.product.dto.response; -import com.zenfulcode.commercify.product.domain.valueobject.ProductId; +import com.zenfulcode.commercify.shared.domain.model.Money; import java.util.List; public record ProductDetailResponse( - ProductId id, + String id, String name, String description, + String imageUrl, int stock, - ProductSummaryResponse.ProductPriceResponse price, + Money price, boolean active, List variants ) { + } diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductSummaryResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductSummaryResponse.java index 69362b5..9f5ed59 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductSummaryResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductSummaryResponse.java @@ -1,24 +1,13 @@ package com.zenfulcode.commercify.api.product.dto.response; +import com.zenfulcode.commercify.shared.domain.model.Money; + public record ProductSummaryResponse( String id, String name, String description, String imageUrl, - ProductPriceResponse price, + Money price, int stock ) { - public record ProductPriceResponse( - double amount, - String currency - ) { - } - - public record ProductInventoryResponse( - int quantity, - String status, - boolean backorderable, - Integer reorderPoint - ) { - } } diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductVariantSummaryResponse.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductVariantSummaryResponse.java index 65f1370..fbc362d 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductVariantSummaryResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/response/ProductVariantSummaryResponse.java @@ -1,12 +1,14 @@ package com.zenfulcode.commercify.api.product.dto.response; +import com.zenfulcode.commercify.shared.domain.model.Money; + import java.util.List; public record ProductVariantSummaryResponse( String id, String sku, List options, - ProductSummaryResponse.ProductPriceResponse price, + Money price, int stock ) { public record VariantOptionResponse( diff --git a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java index 0779997..f67c783 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java @@ -2,13 +2,11 @@ import com.zenfulcode.commercify.api.product.dto.request.*; import com.zenfulcode.commercify.api.product.dto.response.ProductDetailResponse; -import com.zenfulcode.commercify.api.product.dto.response.ProductSummaryResponse; import com.zenfulcode.commercify.api.product.dto.response.ProductVariantSummaryResponse; import com.zenfulcode.commercify.product.application.command.*; import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.model.ProductVariant; import com.zenfulcode.commercify.product.domain.valueobject.*; -import com.zenfulcode.commercify.shared.domain.model.Money; import org.springframework.stereotype.Component; import java.util.List; @@ -19,13 +17,11 @@ public class ProductDtoMapper { public CreateProductCommand toCommand(CreateProductRequest request) { - Money price = new Money(request.price().amount(), request.price().currency()); - return new CreateProductCommand( request.name(), request.description(), request.initialStock(), - price, + request.price(), mapVariantSpecs(request.variants()) ); } @@ -52,8 +48,7 @@ public UpdateProductCommand toCommand(ProductId productId, UpdateProductRequest request.name(), request.description(), request.stock(), - request.price() != null ? - new Money(request.price().amount(), request.price().currency()) : null, + request.price(), request.active() ); return new UpdateProductCommand(productId, updateSpec); @@ -63,7 +58,7 @@ public UpdateVariantPricesCommand toCommand(ProductId productId, UpdateVariantPr List updates = request.updates().stream() .map(update -> new VariantPriceUpdate( update.sku(), - new Money(update.price().amount(), update.price().currency()) + update.price() )) .collect(Collectors.toList()); @@ -81,8 +76,7 @@ private List mapVariantSpecs(List va private VariantSpecification toVariantSpec(CreateVariantRequest request) { return new VariantSpecification( request.stock(), - request.price() != null ? - new Money(request.price().amount(), request.price().currency()) : null, + request.price() != null ? request.price() : null, request.imageUrl(), request.options().stream() .map(opt -> new VariantOption(opt.name(), opt.value())) @@ -101,25 +95,20 @@ private List mapVariants(Set vari opt.getValue() )) .collect(Collectors.toList()), - toPriceResponse(variant.getPrice()), + variant.getPrice(), variant.getStock() )) .collect(Collectors.toList()); } - private ProductSummaryResponse.ProductPriceResponse toPriceResponse(Money price) { - return new ProductSummaryResponse.ProductPriceResponse( - price.getAmount().doubleValue(), price.getCurrency() - ); - } - public ProductDetailResponse toDetailResponse(Product product) { return new ProductDetailResponse( - product.getId(), + product.getId().getId(), product.getName(), product.getDescription(), + product.getImageUrl(), product.getStock(), - toPriceResponse(product.getPrice()), + product.getPrice(), product.isActive(), mapVariants(product.getProductVariants()) ); diff --git a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java index 3f32c49..e4dd6a1 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductResponseMapper.java @@ -40,18 +40,11 @@ private ProductSummaryResponse toSummaryResponse(Product product) { product.getName(), product.getDescription(), product.getImageUrl(), - toPriceResponse(product), + product.getPrice(), product.getStock() ); } - private ProductSummaryResponse.ProductPriceResponse toPriceResponse(Product product) { - return new ProductSummaryResponse.ProductPriceResponse( - product.getPrice().getAmount().doubleValue(), - product.getPrice().getCurrency() - ); - } - private List toVariantResponses(Set variants) { return variants.stream() .map(this::toVariantResponse) @@ -63,7 +56,7 @@ private ProductVariantSummaryResponse toVariantResponse(ProductVariant variant) variant.getId().toString(), variant.getSku(), toVariantOptionResponses(variant.getVariantOptions()), - toVariantPriceResponse(variant), + variant.getPrice(), variant.getStock() != null ? variant.getStock() : variant.getProduct().getStock() ); } @@ -77,12 +70,4 @@ private List toVariantOptio )) .toList(); } - - private ProductSummaryResponse.ProductPriceResponse toVariantPriceResponse( - ProductVariant variant) { - return new ProductSummaryResponse.ProductPriceResponse( - variant.getPrice().getAmount().doubleValue(), - variant.getPrice().getCurrency() - ); - } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index f442718..e21fc41 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -3,6 +3,7 @@ import com.zenfulcode.commercify.auth.application.service.AuthenticationApplicationService; import com.zenfulcode.commercify.auth.infrastructure.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -19,6 +20,11 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; @Configuration @EnableWebSecurity @@ -27,6 +33,9 @@ public class SecurityConfig { private final UserDetailsService userDetailsService; + @Value("${frontend.host}") + private String frontendHost; + @Bean public JwtAuthenticationFilter jwtAuthenticationFilter( AuthenticationApplicationService authService) { @@ -41,7 +50,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, .requestMatchers( "/api/v2/auth/**", "/api/v2/products/active", - "/api/v2/products/{id}", + "/api/v2/products/{productId}", "/api/v2/payments/webhooks/{provider}/callback").permitAll() .anyRequest().authenticated() ) @@ -50,7 +59,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, ) .authenticationProvider(authenticationProvider()) .addFilterBefore(jwtAuthenticationFilter, - UsernamePasswordAuthenticationFilter.class); + UsernamePasswordAuthenticationFilter.class) + .cors(config -> config.configurationSource(corsConfigurationSource())); return http.build(); } @@ -73,4 +83,15 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(15); } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of(frontendHost, "http://localhost:3000")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); + configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/messaging/events/OrderEmailHandler.java similarity index 77% rename from src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java rename to src/main/java/com/zenfulcode/commercify/order/infrastructure/messaging/events/OrderEmailHandler.java index c012bd1..5cfd900 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/events/OrderEmailHandler.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/messaging/events/OrderEmailHandler.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.order.application.events; +package com.zenfulcode.commercify.order.infrastructure.messaging.events; import com.zenfulcode.commercify.order.application.service.OrderApplicationService; import com.zenfulcode.commercify.order.domain.event.OrderStatusChangedEvent; @@ -18,19 +18,6 @@ public class OrderEmailHandler { private final OrderApplicationService orderService; private final OrderEmailNotificationService notificationService; -// @Async -// @EventListener -// @Transactional(readOnly = true) -// public void handleOrderCreated(OrderCreatedEvent event) { -// try { -// log.info("Sending order confirmation notification for order: {}", event.getOrderId()); -// Order order = orderService.getOrderById(event.getOrderId()); -// notificationService.sendOrderConfirmation(order); -// } catch (Exception e) { -// log.error("Failed to send order confirmation notification", e); -// } -// } - @Async @EventListener @Transactional(readOnly = true) diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java index 16282ed..ccfef70 100644 --- a/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/notification/OrderEmailNotificationService.java @@ -27,7 +27,7 @@ public class OrderEmailNotificationService implements OrderNotificationService { @Value("${admin.email}") private String adminEmail; - @Value("${admin.order-dashboard") + @Value("${admin.order-dashboard}") private String orderDashboard; private static final String ORDER_CONFIRMATION_TEMPLATE = "order/confirmation-email"; diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/message/events/PaymentEventHandler.java similarity index 97% rename from src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java rename to src/main/java/com/zenfulcode/commercify/payment/infrastructure/message/events/PaymentEventHandler.java index 00f0b86..04bdf5a 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/events/PaymentEventHandler.java +++ b/src/main/java/com/zenfulcode/commercify/payment/infrastructure/message/events/PaymentEventHandler.java @@ -1,4 +1,4 @@ -package com.zenfulcode.commercify.payment.application.events; +package com.zenfulcode.commercify.payment.infrastructure.message.events; import com.zenfulcode.commercify.order.application.command.CancelOrderCommand; import com.zenfulcode.commercify.order.application.command.UpdateOrderStatusCommand; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ba57a59..3ad4c31 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -33,5 +33,6 @@ spring.mail.username=${MAIL_USERNAME} spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true +frontend.host=${FRONTEND_HOST} # Application Configuration #logging.level.org.springframework.security=debug \ No newline at end of file From 10d315cbc89681b82ccdc54995319c934a02587f Mon Sep 17 00:00:00 2001 From: GustavH Date: Sun, 9 Feb 2025 00:24:57 +0100 Subject: [PATCH 43/57] removing unused request --- .../product/dto/request/ProductVariantRequest.java | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/main/java/com/zenfulcode/commercify/api/product/dto/request/ProductVariantRequest.java diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/ProductVariantRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/ProductVariantRequest.java deleted file mode 100644 index c98e9aa..0000000 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/ProductVariantRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.zenfulcode.commercify.api.product.dto.request; - -import com.zenfulcode.commercify.product.domain.valueobject.VariantOption; -import com.zenfulcode.commercify.shared.domain.model.Money; - -import java.util.List; - -public record ProductVariantRequest( - int stock, - Money price, - String imageUrl, - List options -) { -} From 37c1980f14f6377547ab34e582e190d08b5bb3ed Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Mon, 24 Feb 2025 20:24:16 +0100 Subject: [PATCH 44/57] update CORS configuration and add admin order dashboard property --- .../java/com/zenfulcode/commercify/CommercifyApplication.java | 3 +-- .../commercify/auth/infrastructure/config/SecurityConfig.java | 3 +-- src/main/resources/application-docker.properties | 2 ++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java b/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java index 68b7527..6c169bc 100644 --- a/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java +++ b/src/main/java/com/zenfulcode/commercify/CommercifyApplication.java @@ -15,5 +15,4 @@ public static void main(String[] args) { public RestTemplate restTemplate() { return new RestTemplate(); } -} - +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index e21fc41..c80fbdf 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -87,9 +87,8 @@ public PasswordEncoder passwordEncoder() { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of(frontendHost, "http://localhost:3000")); + configuration.setAllowedOrigins(List.of(frontendHost, "http://localhost:3000", "http://localhost:5170")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); - configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index b46c1f4..56b4d37 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -17,6 +17,7 @@ security.jwt.refresh-token-expiration=86400000 # Admin Configuration admin.email=${ADMIN_EMAIL} admin.password=${ADMIN_PASSWORD} +admin.order-dashboard=${ADMIN_ORDER_DASHBOARD} # MobilePay Configuration integration.payments.mobilepay.client-id=${MOBILEPAY_CLIENT_ID} integration.payments.mobilepay.merchant-id=${MOBILEPAY_MERCHANT_ID} @@ -32,5 +33,6 @@ spring.mail.username=${MAIL_USERNAME} spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true +frontend.host=${FRONTEND_HOST} # Application Configuration #logging.level.org.springframework.security=debug \ No newline at end of file From 59ddede6db1f963238bb6cdf06cd6e8b73642ee5 Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Fri, 28 Feb 2025 19:29:09 +0100 Subject: [PATCH 45/57] Add CapturedPaymentResponse and imageUrl to product commands and requests --- .../api/payment/PaymentAdminController.java | 6 ++++-- .../payment/dto/response/CapturedPaymentResponse.java | 11 +++++++++++ .../api/payment/mapper/PaymentDtoMapper.java | 10 ++++++++++ .../api/product/dto/request/CreateProductRequest.java | 1 + .../api/product/mapper/ProductDtoMapper.java | 1 + .../application/command/CreateProductCommand.java | 1 + .../service/ProductApplicationService.java | 1 + .../commercify/product/domain/model/Product.java | 3 ++- .../product/domain/service/ProductFactory.java | 2 +- .../domain/valueobject/ProductSpecification.java | 1 + 10 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/api/payment/dto/response/CapturedPaymentResponse.java diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java index 4dac3e7..422a6d9 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java @@ -1,5 +1,6 @@ package com.zenfulcode.commercify.api.payment; +import com.zenfulcode.commercify.api.payment.dto.response.CapturedPaymentResponse; import com.zenfulcode.commercify.api.payment.mapper.PaymentDtoMapper; import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; import com.zenfulcode.commercify.payment.application.dto.CapturedPayment; @@ -23,11 +24,12 @@ public class PaymentAdminController { private final PaymentDtoMapper paymentDtoMapper; @PostMapping("/{paymentId}/capture") - public ResponseEntity> capturePayment( + public ResponseEntity> capturePayment( @PathVariable String paymentId) { CapturePaymentCommand command = paymentDtoMapper.toCaptureCommand(PaymentId.of(paymentId)); - CapturedPayment response = paymentService.capturePayment(command); + CapturedPayment capturedPayment = paymentService.capturePayment(command); + CapturedPaymentResponse response = paymentDtoMapper.toCapturedResponse(capturedPayment); return ResponseEntity.ok(ApiResponse.success(response)); } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/dto/response/CapturedPaymentResponse.java b/src/main/java/com/zenfulcode/commercify/api/payment/dto/response/CapturedPaymentResponse.java new file mode 100644 index 0000000..9be8fa3 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/payment/dto/response/CapturedPaymentResponse.java @@ -0,0 +1,11 @@ +package com.zenfulcode.commercify.api.payment.dto.response; + +import com.zenfulcode.commercify.payment.application.dto.CaptureAmount; +import com.zenfulcode.commercify.payment.domain.valueobject.TransactionId; + +public record CapturedPaymentResponse( + TransactionId transactionId, + CaptureAmount captureAmount, + boolean isFullyCaptured +) { +} diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java index 8edf8fb..8b4c16c 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java @@ -4,11 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.zenfulcode.commercify.api.payment.dto.request.InitiatePaymentRequest; import com.zenfulcode.commercify.api.payment.dto.request.PaymentDetailsRequest; +import com.zenfulcode.commercify.api.payment.dto.response.CapturedPaymentResponse; import com.zenfulcode.commercify.api.payment.dto.response.PaymentResponse; import com.zenfulcode.commercify.order.application.service.OrderApplicationService; import com.zenfulcode.commercify.order.domain.model.Order; import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; +import com.zenfulcode.commercify.payment.application.dto.CapturedPayment; import com.zenfulcode.commercify.payment.application.dto.InitializedPayment; import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; import com.zenfulcode.commercify.payment.domain.exception.WebhookProcessingException; @@ -71,4 +73,12 @@ private PaymentProviderRequest toProviderRequest(PaymentDetailsRequest details) details.returnUrl() ); } + + public CapturedPaymentResponse toCapturedResponse(CapturedPayment capturedPayment) { + return new CapturedPaymentResponse( + capturedPayment.transactionId(), + capturedPayment.captureAmount(), + capturedPayment.isFullyCaptured() + ); + } } diff --git a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java index e28fe76..90d7a03 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/dto/request/CreateProductRequest.java @@ -7,6 +7,7 @@ public record CreateProductRequest( String name, String description, + String imageUrl, Integer initialStock, Money price, List variants diff --git a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java index f67c783..79abe53 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/mapper/ProductDtoMapper.java @@ -20,6 +20,7 @@ public CreateProductCommand toCommand(CreateProductRequest request) { return new CreateProductCommand( request.name(), request.description(), + request.imageUrl(), request.initialStock(), request.price(), mapVariantSpecs(request.variants()) diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/CreateProductCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/CreateProductCommand.java index b719c77..2fe25ff 100644 --- a/src/main/java/com/zenfulcode/commercify/product/application/command/CreateProductCommand.java +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/CreateProductCommand.java @@ -8,6 +8,7 @@ public record CreateProductCommand( String name, String description, + String imageUrl, int initialStock, Money price, List variantSpecs diff --git a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java index f6b2bf6..8a025f6 100644 --- a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java @@ -35,6 +35,7 @@ public ProductId createProduct(CreateProductCommand command) { ProductSpecification spec = new ProductSpecification( command.name(), command.description(), + command.imageUrl(), command.initialStock(), command.price(), command.variantSpecs() diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java index 9c18acf..08240e2 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/Product.java @@ -66,11 +66,12 @@ public class Product extends AggregateRoot { @Column(name = "updated_at") private Instant updatedAt; - public static Product create(String name, String description, int stock, Money money) { + public static Product create(String name, String description, String imageUrl, int stock, Money money) { Product product = new Product(); product.id = ProductId.generate(); product.name = Objects.requireNonNull(name, "Product name is required"); product.description = description; + product.imageUrl = imageUrl; product.stock = stock; product.price = Objects.requireNonNull(money, "Product price is required"); product.active = true; diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java index 16f97d1..a184d41 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductFactory.java @@ -24,7 +24,7 @@ public Product createProduct(ProductSpecification spec) { validateSpecification(spec); // Create base product - Product product = Product.create(spec.name(), spec.description(), spec.initialStock(), spec.price()); + Product product = Product.create(spec.name(), spec.description(), spec.imageUrl(), spec.initialStock(), spec.price()); // Apply default policies pricingPolicy.applyDefaultPricing(product); diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductSpecification.java b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductSpecification.java index b92772c..4aa9f84 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductSpecification.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/valueobject/ProductSpecification.java @@ -7,6 +7,7 @@ public record ProductSpecification( String name, String description, + String imageUrl, int initialStock, Money price, List variantSpecs From bd4e91722affc9b619b11f012e263ace2f420d16 Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Fri, 28 Feb 2025 23:32:04 +0100 Subject: [PATCH 46/57] Implement NextAuth endpoints for authentication and session validation --- .../commercify/api/auth/AuthController.java | 65 ++++++++++++------- .../auth/dto/response/NextAuthResponse.java | 33 ++++++++++ .../AuthenticationApplicationService.java | 9 +++ .../InvalidAuthenticationException.java | 9 +++ .../infrastructure/config/SecurityConfig.java | 10 ++- .../security/CustomUserDetailsService.java | 1 + 6 files changed, 100 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/api/auth/dto/response/NextAuthResponse.java create mode 100644 src/main/java/com/zenfulcode/commercify/auth/domain/exception/InvalidAuthenticationException.java diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java index 987d4a0..bb38ad9 100644 --- a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java +++ b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java @@ -4,17 +4,20 @@ import com.zenfulcode.commercify.api.auth.dto.request.RefreshTokenRequest; import com.zenfulcode.commercify.api.auth.dto.request.RegisterRequest; import com.zenfulcode.commercify.api.auth.dto.response.AuthResponse; +import com.zenfulcode.commercify.api.auth.dto.response.NextAuthResponse; import com.zenfulcode.commercify.auth.application.service.AuthenticationApplicationService; import com.zenfulcode.commercify.auth.application.service.AuthenticationResult; +import com.zenfulcode.commercify.auth.domain.exception.InvalidAuthenticationException; +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; import com.zenfulcode.commercify.user.application.service.UserApplicationService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +@Slf4j @RestController @RequestMapping("/api/v2/auth") @RequiredArgsConstructor @@ -22,43 +25,55 @@ public class AuthController { private final AuthenticationApplicationService authService; private final UserApplicationService userService; + @PostMapping("/nextauth") + public ResponseEntity> nextAuthSignIn(@RequestBody LoginRequest request) { + log.info("Next auth request: {}", request); + + // Authenticate through the application service + AuthenticationResult result = authService.authenticate(request.email(), request.password()); + + // Create and return the NextAuth response + return ResponseEntity.ok(ApiResponse.success(NextAuthResponse.from(result))); + } + + @GetMapping("/session") + public ResponseEntity> validateSession(@RequestHeader("Authorization") String authHeader) { + try { + // Extract token using a domain service method + String token = authService.extractTokenFromHeader(authHeader).orElseThrow(() -> new InvalidAuthenticationException("Invalid authorization header")); + + // Validate token through the application service + AuthenticatedUser user = authService.validateAccessToken(token); + + // Create and return the NextAuth session response + return ResponseEntity.ok(ApiResponse.success(NextAuthResponse.fromUser(user))); + } catch (InvalidAuthenticationException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } + @PostMapping("/signin") - public ResponseEntity> login( - @RequestBody LoginRequest request) { - AuthenticationResult result = authService.authenticate( - request.email(), - request.password() - ); + public ResponseEntity> login(@RequestBody LoginRequest request) { + AuthenticationResult result = authService.authenticate(request.email(), request.password()); AuthResponse response = AuthResponse.from(result); return ResponseEntity.ok(ApiResponse.success(response)); } @PostMapping("/signup") - public ResponseEntity> register( - @RequestBody RegisterRequest request) { + public ResponseEntity> register(@RequestBody RegisterRequest request) { - userService.registerUser( - request.firstName(), - request.lastName(), - request.email(), - request.password(), - request.phone() - ); + userService.registerUser(request.firstName(), request.lastName(), request.email(), request.password(), request.phone()); // Authenticate the newly registered user - AuthenticationResult result = authService.authenticate( - request.email(), - request.password() - ); + AuthenticationResult result = authService.authenticate(request.email(), request.password()); AuthResponse response = AuthResponse.from(result); return ResponseEntity.ok(ApiResponse.success(response)); } @PostMapping("/refresh") - public ResponseEntity> refreshToken( - @RequestBody RefreshTokenRequest request) { + public ResponseEntity> refreshToken(@RequestBody RefreshTokenRequest request) { AuthenticationResult result = authService.refreshToken(request.refreshToken()); AuthResponse response = AuthResponse.from(result); diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/NextAuthResponse.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/NextAuthResponse.java new file mode 100644 index 0000000..3425ed2 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/NextAuthResponse.java @@ -0,0 +1,33 @@ +package com.zenfulcode.commercify.api.auth.dto.response; + +import com.zenfulcode.commercify.auth.application.service.AuthenticationResult; +import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; + +public record NextAuthResponse( + String id, + String name, + String email, + String accessToken, + String refreshToken +) { + public static NextAuthResponse from(AuthenticationResult result) { + AuthenticatedUser user = result.user(); + return new NextAuthResponse( + user.getUserId().toString(), + user.getUsername(), + user.getEmail(), + result.accessToken(), + result.refreshToken() + ); + } + + public static NextAuthResponse fromUser(AuthenticatedUser user) { + return new NextAuthResponse( + user.getUserId().toString(), + user.getUsername(), + user.getEmail(), + null, + null + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java index dd0a259..038b6de 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java @@ -15,6 +15,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @AllArgsConstructor public class AuthenticationApplicationService { @@ -65,4 +67,11 @@ public AuthenticationResult refreshToken(String refreshToken) { return new AuthenticationResult(newAccessToken, newRefreshToken, authenticatedUser); } + + public Optional extractTokenFromHeader(String authHeader) { + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return Optional.empty(); + } + return Optional.of(authHeader.substring(7)); + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/exception/InvalidAuthenticationException.java b/src/main/java/com/zenfulcode/commercify/auth/domain/exception/InvalidAuthenticationException.java new file mode 100644 index 0000000..686e6ec --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/exception/InvalidAuthenticationException.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.auth.domain.exception; + +import com.zenfulcode.commercify.shared.domain.exception.DomainException; + +public class InvalidAuthenticationException extends DomainException { + public InvalidAuthenticationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index c80fbdf..d47acea 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -48,7 +48,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(req -> req .requestMatchers( - "/api/v2/auth/**", + "/api/v2/auth/**", // This should cover your NextAuth endpoints + "/api/v2/auth/nextauth", // Add explicitly + "/api/v2/auth/session", "/api/v2/products/active", "/api/v2/products/{productId}", "/api/v2/payments/webhooks/{provider}/callback").permitAll() @@ -88,7 +90,11 @@ public PasswordEncoder passwordEncoder() { public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of(frontendHost, "http://localhost:3000", "http://localhost:5170")); - configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With")); + configuration.setAllowCredentials(true); + configuration.setExposedHeaders(List.of("Authorization")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/CustomUserDetailsService.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/CustomUserDetailsService.java index aca7a06..72b236c 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/CustomUserDetailsService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/CustomUserDetailsService.java @@ -24,6 +24,7 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx return AuthenticatedUser.builder() .userId(user.getId().toString()) .email(user.getEmail()) + .username(user.getFullName()) .password(user.getPassword()) .roles(userRoleMapper.mapRoles(user.getRoles())) .build(); From ab780f05ee25f34eb1d52f1c55f279ead3cb9f0c Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Sat, 1 Mar 2025 01:22:17 +0100 Subject: [PATCH 47/57] Add roles to NextAuthResponse for enhanced user role management --- .../api/auth/dto/response/NextAuthResponse.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/NextAuthResponse.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/NextAuthResponse.java index 3425ed2..25b1a14 100644 --- a/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/NextAuthResponse.java +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/response/NextAuthResponse.java @@ -2,13 +2,17 @@ import com.zenfulcode.commercify.auth.application.service.AuthenticationResult; import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; +import com.zenfulcode.commercify.auth.domain.model.UserRole; + +import java.util.Set; public record NextAuthResponse( String id, String name, String email, String accessToken, - String refreshToken + String refreshToken, + Set roles ) { public static NextAuthResponse from(AuthenticationResult result) { AuthenticatedUser user = result.user(); @@ -17,7 +21,8 @@ public static NextAuthResponse from(AuthenticationResult result) { user.getUsername(), user.getEmail(), result.accessToken(), - result.refreshToken() + result.refreshToken(), + user.getRoles() ); } @@ -27,7 +32,8 @@ public static NextAuthResponse fromUser(AuthenticatedUser user) { user.getUsername(), user.getEmail(), null, - null + null, + user.getRoles() ); } } \ No newline at end of file From b52464a0c498af055e55beb9d4fd5d5200dbeac6 Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Tue, 4 Mar 2025 12:55:16 +0100 Subject: [PATCH 48/57] Refactor product retrieval methods for improved clarity and exception handling - fixing missing variant options --- .../commercify/api/product/ProductController.java | 3 +-- .../application/service/ProductApplicationService.java | 5 ++--- .../commercify/product/domain/model/ProductVariant.java | 2 +- .../product/domain/service/ProductDomainService.java | 9 +++++++++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java index f383220..e8018b4 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java @@ -53,8 +53,7 @@ public ResponseEntity> createProduct( @GetMapping("/{productId}") public ResponseEntity> getProduct( @PathVariable String productId) { - - Product product = productApplicationService.getProduct(ProductId.of(productId)); + Product product = productApplicationService.getProductById(ProductId.of(productId)); ProductDetailResponse response = dtoMapper.toDetailResponse(product); return ResponseEntity.ok(ApiResponse.success(response)); diff --git a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java index 8a025f6..e1d1c4e 100644 --- a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java @@ -170,9 +170,8 @@ public Page findProducts(ProductQuery query, Pageable pageable) { } @Transactional(readOnly = true) - public Product getProduct(ProductId productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new ProductNotFoundException(productId)); + public Product getProductById(ProductId productId) { + return productDomainService.getProductById(productId); } @Transactional(readOnly = true) diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java index 24004ec..6214dcc 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/model/ProductVariant.java @@ -40,7 +40,7 @@ public class ProductVariant { }) private Money price; - @OneToMany(mappedBy = "productVariant") + @OneToMany(mappedBy = "productVariant", fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Set variantOptions = new LinkedHashSet<>(); public static ProductVariant create(String sku, Integer stock, Money price, String imageUrl) { diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java index 99f2a87..5dae41d 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java @@ -2,10 +2,12 @@ import com.zenfulcode.commercify.order.domain.repository.OrderLineRepository; import com.zenfulcode.commercify.product.domain.exception.InvalidPriceException; +import com.zenfulcode.commercify.product.domain.exception.ProductNotFoundException; import com.zenfulcode.commercify.product.domain.exception.ProductValidationException; import com.zenfulcode.commercify.product.domain.exception.VariantNotFoundException; import com.zenfulcode.commercify.product.domain.model.Product; import com.zenfulcode.commercify.product.domain.model.ProductVariant; +import com.zenfulcode.commercify.product.domain.repository.ProductRepository; import com.zenfulcode.commercify.product.domain.valueobject.*; import com.zenfulcode.commercify.shared.domain.model.Money; import lombok.RequiredArgsConstructor; @@ -19,6 +21,7 @@ @RequiredArgsConstructor public class ProductDomainService { private final OrderLineRepository orderLineRepository; + private final ProductRepository productRepository; private final ProductInventoryPolicy inventoryPolicy; private final ProductPricingPolicy pricingPolicy; private final ProductFactory productFactory; @@ -188,6 +191,12 @@ public void updateProduct(Product product, ProductUpdateSpec updateSpec) { inventoryPolicy.initializeInventory(product); } } + + public Product getProductById(ProductId productId) { + return productRepository.findById(productId).orElseThrow( + () -> new ProductNotFoundException(productId) + ); + } } From f5df6eb161f28089e5d55a6088614897c4abc244 Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Fri, 14 Mar 2025 17:13:49 +0100 Subject: [PATCH 49/57] Add GetOrderByIdCommand to streamline order retrieval process --- .../zenfulcode/commercify/api/order/OrderController.java | 5 ++++- .../commercify/api/order/mapper/OrderDtoMapper.java | 5 +++++ .../order/application/command/GetOrderByIdCommand.java | 9 +++++++++ .../application/service/OrderApplicationService.java | 4 +++- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/command/GetOrderByIdCommand.java diff --git a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java index 36fe57f..8ea7ffe 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/OrderController.java @@ -8,6 +8,7 @@ import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; import com.zenfulcode.commercify.order.application.command.CancelOrderCommand; import com.zenfulcode.commercify.order.application.command.CreateOrderCommand; +import com.zenfulcode.commercify.order.application.command.GetOrderByIdCommand; import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; @@ -61,7 +62,9 @@ public ResponseEntity> createOrder( public ResponseEntity> getOrder( @PathVariable String orderId, Authentication authentication) { - OrderDetailsDTO order = orderApplicationService.getOrderDetailsById(OrderId.of(orderId)); + GetOrderByIdCommand command = orderDtoMapper.toCommand(orderId); + + OrderDetailsDTO order = orderApplicationService.getOrderDetailsById(command); AuthenticatedUser user = (AuthenticatedUser) authentication.getPrincipal(); if (isNotUserAuthorized(user, order.userId().getId())) { diff --git a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java index baa1273..c9dfedd 100644 --- a/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/order/mapper/OrderDtoMapper.java @@ -5,6 +5,7 @@ import com.zenfulcode.commercify.api.order.dto.response.*; import com.zenfulcode.commercify.api.product.dto.response.PageInfo; import com.zenfulcode.commercify.order.application.command.CreateOrderCommand; +import com.zenfulcode.commercify.order.application.command.GetOrderByIdCommand; import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; import com.zenfulcode.commercify.order.application.dto.OrderLineDTO; import com.zenfulcode.commercify.order.domain.model.Order; @@ -60,6 +61,10 @@ public CreateOrderCommand toCommand(CreateOrderRequest request) { ); } + public GetOrderByIdCommand toCommand(String orderId) { + return new GetOrderByIdCommand(orderId); + } + private OrderLineDetails toOrderLineDetails(CreateOrderLineRequest request) { return new OrderLineDetails( ProductId.of(request.productId()), diff --git a/src/main/java/com/zenfulcode/commercify/order/application/command/GetOrderByIdCommand.java b/src/main/java/com/zenfulcode/commercify/order/application/command/GetOrderByIdCommand.java new file mode 100644 index 0000000..c28c2d5 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/command/GetOrderByIdCommand.java @@ -0,0 +1,9 @@ +package com.zenfulcode.commercify.order.application.command; + +public record GetOrderByIdCommand(String orderId) { + public GetOrderByIdCommand { + if (orderId == null || orderId.isBlank()) { + throw new IllegalArgumentException("Order ID cannot be null or empty"); + } + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java index cb9590d..07e3780 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java @@ -2,6 +2,7 @@ import com.zenfulcode.commercify.order.application.command.CancelOrderCommand; import com.zenfulcode.commercify.order.application.command.CreateOrderCommand; +import com.zenfulcode.commercify.order.application.command.GetOrderByIdCommand; import com.zenfulcode.commercify.order.application.command.UpdateOrderStatusCommand; import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; @@ -100,7 +101,8 @@ public Page findAllOrders(FindAllOrdersQuery query) { } @Transactional(readOnly = true) - public OrderDetailsDTO getOrderDetailsById(OrderId orderId) { + public OrderDetailsDTO getOrderDetailsById(GetOrderByIdCommand command) { + OrderId orderId = OrderId.of(command.orderId()); Order order = getOrderById(orderId); return OrderDetailsDTO.fromOrder(order); } From 9659886a05c69361dfbe38c0077faa957c95fedd Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Sat, 22 Mar 2025 22:31:02 +0100 Subject: [PATCH 50/57] Refactor authentication process to use LoginCommand and enhance guest user handling --- .../commercify/api/auth/AuthController.java | 6 +- .../api/auth/dto/request/LoginRequest.java | 8 ++- .../application/command/LoginCommand.java | 4 ++ .../AuthenticationApplicationService.java | 56 +++++++++++++++---- .../domain/event/UserAuthenticatedEvent.java | 6 +- .../infrastructure/config/SecurityConfig.java | 3 +- .../security/JwtAuthenticationFilter.java | 2 + .../command/CreateUserCommand.java | 1 + .../service/UserApplicationService.java | 37 ++---------- .../exception/UserNotFoundException.java | 5 ++ 10 files changed, 77 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/auth/application/command/LoginCommand.java diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java index bb38ad9..f73573c 100644 --- a/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java +++ b/src/main/java/com/zenfulcode/commercify/api/auth/AuthController.java @@ -30,7 +30,7 @@ public ResponseEntity> nextAuthSignIn(@RequestBody log.info("Next auth request: {}", request); // Authenticate through the application service - AuthenticationResult result = authService.authenticate(request.email(), request.password()); + AuthenticationResult result = authService.authenticate(request.toCommand()); // Create and return the NextAuth response return ResponseEntity.ok(ApiResponse.success(NextAuthResponse.from(result))); @@ -54,7 +54,7 @@ public ResponseEntity> validateSession(@RequestHea @PostMapping("/signin") public ResponseEntity> login(@RequestBody LoginRequest request) { - AuthenticationResult result = authService.authenticate(request.email(), request.password()); + AuthenticationResult result = authService.authenticate(request.toCommand()); AuthResponse response = AuthResponse.from(result); return ResponseEntity.ok(ApiResponse.success(response)); @@ -66,7 +66,7 @@ public ResponseEntity> register(@RequestBody RegisterR userService.registerUser(request.firstName(), request.lastName(), request.email(), request.password(), request.phone()); // Authenticate the newly registered user - AuthenticationResult result = authService.authenticate(request.email(), request.password()); + AuthenticationResult result = authService.authenticate(new LoginRequest(request.email(), request.password(), false).toCommand()); AuthResponse response = AuthResponse.from(result); return ResponseEntity.ok(ApiResponse.success(response)); diff --git a/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java index 150d713..4276f41 100644 --- a/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java +++ b/src/main/java/com/zenfulcode/commercify/api/auth/dto/request/LoginRequest.java @@ -1,7 +1,13 @@ package com.zenfulcode.commercify.api.auth.dto.request; +import com.zenfulcode.commercify.auth.application.command.LoginCommand; + public record LoginRequest( String email, - String password + String password, + Boolean isGuest ) { + public LoginCommand toCommand() { + return new LoginCommand(email, password, isGuest != null && isGuest); + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/command/LoginCommand.java b/src/main/java/com/zenfulcode/commercify/auth/application/command/LoginCommand.java new file mode 100644 index 0000000..a8c48ab --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/auth/application/command/LoginCommand.java @@ -0,0 +1,4 @@ +package com.zenfulcode.commercify.auth.application.command; + +public record LoginCommand(String email, String password, boolean isGuest) { +} diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java index 038b6de..6aa24f9 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java @@ -1,14 +1,21 @@ package com.zenfulcode.commercify.auth.application.service; +import com.zenfulcode.commercify.auth.application.command.LoginCommand; import com.zenfulcode.commercify.auth.domain.event.UserAuthenticatedEvent; import com.zenfulcode.commercify.auth.domain.model.AuthenticatedUser; import com.zenfulcode.commercify.auth.domain.service.AuthenticationDomainService; import com.zenfulcode.commercify.auth.infrastructure.security.TokenService; import com.zenfulcode.commercify.shared.domain.event.DomainEventPublisher; +import com.zenfulcode.commercify.user.application.command.CreateUserCommand; +import com.zenfulcode.commercify.user.application.service.UserApplicationService; +import com.zenfulcode.commercify.user.domain.exception.UserAlreadyExistsException; +import com.zenfulcode.commercify.user.domain.exception.UserNotFoundException; import com.zenfulcode.commercify.user.domain.model.User; +import com.zenfulcode.commercify.user.domain.model.UserRole; import com.zenfulcode.commercify.user.domain.repository.UserRepository; import com.zenfulcode.commercify.user.domain.valueobject.UserId; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -16,7 +23,10 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Optional; +import java.util.Set; +import java.util.UUID; +@Slf4j @Service @AllArgsConstructor public class AuthenticationApplicationService { @@ -25,23 +35,47 @@ public class AuthenticationApplicationService { private final UserRepository userRepository; private final TokenService tokenService; private final DomainEventPublisher eventPublisher; + private final UserApplicationService userApplicationService; @Transactional - public AuthenticationResult authenticate(String email, String password) { - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(email, password) - ); + public AuthenticationResult authenticate(LoginCommand command) { + String email = command.email(); + String password = command.password(); + + UserId userId; + if (!command.isGuest()) { + User user = userRepository.findByEmail(command.email()).orElseThrow(() -> new UserNotFoundException(command.email())); + + userId = user.getId(); + } else { + Optional user = userRepository.findByEmail(command.email()); + if (user.isPresent()) { + throw new UserAlreadyExistsException("There is already a user with this email"); + } + + email = "guest-" + System.currentTimeMillis() + "@commercify.com"; + password = UUID.randomUUID().toString(); + + CreateUserCommand createUserCommand = new CreateUserCommand(email, "Guest", "User", password, Set.of(UserRole.GUEST), null); + + userId = userApplicationService.createUser(createUserCommand); + } + + log.info("Authenticating"); + Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password)); + + log.info("Authenticated"); AuthenticatedUser authenticatedUser = (AuthenticatedUser) authentication.getPrincipal(); + log.info("Authenticated user: {}", authenticatedUser); + // Generate tokens String accessToken = tokenService.generateAccessToken(authenticatedUser); String refreshToken = tokenService.generateRefreshToken(authenticatedUser); // Publish domain event - User user = userRepository.findByEmail(email) - .orElseThrow(); // User must exist at this point - eventPublisher.publish(new UserAuthenticatedEvent(this, user.getId(), email)); + eventPublisher.publish(new UserAuthenticatedEvent(this, userId, email, command.isGuest())); return new AuthenticationResult(accessToken, refreshToken, authenticatedUser); } @@ -49,16 +83,16 @@ public AuthenticationResult authenticate(String email, String password) { @Transactional(readOnly = true) public AuthenticatedUser validateAccessToken(String token) { String userId = tokenService.validateTokenAndGetUserId(token); - User user = userRepository.findById(UserId.of(userId)) - .orElseThrow(); + log.info("Validating access token for user: '{}'", userId); + + User user = userRepository.findById(UserId.of(userId)).orElseThrow(); return authenticationDomainService.createAuthenticatedUser(user); } @Transactional public AuthenticationResult refreshToken(String refreshToken) { String userId = tokenService.validateTokenAndGetUserId(refreshToken); - User user = userRepository.findById(UserId.of(userId)) - .orElseThrow(); + User user = userRepository.findById(UserId.of(userId)).orElseThrow(); AuthenticatedUser authenticatedUser = authenticationDomainService.createAuthenticatedUser(user); diff --git a/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java b/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java index 7b3273a..cfce0e8 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java +++ b/src/main/java/com/zenfulcode/commercify/auth/domain/event/UserAuthenticatedEvent.java @@ -10,15 +10,17 @@ public class UserAuthenticatedEvent extends DomainEvent { @AggregateId private final UserId userId; private final String username; + private final boolean isGuest; - public UserAuthenticatedEvent(Object source, UserId userId, String username) { + public UserAuthenticatedEvent(Object source, UserId userId, String username, boolean isGuest) { super(source); this.userId = userId; this.username = username; + this.isGuest = isGuest; } @Override public String getEventType() { - return "USER_AUTHENTICATED"; + return isGuest ? "GUEST_AUTHENTICATED" : "USER_AUTHENTICATED"; } } diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index d47acea..6704483 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -60,8 +60,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authenticationProvider(authenticationProvider()) - .addFilterBefore(jwtAuthenticationFilter, - UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .cors(config -> config.configurationSource(corsConfigurationSource())); return http.build(); diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java index 12b5cbc..3460cbd 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java @@ -30,6 +30,8 @@ protected void doFilterInternal( try { String token = extractJwtToken(request); + System.out.println("Token: " + token); + if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) { AuthenticatedUser user = authService.validateAccessToken(token); diff --git a/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java b/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java index e2f94b9..0063a9a 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java +++ b/src/main/java/com/zenfulcode/commercify/user/application/command/CreateUserCommand.java @@ -4,6 +4,7 @@ import java.util.Set; + public record CreateUserCommand( String email, String firstName, diff --git a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java index 79e6348..c90249a 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java @@ -34,15 +34,7 @@ public UserId createUser(CreateUserCommand command) { // Hash the password String hashedPassword = passwordEncoder.encode(command.password()); - UserSpecification userSpecification = new UserSpecification( - command.firstName(), - command.lastName(), - command.email(), - hashedPassword, - command.phoneNumber(), - UserStatus.PENDING, - command.roles() - ); + UserSpecification userSpecification = new UserSpecification(command.firstName(), command.lastName(), command.email(), hashedPassword, command.phoneNumber(), UserStatus.PENDING, command.roles()); // Create the user through domain service User user = userDomainService.createUser(userSpecification); @@ -53,29 +45,10 @@ public UserId createUser(CreateUserCommand command) { } @Transactional - public void registerUser( - String firstName, - String lastName, - String email, - String password, - String phone - ) { - // Create user specification - UserSpecification spec = UserSpecification.builder() - .firstName(firstName) - .lastName(lastName) - .email(email) - .password(passwordEncoder.encode(password)) - .phone(phone) - .status(UserStatus.ACTIVE) - .roles(Set.of(UserRole.USER)) - .build(); - - // Create user through domain service - User user = userDomainService.createUser(spec); - - // Publish domain event - eventPublisher.publish(user.getDomainEvents()); + public void registerUser(String firstName, String lastName, String email, String password, String phone) { + CreateUserCommand createUserCommand = new CreateUserCommand(email, firstName, lastName, password, Set.of(UserRole.USER), phone); + + createUser(createUserCommand); } /** diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java index b4b8f91..f0bd051 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/exception/UserNotFoundException.java @@ -13,6 +13,11 @@ public UserNotFoundException(UserId userId) { this.identifier = userId; } + public UserNotFoundException(String email) { + super("User not found with Email: " + email); + this.identifier = email; + } + public UserNotFoundException(String message, String identifier) { super(message + ": " + identifier); this.identifier = identifier; From 74f1101b8031961238c295a10666a617bc6d9f10 Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Sat, 22 Mar 2025 22:32:08 +0100 Subject: [PATCH 51/57] Refactor authentication logging to reduce verbosity --- .../service/AuthenticationApplicationService.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java index 6aa24f9..83bd6c1 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/auth/application/service/AuthenticationApplicationService.java @@ -61,15 +61,9 @@ public AuthenticationResult authenticate(LoginCommand command) { userId = userApplicationService.createUser(createUserCommand); } - log.info("Authenticating"); Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password)); - - log.info("Authenticated"); - AuthenticatedUser authenticatedUser = (AuthenticatedUser) authentication.getPrincipal(); - log.info("Authenticated user: {}", authenticatedUser); - // Generate tokens String accessToken = tokenService.generateAccessToken(authenticatedUser); String refreshToken = tokenService.generateRefreshToken(authenticatedUser); From 2caa0a121f8342f1a415bf1ad8862696fa534198 Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Mon, 24 Mar 2025 21:43:42 +0100 Subject: [PATCH 52/57] fix: Orders with invalid product id does not throw error --- .../service/ProductApplicationService.java | 2 +- .../domain/service/ProductDomainService.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java index e1d1c4e..93b2512 100644 --- a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java @@ -153,7 +153,7 @@ public void deleteProduct(DeleteProductCommand command) { @Transactional(readOnly = true) public List findAllProducts(Collection productIds) { - return productRepository.findAllById(productIds); + return productDomainService.getAllProductsById(productIds); } /** diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java index 5dae41d..1e97402 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java @@ -14,6 +14,7 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; @@ -197,6 +198,20 @@ public Product getProductById(ProductId productId) { () -> new ProductNotFoundException(productId) ); } + + public List getAllProductsById(Collection productIds) { + List products = productRepository.findAllById(productIds); + + if (products.size() != productIds.size()) { + for (ProductId prodId : productIds) { + if (products.stream().noneMatch(p -> p.getId().equals(prodId))) { + throw new ProductNotFoundException(prodId); + } + } + } + + return products; + } } From 7671ed9944832a256716d50ee2e45b207f67bb8d Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Tue, 25 Mar 2025 18:02:04 +0100 Subject: [PATCH 53/57] feat: Enhance product retrieval with active filter option --- .../api/product/ProductController.java | 25 +++---------------- .../infrastructure/config/SecurityConfig.java | 2 +- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java index e8018b4..0360def 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java @@ -60,37 +60,18 @@ public ResponseEntity> getProduct( } @GetMapping - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> getAllProducts( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "id") String sortBy, - @RequestParam(defaultValue = "DESC") String sortDirection) { - - Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); - PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); - - Page products = productApplicationService.findProducts( - ProductQuery.all(), - pageRequest - ); - - PagedProductResponse response = responseMapper.toPagedResponse(products); - return ResponseEntity.ok(ApiResponse.success(response)); - } - - @GetMapping("/active") - public ResponseEntity> getActiveProducts( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "id") String sortBy, - @RequestParam(defaultValue = "DESC") String sortDirection) { + @RequestParam(defaultValue = "DESC") String sortDirection, + @RequestParam(defaultValue = "true") boolean active) { Sort.Direction direction = Sort.Direction.fromString(sortDirection.toUpperCase()); PageRequest pageRequest = PageRequest.of(page, size, Sort.by(direction, sortBy)); Page products = productApplicationService.findProducts( - ProductQuery.active(), + active ? ProductQuery.active() : ProductQuery.all(), pageRequest ); diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java index 6704483..3ef9e69 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/config/SecurityConfig.java @@ -51,7 +51,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, "/api/v2/auth/**", // This should cover your NextAuth endpoints "/api/v2/auth/nextauth", // Add explicitly "/api/v2/auth/session", - "/api/v2/products/active", + "/api/v2/products", "/api/v2/products/{productId}", "/api/v2/payments/webhooks/{provider}/callback").permitAll() .anyRequest().authenticated() From 194c09451988a9a946f3b7079b506f1c6a51dd8b Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Fri, 28 Mar 2025 21:01:16 +0100 Subject: [PATCH 54/57] feat: Implement metrics queries for revenue, orders, products, and users --- pom.xml | 4 + .../MetricsApplicationService.java | 91 +++++++++++++++++++ .../metrics/application/dto/MetricsQuery.java | 18 ++++ .../query/CalculateTotalRevenueQuery.java | 21 +++++ .../query/CountOrdersInPeriodQuery.java | 21 +++++ .../service/OrderApplicationService.java | 12 +++ .../domain/repository/OrderRepository.java | 10 +- .../domain/service/OrderDomainService.java | 21 +++++ .../persistence/JpaOrderRepository.java | 12 +++ .../SpringDataJpaOrderRepository.java | 27 ++++++ .../query/CountNewProductsInPeriodQuery.java | 19 ++++ .../service/ProductApplicationService.java | 5 + .../domain/repository/ProductRepository.java | 3 + .../domain/service/ProductDomainService.java | 11 ++- .../persistence/JpaProductRepository.java | 6 ++ .../SpringDataJpaProductRepository.java | 14 +++ .../query/CountActiveUsersInPeriodQuery.java | 19 ++++ .../service/UserApplicationService.java | 5 + .../domain/repository/UserRepository.java | 3 + .../domain/service/UserDomainService.java | 10 ++ .../persistence/JpaUserRepository.java | 6 ++ .../SpringDataJpaUserRepository.java | 12 +++ 22 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/metrics/application/MetricsApplicationService.java create mode 100644 src/main/java/com/zenfulcode/commercify/metrics/application/dto/MetricsQuery.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/query/CalculateTotalRevenueQuery.java create mode 100644 src/main/java/com/zenfulcode/commercify/order/application/query/CountOrdersInPeriodQuery.java create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/query/CountNewProductsInPeriodQuery.java create mode 100644 src/main/java/com/zenfulcode/commercify/user/application/query/CountActiveUsersInPeriodQuery.java diff --git a/pom.xml b/pom.xml index cd07427..9eec35d 100644 --- a/pom.xml +++ b/pom.xml @@ -154,6 +154,10 @@ org.thymeleaf.extras thymeleaf-extras-springsecurity6 + + org.springframework.boot + spring-boot-starter-validation + diff --git a/src/main/java/com/zenfulcode/commercify/metrics/application/MetricsApplicationService.java b/src/main/java/com/zenfulcode/commercify/metrics/application/MetricsApplicationService.java new file mode 100644 index 0000000..fce69ba --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/metrics/application/MetricsApplicationService.java @@ -0,0 +1,91 @@ +package com.zenfulcode.commercify.metrics.application; + +import com.zenfulcode.commercify.api.system.dto.MetricsResponse; +import com.zenfulcode.commercify.metrics.application.dto.MetricsQuery; +import com.zenfulcode.commercify.order.application.query.CalculateTotalRevenueQuery; +import com.zenfulcode.commercify.order.application.query.CountOrdersInPeriodQuery; +import com.zenfulcode.commercify.order.application.service.OrderApplicationService; +import com.zenfulcode.commercify.product.application.query.CountNewProductsInPeriodQuery; +import com.zenfulcode.commercify.product.application.service.ProductApplicationService; +import com.zenfulcode.commercify.user.application.query.CountActiveUsersInPeriodQuery; +import com.zenfulcode.commercify.user.application.service.UserApplicationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; + +@Service +@Slf4j +@RequiredArgsConstructor +public class MetricsApplicationService { + private final OrderApplicationService orderApplicationService; + private final ProductApplicationService productApplicationService; + private final UserApplicationService userApplicationService; + + @Cacheable(value = "metricsCache", key = "{#metricsQuery.startDate, #metricsQuery.endDate, #metricsQuery.productCategory, #metricsQuery.region}") + public MetricsResponse getMetrics(MetricsQuery metricsQuery) { + LocalDate startDate = metricsQuery.startDate(); + LocalDate endDate = metricsQuery.endDate(); + + log.info("Calculating metrics from {} to {}", startDate, endDate); + + final CalculateTotalRevenueQuery calculateTotalRevenueQuery = CalculateTotalRevenueQuery.of(metricsQuery); + final CountOrdersInPeriodQuery countOrdersInPeriodQuery = CountOrdersInPeriodQuery.of(metricsQuery); + final CountNewProductsInPeriodQuery countNewProductsInPeriodQuery = CountNewProductsInPeriodQuery.of(metricsQuery); + final CountActiveUsersInPeriodQuery countActiveUsersInPeriodQuery = CountActiveUsersInPeriodQuery.of(metricsQuery); + + // Get metrics in parallel for better performance + BigDecimal totalRevenue = orderApplicationService.calculateTotalRevenue(calculateTotalRevenueQuery); + int totalOrders = orderApplicationService.countOrdersInPeriod(countOrdersInPeriodQuery); + int newProductsAdded = productApplicationService.countNewProductsInPeriod(countNewProductsInPeriodQuery); + int activeUsers = userApplicationService.countActiveUsersInPeriod(countActiveUsersInPeriodQuery); + + // Optional: calculate trends compared to previous period + BigDecimal revenueChangePercent = calculateRevenueChangePercent(startDate, endDate, metricsQuery); + + return MetricsResponse.builder() + .startDate(startDate) + .endDate(endDate) + .totalRevenue(totalRevenue) + .totalOrders(totalOrders) + .newProductsAdded(newProductsAdded) + .activeUsers(activeUsers) + .revenueChangePercent(revenueChangePercent) + .build(); + } + + private BigDecimal calculateRevenueChangePercent(LocalDate startDate, LocalDate endDate, MetricsQuery metricsQuery) { + // Calculate the same period length before the startDate + long periodDays = java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate); + LocalDate previousPeriodStart = startDate.minusDays(periodDays); + LocalDate previousPeriodEnd = startDate.minusDays(1); + + final CalculateTotalRevenueQuery currentRevenueQuery = new CalculateTotalRevenueQuery( + metricsQuery.productCategory(), + metricsQuery.region(), + startDate, + endDate + ); + + final CalculateTotalRevenueQuery previousRevenueQuery = new CalculateTotalRevenueQuery( + metricsQuery.productCategory(), + metricsQuery.region(), + previousPeriodStart, + previousPeriodEnd + ); + + BigDecimal currentRevenue = orderApplicationService.calculateTotalRevenue(currentRevenueQuery); + BigDecimal previousRevenue = orderApplicationService.calculateTotalRevenue(previousRevenueQuery); + + if (previousRevenue.equals(BigDecimal.ZERO)) { + return currentRevenue.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : new BigDecimal(100); + } + + return currentRevenue.subtract(previousRevenue) + .multiply(new BigDecimal(100)).divide(previousRevenue, 2, RoundingMode.HALF_UP); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/metrics/application/dto/MetricsQuery.java b/src/main/java/com/zenfulcode/commercify/metrics/application/dto/MetricsQuery.java new file mode 100644 index 0000000..51ea828 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/metrics/application/dto/MetricsQuery.java @@ -0,0 +1,18 @@ +package com.zenfulcode.commercify.metrics.application.dto; + +import com.zenfulcode.commercify.api.system.dto.MetricsRequest; + +import java.time.LocalDate; + +public record MetricsQuery(LocalDate startDate, LocalDate endDate, String productCategory, + String region) { + + public static MetricsQuery of(MetricsRequest metricsRequest) { + return new MetricsQuery( + metricsRequest.getEffectiveStartDate(), + metricsRequest.getEffectiveEndDate(), + metricsRequest.getProductCategory(), + metricsRequest.getRegion() + ); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/query/CalculateTotalRevenueQuery.java b/src/main/java/com/zenfulcode/commercify/order/application/query/CalculateTotalRevenueQuery.java new file mode 100644 index 0000000..c12cd53 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/query/CalculateTotalRevenueQuery.java @@ -0,0 +1,21 @@ +package com.zenfulcode.commercify.order.application.query; + +import com.zenfulcode.commercify.metrics.application.dto.MetricsQuery; + +import java.time.LocalDate; + +public record CalculateTotalRevenueQuery( + String productCategory, + String region, + LocalDate startDate, + LocalDate endDate +) { + public static CalculateTotalRevenueQuery of(MetricsQuery metricsQuery) { + return new CalculateTotalRevenueQuery( + metricsQuery.productCategory(), + metricsQuery.region(), + metricsQuery.startDate(), + metricsQuery.endDate() + ); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/query/CountOrdersInPeriodQuery.java b/src/main/java/com/zenfulcode/commercify/order/application/query/CountOrdersInPeriodQuery.java new file mode 100644 index 0000000..1fa3184 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/order/application/query/CountOrdersInPeriodQuery.java @@ -0,0 +1,21 @@ +package com.zenfulcode.commercify.order.application.query; + +import com.zenfulcode.commercify.metrics.application.dto.MetricsQuery; + +import java.time.LocalDate; + +public record CountOrdersInPeriodQuery( + String productCategory, + String region, + LocalDate startDate, + LocalDate endDate +) { + public static CountOrdersInPeriodQuery of(MetricsQuery metricsQuery) { + return new CountOrdersInPeriodQuery( + metricsQuery.productCategory(), + metricsQuery.region(), + metricsQuery.startDate(), + metricsQuery.endDate() + ); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java index 07e3780..15852fb 100644 --- a/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/order/application/service/OrderApplicationService.java @@ -5,6 +5,8 @@ import com.zenfulcode.commercify.order.application.command.GetOrderByIdCommand; import com.zenfulcode.commercify.order.application.command.UpdateOrderStatusCommand; import com.zenfulcode.commercify.order.application.dto.OrderDetailsDTO; +import com.zenfulcode.commercify.order.application.query.CalculateTotalRevenueQuery; +import com.zenfulcode.commercify.order.application.query.CountOrdersInPeriodQuery; import com.zenfulcode.commercify.order.application.query.FindAllOrdersQuery; import com.zenfulcode.commercify.order.application.query.FindOrdersByUserIdQuery; import com.zenfulcode.commercify.order.domain.model.Order; @@ -25,6 +27,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -116,4 +119,13 @@ public Order getOrderById(OrderId orderId) { public boolean isOrderOwnedByUser(OrderId orderId, UserId userId) { return orderDomainService.isOrderOwnedByUser(orderId, userId); } + + @Transactional(readOnly = true) + public BigDecimal calculateTotalRevenue(CalculateTotalRevenueQuery query) { + return orderDomainService.calculateTotalRevenue(query.startDate(), query.endDate()); + } + + public int countOrdersInPeriod(CountOrdersInPeriodQuery query) { + return orderDomainService.countOrdersInPeriod(query.startDate(), query.endDate()); + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java index 9e593b1..4ed763e 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/repository/OrderRepository.java @@ -6,13 +6,15 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import java.math.BigDecimal; +import java.time.Instant; import java.util.Optional; public interface OrderRepository { Order save(Order order); Optional findById(OrderId id); - + Page findByUserId(UserId userId, PageRequest pageRequest); Page findAll(PageRequest pageRequest); @@ -20,4 +22,8 @@ public interface OrderRepository { boolean existsByIdAndUserId(OrderId id, UserId userId); boolean existsByUserId(UserId userId); -} + + Optional calculateTotalRevenue(Instant startDate, Instant endDate); + + int countOrders(Instant startDate, Instant endDate); +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index 56a1698..4919269 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -22,6 +22,10 @@ import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -134,4 +138,21 @@ public Page findAllOrders(FindAllOrdersQuery query) { public boolean isOrderOwnedByUser(OrderId orderId, UserId userId) { return orderRepository.existsByIdAndUserId(orderId, userId); } + + public BigDecimal calculateTotalRevenue(LocalDate startDate, LocalDate endDate) { + Instant start = startDate.atStartOfDay().toInstant(ZoneOffset.from(ZoneOffset.UTC)); + Instant end = endDate.atStartOfDay().toInstant(ZoneOffset.UTC); + + // Implement logic to calculate total revenue based on the provided parameters + // This is a placeholder implementation and should be replaced with actual logic + return orderRepository.calculateTotalRevenue(start, end) + .orElse(BigDecimal.ZERO); + } + + public int countOrdersInPeriod(LocalDate startDate, LocalDate endDate) { + Instant start = startDate.atStartOfDay().toInstant(ZoneOffset.from(ZoneOffset.UTC)); + Instant end = endDate.atStartOfDay().toInstant(ZoneOffset.UTC); + + return orderRepository.countOrders(start, end); + } } diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java index f15dedd..aad0244 100644 --- a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/JpaOrderRepository.java @@ -9,6 +9,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Repository; +import java.math.BigDecimal; +import java.time.Instant; import java.util.Optional; @Repository @@ -45,4 +47,14 @@ public boolean existsByIdAndUserId(OrderId id, UserId userId) { public boolean existsByUserId(UserId userId) { return repository.findByUserId(userId, PageRequest.of(0, 1)).hasContent(); } + + @Override + public Optional calculateTotalRevenue(Instant startDate, Instant endDate) { + return repository.calculateTotalRevenue(startDate, endDate); + } + + @Override + public int countOrders(Instant startDate, Instant endDate) { + return repository.countOrders(startDate, endDate); + } } diff --git a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderRepository.java b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderRepository.java index 93da3ea..c8bfd10 100644 --- a/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderRepository.java +++ b/src/main/java/com/zenfulcode/commercify/order/infrastructure/persistence/SpringDataJpaOrderRepository.java @@ -6,11 +6,38 @@ 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; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Optional; + @Repository interface SpringDataJpaOrderRepository extends JpaRepository { Page findByUserId(UserId userId, Pageable pageable); boolean existsByIdAndUserId(OrderId id, UserId userId); + + @Query(""" + SELECT SUM(o.subtotal.amount) + FROM Order o + WHERE o.status = 'COMPLETED' + AND o.createdAt BETWEEN :startDate AND :endDate + """) + Optional calculateTotalRevenue( + @Param("startDate") Instant startDate, + @Param("endDate") Instant endDate + ); + + @Query(""" + SELECT COUNT(o) + FROM Order o + WHERE o.createdAt BETWEEN :startDate AND :endDate + """) + int countOrders( + @Param("startDate") Instant startDate, + @Param("endDate") Instant endDate + ); } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/application/query/CountNewProductsInPeriodQuery.java b/src/main/java/com/zenfulcode/commercify/product/application/query/CountNewProductsInPeriodQuery.java new file mode 100644 index 0000000..12cffe6 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/query/CountNewProductsInPeriodQuery.java @@ -0,0 +1,19 @@ +package com.zenfulcode.commercify.product.application.query; + +import com.zenfulcode.commercify.metrics.application.dto.MetricsQuery; + +import java.time.LocalDate; + +public record CountNewProductsInPeriodQuery( + String productCategory, + LocalDate startDate, + LocalDate endDate +) { + public static CountNewProductsInPeriodQuery of(MetricsQuery metricsQuery) { + return new CountNewProductsInPeriodQuery( + metricsQuery.productCategory(), + metricsQuery.startDate(), + metricsQuery.endDate() + ); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java index 93b2512..ec089e2 100644 --- a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java @@ -1,6 +1,7 @@ package com.zenfulcode.commercify.product.application.service; import com.zenfulcode.commercify.product.application.command.*; +import com.zenfulcode.commercify.product.application.query.CountNewProductsInPeriodQuery; import com.zenfulcode.commercify.product.application.query.ProductQuery; import com.zenfulcode.commercify.product.domain.exception.ProductDeletionException; import com.zenfulcode.commercify.product.domain.exception.ProductNotFoundException; @@ -178,4 +179,8 @@ public Product getProductById(ProductId productId) { public List findVariantsByIds(List variantIds) { return productRepository.findVariantsByIds(variantIds); } + + public int countNewProductsInPeriod(CountNewProductsInPeriodQuery query) { + return productDomainService.countNewProductsInPeriod(query.startDate(), query.endDate()); + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java index 9d5b3a7..6208dd6 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/repository/ProductRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -30,4 +31,6 @@ public interface ProductRepository { List findAllById(Collection ids); List findVariantsByIds(Collection variantIds); + + int findNewProducts(Instant startDate, Instant endDate); } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java index 1e97402..0450a4b 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java @@ -13,6 +13,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -212,7 +215,11 @@ public List getAllProductsById(Collection productIds) { return products; } -} - + public int countNewProductsInPeriod(LocalDate startDate, LocalDate endDate) { + Instant start = startDate.atStartOfDay().toInstant(ZoneOffset.from(ZoneOffset.UTC)); + Instant end = endDate.atStartOfDay().toInstant(ZoneOffset.UTC); + return productRepository.findNewProducts(start, end); + } +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java index 06e2417..6a70c86 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/JpaProductRepository.java @@ -11,6 +11,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -69,4 +70,9 @@ public List findAllById(Collection ids) { public List findVariantsByIds(Collection variantIds) { return variantRepository.findAllById(variantIds); } + + @Override + public int findNewProducts(Instant startDate, Instant endDate) { + return repository.countNewProducts(startDate, endDate); + } } diff --git a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java index c570414..956c7d0 100644 --- a/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java +++ b/src/main/java/com/zenfulcode/commercify/product/infrastructure/persistence/SpringDataJpaProductRepository.java @@ -6,8 +6,12 @@ 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; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.Instant; + @Repository interface SpringDataJpaProductRepository extends JpaRepository { Page findByActiveTrue(Pageable pageable); @@ -15,4 +19,14 @@ interface SpringDataJpaProductRepository extends JpaRepository findByCategoryId(CategoryId categoryId, Pageable pageable); Page findByStockLessThan(int threshold, Pageable pageable); + + @Query(""" + SELECT COUNT(p) + FROM Product p + WHERE p.createdAt BETWEEN :startDate AND :endDate + """) + int countNewProducts( + @Param("startDate") Instant startDate, + @Param("endDate") Instant endDate + ); } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/application/query/CountActiveUsersInPeriodQuery.java b/src/main/java/com/zenfulcode/commercify/user/application/query/CountActiveUsersInPeriodQuery.java new file mode 100644 index 0000000..7999c08 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/user/application/query/CountActiveUsersInPeriodQuery.java @@ -0,0 +1,19 @@ +package com.zenfulcode.commercify.user.application.query; + +import com.zenfulcode.commercify.metrics.application.dto.MetricsQuery; + +import java.time.LocalDate; + +public record CountActiveUsersInPeriodQuery( + String region, + LocalDate startDate, + LocalDate endDate +) { + public static CountActiveUsersInPeriodQuery of(MetricsQuery metricsQuery) { + return new CountActiveUsersInPeriodQuery( + metricsQuery.region(), + metricsQuery.startDate(), + metricsQuery.endDate() + ); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java index c90249a..d10e9cb 100644 --- a/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/user/application/service/UserApplicationService.java @@ -4,6 +4,7 @@ import com.zenfulcode.commercify.user.application.command.CreateUserCommand; import com.zenfulcode.commercify.user.application.command.UpdateUserCommand; import com.zenfulcode.commercify.user.application.command.UpdateUserStatusCommand; +import com.zenfulcode.commercify.user.application.query.CountActiveUsersInPeriodQuery; import com.zenfulcode.commercify.user.domain.model.User; import com.zenfulcode.commercify.user.domain.model.UserRole; import com.zenfulcode.commercify.user.domain.model.UserStatus; @@ -164,4 +165,8 @@ public void resetPassword(UserId userId, String newPassword) { eventPublisher.publish(user.getDomainEvents()); } + + public int countActiveUsersInPeriod(CountActiveUsersInPeriodQuery query) { + return userDomainService.findNewUsers(query.startDate(), query.endDate()); + } } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/repository/UserRepository.java b/src/main/java/com/zenfulcode/commercify/user/domain/repository/UserRepository.java index 93275b9..b5ffda1 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/repository/UserRepository.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/repository/UserRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.time.Instant; import java.util.Optional; public interface UserRepository { @@ -22,4 +23,6 @@ public interface UserRepository { Page findByStatus(UserStatus status, Pageable pageable); void delete(User user); + + int findNewUsers(Instant startDate, Instant endDate); } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java index b075309..fd3316d 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java @@ -14,6 +14,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; @@ -161,4 +164,11 @@ public Page getUsersByStatus(UserStatus userStatus, Pageable pageable) { public boolean emailExists(String email) { return userRepository.existsByEmail(email); } + + public int findNewUsers(LocalDate startDate, LocalDate endDate) { + Instant start = startDate.atStartOfDay().toInstant(ZoneOffset.from(ZoneOffset.UTC)); + Instant end = endDate.atStartOfDay().toInstant(ZoneOffset.UTC); + + return userRepository.findNewUsers(start, end); + } } \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/JpaUserRepository.java b/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/JpaUserRepository.java index 044841d..854f758 100644 --- a/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/JpaUserRepository.java +++ b/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/JpaUserRepository.java @@ -9,6 +9,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import java.time.Instant; import java.util.Optional; @Repository @@ -50,4 +51,9 @@ public Page findByStatus(UserStatus status, Pageable pageable) { public void delete(User user) { repository.delete(user); } + + @Override + public int findNewUsers(Instant startDate, Instant endDate) { + return repository.findNewUsers(startDate, endDate); + } } diff --git a/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/SpringDataJpaUserRepository.java b/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/SpringDataJpaUserRepository.java index f05764a..629be35 100644 --- a/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/SpringDataJpaUserRepository.java +++ b/src/main/java/com/zenfulcode/commercify/user/infrastructure/persistence/SpringDataJpaUserRepository.java @@ -6,8 +6,11 @@ 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; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.Instant; import java.util.Optional; @Repository @@ -17,4 +20,13 @@ interface SpringDataJpaUserRepository extends JpaRepository { boolean existsByEmailIgnoreCase(String email); Page findByStatus(UserStatus status, Pageable pageable); + + @Query(""" + SELECT COUNT(u) + FROM User u + WHERE u.createdAt BETWEEN :startDate AND :endDate + """) + int findNewUsers( + @Param("startDate") Instant startDate, + @Param("endDate") Instant endDate); } \ No newline at end of file From fb21ad35f897daed0e76bc242ae47bef85b788cc Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Fri, 28 Mar 2025 21:03:16 +0100 Subject: [PATCH 55/57] feat: Add metrics API with request and response DTOs for revenue, orders, and user statistics --- .../api/system/MetricsController.java | 61 +++++++++++++++++++ .../api/system/dto/MetricsRequest.java | 42 +++++++++++++ .../api/system/dto/MetricsResponse.java | 31 ++++++++++ 3 files changed, 134 insertions(+) create mode 100644 src/main/java/com/zenfulcode/commercify/api/system/MetricsController.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/system/dto/MetricsRequest.java create mode 100644 src/main/java/com/zenfulcode/commercify/api/system/dto/MetricsResponse.java diff --git a/src/main/java/com/zenfulcode/commercify/api/system/MetricsController.java b/src/main/java/com/zenfulcode/commercify/api/system/MetricsController.java new file mode 100644 index 0000000..cf79d91 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/system/MetricsController.java @@ -0,0 +1,61 @@ +package com.zenfulcode.commercify.api.system; + +import com.zenfulcode.commercify.api.system.dto.MetricsRequest; +import com.zenfulcode.commercify.api.system.dto.MetricsResponse; +import com.zenfulcode.commercify.metrics.application.MetricsApplicationService; +import com.zenfulcode.commercify.metrics.application.dto.MetricsQuery; +import com.zenfulcode.commercify.shared.interfaces.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@Slf4j +@RestController +@RequestMapping("/api/v2/metrics") +@RequiredArgsConstructor +public class MetricsController { + + private final MetricsApplicationService metricsService; + + @GetMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getMetrics( + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + @RequestParam(required = false) Integer lastDays) { + + log.info("Getting metrics"); + + MetricsRequest request = new MetricsRequest(startDate, endDate, lastDays); + + // Handle either date range or "last X days" + if (lastDays != null) { + request.setLastDays(lastDays); + } else { + request.setStartDate(startDate); + request.setEndDate(endDate); + } + + final MetricsQuery metricsQuery = MetricsQuery.of(request); + + MetricsResponse metrics = metricsService.getMetrics(metricsQuery); + return ResponseEntity.ok(ApiResponse.success(metrics)); + } + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getMetricsWithFilters( + @Validated @RequestBody MetricsRequest request) { + + final MetricsQuery metricsQuery = MetricsQuery.of(request); + + MetricsResponse metrics = metricsService.getMetrics(metricsQuery); + return ResponseEntity.ok(ApiResponse.success(metrics)); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/system/dto/MetricsRequest.java b/src/main/java/com/zenfulcode/commercify/api/system/dto/MetricsRequest.java new file mode 100644 index 0000000..224db29 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/system/dto/MetricsRequest.java @@ -0,0 +1,42 @@ +package com.zenfulcode.commercify.api.system.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.Min; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +@Data +public class MetricsRequest { + private LocalDate startDate; + private LocalDate endDate; + + @Min(1) + private Integer lastDays; + + public MetricsRequest(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + Integer lastDays) { + this.startDate = startDate; + this.endDate = endDate; + this.lastDays = lastDays; + } + + // Optional filters + private String productCategory; + private String region; + + @JsonIgnore + public LocalDate getEffectiveStartDate() { + if (lastDays != null) { + return LocalDate.now().minusDays(lastDays); + } + return startDate != null ? startDate : LocalDate.now().minusDays(30); // Default 30 days + } + + @JsonIgnore + public LocalDate getEffectiveEndDate() { + return endDate != null ? endDate : LocalDate.now(); + } +} diff --git a/src/main/java/com/zenfulcode/commercify/api/system/dto/MetricsResponse.java b/src/main/java/com/zenfulcode/commercify/api/system/dto/MetricsResponse.java new file mode 100644 index 0000000..9af50d1 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/api/system/dto/MetricsResponse.java @@ -0,0 +1,31 @@ +package com.zenfulcode.commercify.api.system.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MetricsResponse { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate endDate; + + private BigDecimal totalRevenue; + private int totalOrders; + private int newProductsAdded; + private int activeUsers; + + // Optional: include trend data + private BigDecimal revenueChangePercent; + private int orderChangePercent; +} From abc1d6212964a786eb84284df7bde26995729413 Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Fri, 18 Apr 2025 20:27:28 +0200 Subject: [PATCH 56/57] fix: Adjust end date time to include the full day in product, order, and user counts --- .../auth/infrastructure/security/JwtAuthenticationFilter.java | 2 -- .../commercify/order/domain/service/OrderDomainService.java | 4 ++-- .../product/domain/service/ProductDomainService.java | 2 +- .../commercify/user/domain/service/UserDomainService.java | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java index 3460cbd..12b5cbc 100644 --- a/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/zenfulcode/commercify/auth/infrastructure/security/JwtAuthenticationFilter.java @@ -30,8 +30,6 @@ protected void doFilterInternal( try { String token = extractJwtToken(request); - System.out.println("Token: " + token); - if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) { AuthenticatedUser user = authService.validateAccessToken(token); diff --git a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java index 4919269..f7c6e72 100644 --- a/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/order/domain/service/OrderDomainService.java @@ -141,7 +141,7 @@ public boolean isOrderOwnedByUser(OrderId orderId, UserId userId) { public BigDecimal calculateTotalRevenue(LocalDate startDate, LocalDate endDate) { Instant start = startDate.atStartOfDay().toInstant(ZoneOffset.from(ZoneOffset.UTC)); - Instant end = endDate.atStartOfDay().toInstant(ZoneOffset.UTC); + Instant end = endDate.atTime(23, 59).toInstant(ZoneOffset.UTC); // Implement logic to calculate total revenue based on the provided parameters // This is a placeholder implementation and should be replaced with actual logic @@ -151,7 +151,7 @@ public BigDecimal calculateTotalRevenue(LocalDate startDate, LocalDate endDate) public int countOrdersInPeriod(LocalDate startDate, LocalDate endDate) { Instant start = startDate.atStartOfDay().toInstant(ZoneOffset.from(ZoneOffset.UTC)); - Instant end = endDate.atStartOfDay().toInstant(ZoneOffset.UTC); + Instant end = endDate.atTime(23, 59).toInstant(ZoneOffset.UTC); return orderRepository.countOrders(start, end); } diff --git a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java index 0450a4b..653281c 100644 --- a/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/product/domain/service/ProductDomainService.java @@ -218,7 +218,7 @@ public List getAllProductsById(Collection productIds) { public int countNewProductsInPeriod(LocalDate startDate, LocalDate endDate) { Instant start = startDate.atStartOfDay().toInstant(ZoneOffset.from(ZoneOffset.UTC)); - Instant end = endDate.atStartOfDay().toInstant(ZoneOffset.UTC); + Instant end = endDate.atTime(23, 59).toInstant(ZoneOffset.UTC); return productRepository.findNewProducts(start, end); } diff --git a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java index fd3316d..0599bfb 100644 --- a/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/user/domain/service/UserDomainService.java @@ -167,7 +167,7 @@ public boolean emailExists(String email) { public int findNewUsers(LocalDate startDate, LocalDate endDate) { Instant start = startDate.atStartOfDay().toInstant(ZoneOffset.from(ZoneOffset.UTC)); - Instant end = endDate.atStartOfDay().toInstant(ZoneOffset.UTC); + Instant end = endDate.atTime(23, 59).toInstant(ZoneOffset.UTC); return userRepository.findNewUsers(start, end); } From 12bb4d42420a5a2b1b07d44cc376368003af242a Mon Sep 17 00:00:00 2001 From: gkhaavik Date: Sat, 19 Apr 2025 14:06:00 +0200 Subject: [PATCH 57/57] feat: Add product activation functionality with corresponding command and controller endpoint --- .../api/payment/PaymentAdminController.java | 13 ++++++------- .../api/payment/mapper/PaymentDtoMapper.java | 5 +++-- .../api/product/ProductController.java | 17 ++++++++++++++++- .../command/CapturePaymentCommand.java | 3 ++- .../service/PaymentApplicationService.java | 2 +- .../domain/service/PaymentDomainService.java | 6 ++++++ .../command/ActivateProductCommand.java | 8 ++++++++ .../service/ProductApplicationService.java | 13 +++++++++++++ 8 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/zenfulcode/commercify/product/application/command/ActivateProductCommand.java diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java index 422a6d9..9d8606b 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/PaymentAdminController.java @@ -2,6 +2,7 @@ import com.zenfulcode.commercify.api.payment.dto.response.CapturedPaymentResponse; import com.zenfulcode.commercify.api.payment.mapper.PaymentDtoMapper; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; import com.zenfulcode.commercify.payment.application.dto.CapturedPayment; import com.zenfulcode.commercify.payment.application.service.PaymentApplicationService; @@ -10,10 +11,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v2/payments/admin") @@ -23,11 +21,11 @@ public class PaymentAdminController { private final PaymentApplicationService paymentService; private final PaymentDtoMapper paymentDtoMapper; - @PostMapping("/{paymentId}/capture") + @PostMapping("/{orderId}/capture") public ResponseEntity> capturePayment( - @PathVariable String paymentId) { + @PathVariable String orderId) { - CapturePaymentCommand command = paymentDtoMapper.toCaptureCommand(PaymentId.of(paymentId)); + CapturePaymentCommand command = paymentDtoMapper.toCaptureCommand(OrderId.of(orderId)); CapturedPayment capturedPayment = paymentService.capturePayment(command); CapturedPaymentResponse response = paymentDtoMapper.toCapturedResponse(capturedPayment); @@ -41,4 +39,5 @@ public ResponseEntity> refundPayment( // paymentService.refundPayment(OrderId.of(orderId)); return ResponseEntity.ok(ApiResponse.success("Refund initiated")); } + } diff --git a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java index 8b4c16c..b8f537d 100644 --- a/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java +++ b/src/main/java/com/zenfulcode/commercify/api/payment/mapper/PaymentDtoMapper.java @@ -8,6 +8,7 @@ import com.zenfulcode.commercify.api.payment.dto.response.PaymentResponse; import com.zenfulcode.commercify.order.application.service.OrderApplicationService; import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.payment.application.command.CapturePaymentCommand; import com.zenfulcode.commercify.payment.application.command.InitiatePaymentCommand; import com.zenfulcode.commercify.payment.application.dto.CapturedPayment; @@ -59,9 +60,9 @@ public WebhookPayload toWebhookPayload(WebhookRequest request) { } } - public CapturePaymentCommand toCaptureCommand(PaymentId paymentId) { + public CapturePaymentCommand toCaptureCommand(OrderId orderId) { return new CapturePaymentCommand( - paymentId, + orderId, null ); } diff --git a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java index 0360def..8bd44bd 100644 --- a/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java +++ b/src/main/java/com/zenfulcode/commercify/api/product/ProductController.java @@ -14,6 +14,7 @@ import com.zenfulcode.commercify.product.domain.valueobject.ProductId; import com.zenfulcode.commercify.shared.interfaces.ApiResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -22,6 +23,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +@Slf4j @RestController @RequestMapping("/api/v2/products") @RequiredArgsConstructor @@ -135,11 +137,24 @@ public ResponseEntity> updateVariantPrices( @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> deactivateProduct(@PathVariable String productId) { DeactivateProductCommand command = new DeactivateProductCommand(ProductId.of(productId)); + log.info("Deactivating product with ID: {}", productId); productApplicationService.deactivateProduct(command); - + log.info("Product deactivated successfully"); return ResponseEntity.ok(ApiResponse.success("Product deactivated successfully")); } + @PostMapping("/{productId}/activate") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> activateProduct(@PathVariable String productId) { + ActivateProductCommand command = new ActivateProductCommand(ProductId.of(productId)); + log.info("Activating product with ID: {}", productId); + productApplicationService.activateProduct(command); + + log.info("Product activated successfully"); + + return ResponseEntity.ok(ApiResponse.success("Product activated successfully")); + } + @DeleteMapping("/{productId}") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> deleteProduct(@PathVariable String productId) { diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java b/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java index f3c86bb..d063fd5 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/command/CapturePaymentCommand.java @@ -1,10 +1,11 @@ package com.zenfulcode.commercify.payment.application.command; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.payment.domain.valueobject.PaymentId; import com.zenfulcode.commercify.shared.domain.model.Money; public record CapturePaymentCommand( - PaymentId paymentId, + OrderId orderId, Money captureAmount ) { } diff --git a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java index e3bc522..97fb755 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/application/service/PaymentApplicationService.java @@ -79,7 +79,7 @@ public void handlePaymentCallback(PaymentProvider provider, WebhookPayload paylo // TODO: Make sure the capture currency is the same as the payment currency @Transactional public CapturedPayment capturePayment(CapturePaymentCommand command) { - Payment payment = paymentDomainService.getPaymentById(command.paymentId()); + Payment payment = paymentDomainService.getPaymentByOrderId(command.orderId()); Money captureAmount = command.captureAmount() == null ? payment.getAmount() : command.captureAmount(); diff --git a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java index 6647ffd..fb3dcd7 100644 --- a/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java +++ b/src/main/java/com/zenfulcode/commercify/payment/domain/service/PaymentDomainService.java @@ -1,6 +1,7 @@ package com.zenfulcode.commercify.payment.domain.service; import com.zenfulcode.commercify.order.domain.model.Order; +import com.zenfulcode.commercify.order.domain.valueobject.OrderId; import com.zenfulcode.commercify.payment.domain.event.PaymentCreatedEvent; import com.zenfulcode.commercify.payment.domain.exception.PaymentNotFoundException; import com.zenfulcode.commercify.payment.domain.model.FailureReason; @@ -126,4 +127,9 @@ public Payment getPaymentByProviderReference(String providerReference) { return paymentRepository.findByProviderReference(providerReference) .orElseThrow(() -> new PaymentNotFoundException(providerReference)); } + + public Payment getPaymentByOrderId(OrderId orderId) { + return paymentRepository.findByOrderId(orderId) + .orElseThrow(() -> new PaymentNotFoundException(orderId)); + } } diff --git a/src/main/java/com/zenfulcode/commercify/product/application/command/ActivateProductCommand.java b/src/main/java/com/zenfulcode/commercify/product/application/command/ActivateProductCommand.java new file mode 100644 index 0000000..e330b08 --- /dev/null +++ b/src/main/java/com/zenfulcode/commercify/product/application/command/ActivateProductCommand.java @@ -0,0 +1,8 @@ +package com.zenfulcode.commercify.product.application.command; + +import com.zenfulcode.commercify.product.domain.valueobject.ProductId; + +public record ActivateProductCommand( + ProductId productId +) { +} \ No newline at end of file diff --git a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java index ec089e2..f033997 100644 --- a/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java +++ b/src/main/java/com/zenfulcode/commercify/product/application/service/ProductApplicationService.java @@ -131,6 +131,19 @@ public void deactivateProduct(DeactivateProductCommand command) { eventPublisher.publish(product.getDomainEvents()); } + /** + * Activates a product + */ + @Transactional + public void activateProduct(ActivateProductCommand command) { + Product product = productRepository.findById(command.productId()) + .orElseThrow(() -> new ProductNotFoundException(command.productId())); + + product.activate(); + productRepository.save(product); + eventPublisher.publish(product.getDomainEvents()); + } + /** * Deletes a product */