diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/controller/RestaurantSubscriptionController.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/controller/RestaurantSubscriptionController.java new file mode 100644 index 0000000..d07b4f8 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/controller/RestaurantSubscriptionController.java @@ -0,0 +1,24 @@ +package com.restroly.qrmenu.subscription.controller; + +import com.restroly.qrmenu.subscription.dto.RestaurantSubscriptionDto; +import com.restroly.qrmenu.subscription.service.SubscriptionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import static com.restroly.qrmenu.common.util.ApiConstants.SECURE_API_VERSION; + +@RestController +@RequestMapping(SECURE_API_VERSION + "/restaurant/{restId}/subscription") +public class RestaurantSubscriptionController { + + @Autowired + private SubscriptionService subscriptionService; + + @GetMapping + @PreAuthorize("hasAnyAuthority('RESTAURANT_OWNER', 'SUPER_ADMIN')") + public ResponseEntity getRestaurantSubscription(@PathVariable Long restId) { + return ResponseEntity.ok(subscriptionService.getRestaurantSubscription(restId)); + } +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/controller/SuperAdminSubscriptionController.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/controller/SuperAdminSubscriptionController.java new file mode 100644 index 0000000..6eb75da --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/controller/SuperAdminSubscriptionController.java @@ -0,0 +1,89 @@ +package com.restroly.qrmenu.subscription.controller; + +import com.restroly.qrmenu.subscription.dto.SubscriptionFeatureDto; +import com.restroly.qrmenu.subscription.dto.SubscriptionFeatureRequest; +import com.restroly.qrmenu.subscription.dto.RestaurantSubscriptionDto; +import com.restroly.qrmenu.subscription.dto.RestaurantSubscriptionRequest; +import com.restroly.qrmenu.subscription.dto.SubscriptionPlanDto; +import com.restroly.qrmenu.subscription.dto.SubscriptionPlanRequest; +import com.restroly.qrmenu.subscription.service.SubscriptionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.restroly.qrmenu.common.util.ApiConstants.SECURE_API_VERSION; + +@RestController +@RequestMapping(SECURE_API_VERSION + "/admin/subscriptions") +public class SuperAdminSubscriptionController { + + @Autowired + private SubscriptionService subscriptionService; + + @PostMapping("/plans") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity createPlan(@RequestBody SubscriptionPlanRequest request) { + return ResponseEntity.ok(subscriptionService.createPlan(request)); + } + + @PutMapping("/plans/{id}") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity updatePlan(@PathVariable Long id, @RequestBody SubscriptionPlanRequest request) { + return ResponseEntity.ok(subscriptionService.updatePlan(id, request)); + } + + @DeleteMapping("/plans/{id}") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity deletePlan(@PathVariable Long id) { + subscriptionService.deletePlan(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/plans") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity> getAllPlans() { + return ResponseEntity.ok(subscriptionService.getAllPlans()); + } + + @GetMapping("/plans/{id}") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity getPlanById(@PathVariable Long id) { + return ResponseEntity.ok(subscriptionService.getPlanById(id)); + } + + @PostMapping("/restaurants/{restId}/assign") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity assignPlanToRestaurant( + @PathVariable Long restId, + @RequestBody RestaurantSubscriptionRequest request) { + return ResponseEntity.ok(subscriptionService.assignPlanToRestaurant(restId, request)); + } + + @PostMapping("/features") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity createFeature(@RequestBody SubscriptionFeatureRequest request) { + return ResponseEntity.ok(subscriptionService.createFeature(request)); + } + + @PutMapping("/features/{id}") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity updateFeature(@PathVariable Long id, @RequestBody SubscriptionFeatureRequest request) { + return ResponseEntity.ok(subscriptionService.updateFeature(id, request)); + } + + @DeleteMapping("/features/{id}") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity deleteFeature(@PathVariable Long id) { + subscriptionService.deleteFeature(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/features") + @PreAuthorize("hasAuthority('SUPER_ADMIN')") + public ResponseEntity> getAllFeatures() { + return ResponseEntity.ok(subscriptionService.getAllFeatures()); + } +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/FeatureMappingRequest.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/FeatureMappingRequest.java new file mode 100644 index 0000000..8689c7d --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/FeatureMappingRequest.java @@ -0,0 +1,15 @@ +package com.restroly.qrmenu.subscription.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FeatureMappingRequest { + private Long featureId; + private String featureValue; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/PlanFeatureMappingDto.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/PlanFeatureMappingDto.java new file mode 100644 index 0000000..60aff04 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/PlanFeatureMappingDto.java @@ -0,0 +1,17 @@ +package com.restroly.qrmenu.subscription.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PlanFeatureMappingDto { + private Long id; + private Long featureId; + private String featureKey; + private String featureValue; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/RestaurantSubscriptionDto.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/RestaurantSubscriptionDto.java new file mode 100644 index 0000000..c6d32fb --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/RestaurantSubscriptionDto.java @@ -0,0 +1,22 @@ +package com.restroly.qrmenu.subscription.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RestaurantSubscriptionDto { + private Long id; + private Long restaurantId; + private SubscriptionPlanDto plan; + private LocalDateTime startDate; + private LocalDateTime endDate; + private String status; + private Boolean isAutoRenew; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/RestaurantSubscriptionRequest.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/RestaurantSubscriptionRequest.java new file mode 100644 index 0000000..051aea8 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/RestaurantSubscriptionRequest.java @@ -0,0 +1,16 @@ +package com.restroly.qrmenu.subscription.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RestaurantSubscriptionRequest { + private Long planId; + private Boolean isAutoRenew; + private Integer durationInMonths; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionFeatureDto.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionFeatureDto.java new file mode 100644 index 0000000..e4e03c4 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionFeatureDto.java @@ -0,0 +1,18 @@ +package com.restroly.qrmenu.subscription.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubscriptionFeatureDto { + private Long id; + private String featureKey; + private String displayName; + private String description; + private Boolean isActive; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionFeatureRequest.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionFeatureRequest.java new file mode 100644 index 0000000..4823e6b --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionFeatureRequest.java @@ -0,0 +1,17 @@ +package com.restroly.qrmenu.subscription.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubscriptionFeatureRequest { + private String featureKey; + private String displayName; + private String description; + private Boolean isActive; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionPlanDto.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionPlanDto.java new file mode 100644 index 0000000..7aebfb6 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionPlanDto.java @@ -0,0 +1,22 @@ +package com.restroly.qrmenu.subscription.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubscriptionPlanDto { + private Long id; + private String name; + private String description; + private Double price; + private String billingCycle; + private Boolean isActive; + private List features; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionPlanRequest.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionPlanRequest.java new file mode 100644 index 0000000..6825746 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/dto/SubscriptionPlanRequest.java @@ -0,0 +1,21 @@ +package com.restroly.qrmenu.subscription.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubscriptionPlanRequest { + private String name; + private String description; + private Double price; + private String billingCycle; + private Boolean isActive; + private List features; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/PlanFeatureMapping.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/PlanFeatureMapping.java new file mode 100644 index 0000000..2c21705 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/PlanFeatureMapping.java @@ -0,0 +1,29 @@ +package com.restroly.qrmenu.subscription.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "t_plan_feature_mapping") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PlanFeatureMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "plan_id", nullable = false) + private SubscriptionPlan plan; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "feature_id", nullable = false) + private SubscriptionFeature feature; + + @Column(name = "feature_value", nullable = false) + private String featureValue; // Can be "true", "false", or a number like "3" +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/RestaurantSubscription.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/RestaurantSubscription.java new file mode 100644 index 0000000..40fb3e0 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/RestaurantSubscription.java @@ -0,0 +1,59 @@ +package com.restroly.qrmenu.subscription.entity; + +import com.restroly.qrmenu.restaurant.entity.Restaurant; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "t_restaurant_subscription") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RestaurantSubscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "restaurant_id", nullable = false) + private Restaurant restaurant; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "plan_id", nullable = false) + private SubscriptionPlan plan; + + @Column(name = "start_date", nullable = false) + private LocalDateTime startDate; + + @Column(name = "end_date", nullable = false) + private LocalDateTime endDate; + + @Column(nullable = false) + private String status; // ACTIVE, EXPIRED, CANCELLED + + @Column(name = "is_auto_renew") + @Builder.Default + private Boolean isAutoRenew = true; + + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/SubscriptionFeature.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/SubscriptionFeature.java new file mode 100644 index 0000000..fc6cfd0 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/SubscriptionFeature.java @@ -0,0 +1,30 @@ +package com.restroly.qrmenu.subscription.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "t_subscription_feature") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SubscriptionFeature { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "feature_key", unique = true, nullable = false) + private String featureKey; + + @Column(name = "display_name") + private String displayName; + + private String description; + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/SubscriptionPlan.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/SubscriptionPlan.java new file mode 100644 index 0000000..1c4d9c5 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/entity/SubscriptionPlan.java @@ -0,0 +1,55 @@ +package com.restroly.qrmenu.subscription.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Table(name = "t_subscription_plan") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SubscriptionPlan { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String name; + + private String description; + + @Column(nullable = false) + private Double price; + + @Column(name = "billing_cycle") + private String billingCycle; // e.g., MONTHLY, YEARLY + + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + @OneToMany(mappedBy = "plan", cascade = CascadeType.ALL, orphanRemoval = true) + private List features; + + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/PlanFeatureMappingRepository.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/PlanFeatureMappingRepository.java new file mode 100644 index 0000000..58a7be2 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/PlanFeatureMappingRepository.java @@ -0,0 +1,15 @@ +package com.restroly.qrmenu.subscription.repository; + +import com.restroly.qrmenu.subscription.entity.PlanFeatureMapping; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PlanFeatureMappingRepository extends JpaRepository { + List findByPlanId(Long planId); + Optional findByPlanIdAndFeatureId(Long planId, Long featureId); + void deleteByPlanId(Long planId); +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/RestaurantSubscriptionRepository.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/RestaurantSubscriptionRepository.java new file mode 100644 index 0000000..de50211 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/RestaurantSubscriptionRepository.java @@ -0,0 +1,19 @@ +package com.restroly.qrmenu.subscription.repository; + +import com.restroly.qrmenu.subscription.entity.RestaurantSubscription; +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.List; +import java.util.Optional; + +@Repository +public interface RestaurantSubscriptionRepository extends JpaRepository { + + @Query("SELECT rs FROM RestaurantSubscription rs WHERE rs.restaurant.restId = :restId AND rs.status = 'ACTIVE'") + Optional findActiveSubscriptionByRestaurantId(@Param("restId") Long restId); + + List findByRestaurantRestId(Long restId); +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/SubscriptionFeatureRepository.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/SubscriptionFeatureRepository.java new file mode 100644 index 0000000..3725b65 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/SubscriptionFeatureRepository.java @@ -0,0 +1,12 @@ +package com.restroly.qrmenu.subscription.repository; + +import com.restroly.qrmenu.subscription.entity.SubscriptionFeature; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SubscriptionFeatureRepository extends JpaRepository { + Optional findByFeatureKey(String featureKey); +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/SubscriptionPlanRepository.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/SubscriptionPlanRepository.java new file mode 100644 index 0000000..ffc6a1b --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/repository/SubscriptionPlanRepository.java @@ -0,0 +1,12 @@ +package com.restroly.qrmenu.subscription.repository; + +import com.restroly.qrmenu.subscription.entity.SubscriptionPlan; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SubscriptionPlanRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/service/SubscriptionService.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/service/SubscriptionService.java new file mode 100644 index 0000000..05e6ea3 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/service/SubscriptionService.java @@ -0,0 +1,27 @@ +package com.restroly.qrmenu.subscription.service; + +import com.restroly.qrmenu.subscription.dto.RestaurantSubscriptionDto; +import com.restroly.qrmenu.subscription.dto.RestaurantSubscriptionRequest; +import com.restroly.qrmenu.subscription.dto.SubscriptionPlanDto; +import com.restroly.qrmenu.subscription.dto.SubscriptionPlanRequest; +import com.restroly.qrmenu.subscription.dto.SubscriptionFeatureDto; +import com.restroly.qrmenu.subscription.dto.SubscriptionFeatureRequest; + +import java.util.List; + +public interface SubscriptionService { + SubscriptionPlanDto createPlan(SubscriptionPlanRequest request); + SubscriptionPlanDto updatePlan(Long planId, SubscriptionPlanRequest request); + void deletePlan(Long planId); + List getAllPlans(); + SubscriptionPlanDto getPlanById(Long planId); + + RestaurantSubscriptionDto assignPlanToRestaurant(Long restaurantId, RestaurantSubscriptionRequest request); + RestaurantSubscriptionDto getRestaurantSubscription(Long restaurantId); + boolean isFeatureEnabled(Long restaurantId, String featureKey); + + SubscriptionFeatureDto createFeature(SubscriptionFeatureRequest request); + SubscriptionFeatureDto updateFeature(Long id, SubscriptionFeatureRequest request); + void deleteFeature(Long id); + List getAllFeatures(); +} diff --git a/RestroHub/src/main/java/com/restroly/qrmenu/subscription/service/impl/SubscriptionServiceImpl.java b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/service/impl/SubscriptionServiceImpl.java new file mode 100644 index 0000000..ea2b5e4 --- /dev/null +++ b/RestroHub/src/main/java/com/restroly/qrmenu/subscription/service/impl/SubscriptionServiceImpl.java @@ -0,0 +1,271 @@ +package com.restroly.qrmenu.subscription.service.impl; + +import com.restroly.qrmenu.restaurant.entity.Restaurant; +import com.restroly.qrmenu.restaurant.repository.RestaurantRepository; +import com.restroly.qrmenu.subscription.dto.*; +import com.restroly.qrmenu.subscription.entity.*; +import com.restroly.qrmenu.subscription.repository.*; +import com.restroly.qrmenu.subscription.service.SubscriptionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public class SubscriptionServiceImpl implements SubscriptionService { + + @Autowired + private SubscriptionPlanRepository planRepository; + + @Autowired + private SubscriptionFeatureRepository featureRepository; + + @Autowired + private PlanFeatureMappingRepository mappingRepository; + + @Autowired + private RestaurantSubscriptionRepository restaurantSubscriptionRepository; + + @Autowired + private RestaurantRepository restaurantRepository; + + @Override + @Transactional + public SubscriptionPlanDto createPlan(SubscriptionPlanRequest request) { + SubscriptionPlan plan = SubscriptionPlan.builder() + .name(request.getName()) + .description(request.getDescription()) + .price(request.getPrice()) + .billingCycle(request.getBillingCycle()) + .isActive(request.getIsActive() != null ? request.getIsActive() : true) + .build(); + + plan = planRepository.save(plan); + + if (request.getFeatures() != null && !request.getFeatures().isEmpty()) { + saveFeatureMappings(plan, request.getFeatures()); + } + + return mapToDto(planRepository.findById(plan.getId()).orElse(plan)); + } + + @Override + @Transactional + public SubscriptionPlanDto updatePlan(Long planId, SubscriptionPlanRequest request) { + SubscriptionPlan plan = planRepository.findById(planId) + .orElseThrow(() -> new RuntimeException("Plan not found with id: " + planId)); + + plan.setName(request.getName()); + plan.setDescription(request.getDescription()); + plan.setPrice(request.getPrice()); + plan.setBillingCycle(request.getBillingCycle()); + if (request.getIsActive() != null) { + plan.setIsActive(request.getIsActive()); + } + + mappingRepository.deleteByPlanId(planId); + + if (request.getFeatures() != null && !request.getFeatures().isEmpty()) { + saveFeatureMappings(plan, request.getFeatures()); + } + + return mapToDto(planRepository.save(plan)); + } + + private void saveFeatureMappings(SubscriptionPlan plan, List features) { + for (FeatureMappingRequest fmr : features) { + SubscriptionFeature feature = featureRepository.findById(fmr.getFeatureId()) + .orElseThrow(() -> new RuntimeException("Feature not found with id: " + fmr.getFeatureId())); + + PlanFeatureMapping mapping = PlanFeatureMapping.builder() + .plan(plan) + .feature(feature) + .featureValue(fmr.getFeatureValue()) + .build(); + mappingRepository.save(mapping); + } + } + + @Override + @Transactional + public void deletePlan(Long planId) { + if (!planRepository.existsById(planId)) { + throw new RuntimeException("Plan not found with id: " + planId); + } + planRepository.deleteById(planId); + } + + @Override + public List getAllPlans() { + return planRepository.findAll().stream() + .map(this::mapToDto) + .collect(Collectors.toList()); + } + + @Override + public SubscriptionPlanDto getPlanById(Long planId) { + return planRepository.findById(planId) + .map(this::mapToDto) + .orElseThrow(() -> new RuntimeException("Plan not found with id: " + planId)); + } + + @Override + @Transactional + public RestaurantSubscriptionDto assignPlanToRestaurant(Long restaurantId, RestaurantSubscriptionRequest request) { + Restaurant restaurant = restaurantRepository.findById(restaurantId) + .orElseThrow(() -> new RuntimeException("Restaurant not found with id: " + restaurantId)); + + SubscriptionPlan plan = planRepository.findById(request.getPlanId()) + .orElseThrow(() -> new RuntimeException("Plan not found with id: " + request.getPlanId())); + + Optional existingActive = restaurantSubscriptionRepository.findActiveSubscriptionByRestaurantId(restaurantId); + if (existingActive.isPresent()) { + RestaurantSubscription activeSub = existingActive.get(); + activeSub.setStatus("CANCELLED"); + activeSub.setEndDate(LocalDateTime.now()); + restaurantSubscriptionRepository.save(activeSub); + } + + LocalDateTime startDate = LocalDateTime.now(); + LocalDateTime endDate = startDate.plusMonths(request.getDurationInMonths() != null ? request.getDurationInMonths() : 1); + + RestaurantSubscription subscription = RestaurantSubscription.builder() + .restaurant(restaurant) + .plan(plan) + .startDate(startDate) + .endDate(endDate) + .status("ACTIVE") + .isAutoRenew(request.getIsAutoRenew() != null ? request.getIsAutoRenew() : true) + .build(); + + subscription = restaurantSubscriptionRepository.save(subscription); + return mapToDto(subscription); + } + + @Override + public RestaurantSubscriptionDto getRestaurantSubscription(Long restaurantId) { + return restaurantSubscriptionRepository.findActiveSubscriptionByRestaurantId(restaurantId) + .map(this::mapToDto) + .orElseThrow(() -> new RuntimeException("No active subscription found for restaurant id: " + restaurantId)); + } + + @Override + public boolean isFeatureEnabled(Long restaurantId, String featureKey) { + Optional activeSub = restaurantSubscriptionRepository.findActiveSubscriptionByRestaurantId(restaurantId); + if (activeSub.isEmpty()) { + return false; + } + + SubscriptionPlan plan = activeSub.get().getPlan(); + List mappings = mappingRepository.findByPlanId(plan.getId()); + + for (PlanFeatureMapping mapping : mappings) { + if (mapping.getFeature().getFeatureKey().equals(featureKey)) { + String val = mapping.getFeatureValue(); + return val != null && !val.equalsIgnoreCase("false") && !val.equals("0"); + } + } + return false; + } + + private SubscriptionPlanDto mapToDto(SubscriptionPlan plan) { + List featureDtos = new ArrayList<>(); + if (plan.getFeatures() != null) { + featureDtos = plan.getFeatures().stream().map(f -> PlanFeatureMappingDto.builder() + .id(f.getId()) + .featureId(f.getFeature().getId()) + .featureKey(f.getFeature().getFeatureKey()) + .featureValue(f.getFeatureValue()) + .build()).collect(Collectors.toList()); + } else { + List mappings = mappingRepository.findByPlanId(plan.getId()); + featureDtos = mappings.stream().map(f -> PlanFeatureMappingDto.builder() + .id(f.getId()) + .featureId(f.getFeature().getId()) + .featureKey(f.getFeature().getFeatureKey()) + .featureValue(f.getFeatureValue()) + .build()).collect(Collectors.toList()); + } + + return SubscriptionPlanDto.builder() + .id(plan.getId()) + .name(plan.getName()) + .description(plan.getDescription()) + .price(plan.getPrice()) + .billingCycle(plan.getBillingCycle()) + .isActive(plan.getIsActive()) + .features(featureDtos) + .build(); + } + + private RestaurantSubscriptionDto mapToDto(RestaurantSubscription rs) { + return RestaurantSubscriptionDto.builder() + .id(rs.getId()) + .restaurantId(rs.getRestaurant().getRestId()) + .plan(mapToDto(rs.getPlan())) + .startDate(rs.getStartDate()) + .endDate(rs.getEndDate()) + .status(rs.getStatus()) + .isAutoRenew(rs.getIsAutoRenew()) + .build(); + } + + @Override + @Transactional + public SubscriptionFeatureDto createFeature(SubscriptionFeatureRequest request) { + SubscriptionFeature feature = SubscriptionFeature.builder() + .featureKey(request.getFeatureKey()) + .displayName(request.getDisplayName()) + .description(request.getDescription()) + .isActive(request.getIsActive() != null ? request.getIsActive() : true) + .build(); + feature = featureRepository.save(feature); + return mapToFeatureDto(feature); + } + + @Override + @Transactional + public SubscriptionFeatureDto updateFeature(Long id, SubscriptionFeatureRequest request) { + SubscriptionFeature feature = featureRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Feature not found with id: " + id)); + feature.setFeatureKey(request.getFeatureKey()); + feature.setDisplayName(request.getDisplayName()); + feature.setDescription(request.getDescription()); + if (request.getIsActive() != null) { + feature.setIsActive(request.getIsActive()); + } + feature = featureRepository.save(feature); + return mapToFeatureDto(feature); + } + + @Override + @Transactional + public void deleteFeature(Long id) { + if (!featureRepository.existsById(id)) { + throw new RuntimeException("Feature not found with id: " + id); + } + featureRepository.deleteById(id); + } + + @Override + public List getAllFeatures() { + return featureRepository.findAll().stream() + .map(this::mapToFeatureDto) + .collect(Collectors.toList()); + } + + private SubscriptionFeatureDto mapToFeatureDto(SubscriptionFeature feature) { + return SubscriptionFeatureDto.builder() + .id(feature.getId()) + .featureKey(feature.getFeatureKey()) + .displayName(feature.getDisplayName()) + .description(feature.getDescription()) + .isActive(feature.getIsActive()) + .build(); + } +}