From b2bf113bbc95b3a2e01e262dddae08597f071310 Mon Sep 17 00:00:00 2001 From: Shivam Verma Date: Sat, 24 Jan 2026 11:28:03 +0530 Subject: [PATCH 01/23] Add GitHub Actions CI for Spring Boot tests (#68) --- .../.github/workflows/spring-tests.yml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 divinespark-backend/.github/workflows/spring-tests.yml diff --git a/divinespark-backend/.github/workflows/spring-tests.yml b/divinespark-backend/.github/workflows/spring-tests.yml new file mode 100644 index 0000000..97fdc77 --- /dev/null +++ b/divinespark-backend/.github/workflows/spring-tests.yml @@ -0,0 +1,31 @@ +name: Spring Boot CI + +on: + pull_request: + branches: [ main ] # run on PRs to main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' # or 21 depending on your project + + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build and run tests + run: mvn -B test + From 7dec930b17fe2bad8eb883875164e1fb8e22da06 Mon Sep 17 00:00:00 2001 From: ShivamVerma380 Date: Sat, 24 Jan 2026 11:33:47 +0530 Subject: [PATCH 02/23] Trigger CI From 8d88142821b2e97aac24be6626743f02796866bc Mon Sep 17 00:00:00 2001 From: Shivam Verma Date: Sat, 24 Jan 2026 11:37:15 +0530 Subject: [PATCH 03/23] Update Java version to 21 in workflow (#69) --- divinespark-backend/.github/workflows/spring-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/divinespark-backend/.github/workflows/spring-tests.yml b/divinespark-backend/.github/workflows/spring-tests.yml index 97fdc77..3c39c93 100644 --- a/divinespark-backend/.github/workflows/spring-tests.yml +++ b/divinespark-backend/.github/workflows/spring-tests.yml @@ -16,7 +16,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' # or 21 depending on your project + java-version: '21' # or 21 depending on your project - name: Cache Maven packages uses: actions/cache@v3 From de7bc5420fb5261fa91474490d76e5d0ca5509ec Mon Sep 17 00:00:00 2001 From: Shivam Verma Date: Sat, 24 Jan 2026 11:49:07 +0530 Subject: [PATCH 04/23] Rs/fix workflow location (#71) * Fix workflow path so GitHub Actions can detect it * Updated junit run * Updated yml --- .github/workflows/spring-tests.yml | 20 ++++++++++++ .../.github/workflows/spring-tests.yml | 31 ------------------- 2 files changed, 20 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/spring-tests.yml delete mode 100644 divinespark-backend/.github/workflows/spring-tests.yml diff --git a/.github/workflows/spring-tests.yml b/.github/workflows/spring-tests.yml new file mode 100644 index 0000000..b2ef8a2 --- /dev/null +++ b/.github/workflows/spring-tests.yml @@ -0,0 +1,20 @@ +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: divinespark-backend # <--- Add this line + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Build and test + run: mvn -B test diff --git a/divinespark-backend/.github/workflows/spring-tests.yml b/divinespark-backend/.github/workflows/spring-tests.yml deleted file mode 100644 index 3c39c93..0000000 --- a/divinespark-backend/.github/workflows/spring-tests.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Spring Boot CI - -on: - pull_request: - branches: [ main ] # run on PRs to main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' # or 21 depending on your project - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - - name: Build and run tests - run: mvn -B test - From 0074d88a4c53cfaff2bf401e84eb628e29fb6c59 Mon Sep 17 00:00:00 2001 From: rucha Date: Sat, 24 Jan 2026 16:42:04 +0530 Subject: [PATCH 05/23] Added blog feature --- .../controller/admin/AdminBlogController.java | 41 ++++++ .../controller/user/BlogController.java | 28 ++++ .../divinespark/dto/BlogCreateRequest.java | 50 +++++++ .../divinespark/dto/BlogDetailResponse.java | 52 +++++++ .../com/divinespark/dto/BlogListResponse.java | 60 +++++++++ .../java/com/divinespark/entity/Blog.java | 127 ++++++++++++++++++ .../divinespark/entity/enums/BlogStatus.java | 6 + .../repository/BlogRepository.java | 20 +++ .../com/divinespark/service/BlogService.java | 22 +++ .../service/impl/BlogServiceImpl.java | 126 +++++++++++++++++ 10 files changed, 532 insertions(+) create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminBlogController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/user/BlogController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/BlogCreateRequest.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/BlogDetailResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/entity/Blog.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/entity/enums/BlogStatus.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/repository/BlogRepository.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/BlogService.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminBlogController.java b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminBlogController.java new file mode 100644 index 0000000..076d9e9 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminBlogController.java @@ -0,0 +1,41 @@ +package com.divinespark.controller.admin; + + +import com.divinespark.dto.BlogCreateRequest; +import com.divinespark.entity.Blog; +import com.divinespark.service.BlogService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/admin/blogs") +public class AdminBlogController { + + private final BlogService blogService; + + public AdminBlogController(BlogService blogService) { + this.blogService = blogService; + } + + @PreAuthorize("hasRole('ADMIN')") + @PostMapping + public void createBlog(@RequestBody BlogCreateRequest request) { + blogService.createBlog(request); + } + + @PreAuthorize("hasRole('ADMIN')") + @PutMapping("/{id}") + public void updateBlog(@PathVariable Long id, @RequestBody BlogCreateRequest request) { + blogService.updateBlog(id, request); + } + + @PatchMapping("/{id}/publish") + public void publishBlog(@PathVariable Long id) { + blogService.publishBlog(id); + } + + @DeleteMapping("/{id}") + public void deleteBlog(@PathVariable Long id) { + blogService.deleteBlog(id); + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/BlogController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/BlogController.java new file mode 100644 index 0000000..4943629 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/BlogController.java @@ -0,0 +1,28 @@ +package com.divinespark.controller.user; + +import com.divinespark.dto.*; +import com.divinespark.service.BlogService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/blogs") +public class BlogController { + + private final BlogService blogService; + + public BlogController(BlogService blogService) { + this.blogService = blogService; + } + + @GetMapping + public List getBlogs() { + return blogService.getPublishedBlogs(); + } + + @GetMapping("/{slug}") + public BlogDetailResponse getBlog(@PathVariable String slug) { + return blogService.getBlogBySlug(slug); + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/BlogCreateRequest.java b/divinespark-backend/src/main/java/com/divinespark/dto/BlogCreateRequest.java new file mode 100644 index 0000000..b42b876 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/BlogCreateRequest.java @@ -0,0 +1,50 @@ +package com.divinespark.dto; + +public class BlogCreateRequest { + + private String title; + private String excerpt; + private String content; + private String authorName; + private String authorRole; + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getAuthorRole() { + return authorRole; + } + + public void setAuthorRole(String authorRole) { + this.authorRole = authorRole; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getExcerpt() { + return excerpt; + } + + public void setExcerpt(String excerpt) { + this.excerpt = excerpt; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/BlogDetailResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/BlogDetailResponse.java new file mode 100644 index 0000000..6b8a3e5 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/BlogDetailResponse.java @@ -0,0 +1,52 @@ +package com.divinespark.dto; + +import java.time.OffsetDateTime; + +public class BlogDetailResponse { + + private String title; + private String content; + private String authorName; + private String authorRole; + private OffsetDateTime createdAt; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getAuthorRole() { + return authorRole; + } + + public void setAuthorRole(String authorRole) { + this.authorRole = authorRole; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java new file mode 100644 index 0000000..29d29c2 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java @@ -0,0 +1,60 @@ +package com.divinespark.dto; + +import java.time.OffsetDateTime; + +public class BlogListResponse { + private String title; + private String slug; + private String excerpt; + private String authorName; + private String authorRole; + private OffsetDateTime createdAt; + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getAuthorRole() { + return authorRole; + } + + public void setAuthorRole(String authorRole) { + this.authorRole = authorRole; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public String getExcerpt() { + return excerpt; + } + + public void setExcerpt(String excerpt) { + this.excerpt = excerpt; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/Blog.java b/divinespark-backend/src/main/java/com/divinespark/entity/Blog.java new file mode 100644 index 0000000..c2c8fec --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/entity/Blog.java @@ -0,0 +1,127 @@ +package com.divinespark.entity; + +import com.divinespark.entity.enums.BlogStatus; +import jakarta.persistence.*; + +import java.time.OffsetDateTime; + +@Entity +@Table( + name = "blogs", + indexes = { + @Index(name = "idx_blog_slug", columnList = "slug"), + @Index(name = "idx_blog_status", columnList = "status") + } +) +public class Blog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 200) + private String title; + + @Column(nullable = false, unique = true, length = 220) + private String slug; + + @Column(length = 500) + private String excerpt; + + @Column(columnDefinition = "TEXT", nullable = false) + private String content; + + // Author details (denormalized, intentional) + @Column(nullable = false) + private String authorName; + + @Column(nullable = false) + private String authorRole; // Admin / Editorial Team + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private BlogStatus status = BlogStatus.DRAFT; + + @Column(nullable = false, updatable = false) + private OffsetDateTime createdAt = OffsetDateTime.now(); + + private OffsetDateTime updatedAt; + + // ---------------- Getters & Setters ---------------- + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getExcerpt() { + return excerpt; + } + + public void setExcerpt(String excerpt) { + this.excerpt = excerpt; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getAuthorRole() { + return authorRole; + } + + public void setAuthorRole(String authorRole) { + this.authorRole = authorRole; + } + + public BlogStatus getStatus() { + return status; + } + + public void setStatus(BlogStatus status) { + this.status = status; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/enums/BlogStatus.java b/divinespark-backend/src/main/java/com/divinespark/entity/enums/BlogStatus.java new file mode 100644 index 0000000..e4567ac --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/entity/enums/BlogStatus.java @@ -0,0 +1,6 @@ +package com.divinespark.entity.enums; + +public enum BlogStatus { + DRAFT, + PUBLISHED +} diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/BlogRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/BlogRepository.java new file mode 100644 index 0000000..21544bf --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/repository/BlogRepository.java @@ -0,0 +1,20 @@ +package com.divinespark.repository; + +import com.divinespark.entity.Blog; +import com.divinespark.entity.enums.BlogStatus; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface BlogRepository extends JpaRepository { + + // Public blogs (blog listing page) + List findByStatusOrderByCreatedAtDesc(BlogStatus status); + + // Blog detail page (SEO friendly) + Optional findBySlugAndStatus(String slug, BlogStatus status); + + // Admin checks + boolean existsBySlug(String slug); +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/BlogService.java b/divinespark-backend/src/main/java/com/divinespark/service/BlogService.java new file mode 100644 index 0000000..20f7c4a --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/BlogService.java @@ -0,0 +1,22 @@ +package com.divinespark.service; + +import com.divinespark.dto.BlogCreateRequest; +import com.divinespark.dto.BlogDetailResponse; +import com.divinespark.dto.BlogListResponse; + +import java.util.List; + +public interface BlogService { + + void createBlog(BlogCreateRequest request); + + void updateBlog(Long id, BlogCreateRequest request); + + void publishBlog(Long id); + + void deleteBlog(Long id); + + List getPublishedBlogs(); + + BlogDetailResponse getBlogBySlug(String slug); +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java new file mode 100644 index 0000000..c0794c2 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java @@ -0,0 +1,126 @@ +package com.divinespark.service.impl; + +import com.divinespark.dto.*; +import com.divinespark.entity.Blog; +import com.divinespark.entity.enums.BlogStatus; +import com.divinespark.repository.BlogRepository; +import com.divinespark.service.BlogService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.text.Normalizer; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@Service +@Transactional +public class BlogServiceImpl implements BlogService { + + private final BlogRepository blogRepository; + + public BlogServiceImpl(BlogRepository blogRepository) { + this.blogRepository = blogRepository; + } + + @Override + public void createBlog(BlogCreateRequest request) { + + String slug = generateSlug(request.getTitle()); + + if (blogRepository.existsBySlug(slug)) { + throw new RuntimeException("Blog with same title already exists"); + } + + Blog blog = new Blog(); + blog.setTitle(request.getTitle()); + blog.setSlug(slug); + blog.setExcerpt(request.getExcerpt()); + blog.setContent(request.getContent()); + blog.setAuthorName(request.getAuthorName()); + blog.setAuthorRole(request.getAuthorRole()); + blog.setStatus(BlogStatus.DRAFT); + + blogRepository.save(blog); + } + + @Override + public void updateBlog(Long id, BlogCreateRequest request) { + + Blog blog = blogRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Blog not found")); + + blog.setTitle(request.getTitle()); + blog.setExcerpt(request.getExcerpt()); + blog.setContent(request.getContent()); + blog.setAuthorName(request.getAuthorName()); + blog.setAuthorRole(request.getAuthorRole()); + blog.setUpdatedAt(OffsetDateTime.now()); + } + + @Override + public void publishBlog(Long id) { + + Blog blog = blogRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Blog not found")); + + blog.setStatus(BlogStatus.PUBLISHED); + blog.setUpdatedAt(OffsetDateTime.now()); + } + + @Override + public void deleteBlog(Long id) { + blogRepository.deleteById(id); + } + + @Override + public List getPublishedBlogs() { + + List blogs = blogRepository + .findByStatusOrderByCreatedAtDesc(BlogStatus.PUBLISHED); + + List response = new ArrayList<>(); + + for (Blog b : blogs) { + BlogListResponse dto = new BlogListResponse(); + dto.setTitle(b.getTitle()); + dto.setSlug(b.getSlug()); + dto.setExcerpt(b.getExcerpt()); + dto.setAuthorName(b.getAuthorName()); + dto.setAuthorRole(b.getAuthorRole()); + dto.setCreatedAt(b.getCreatedAt()); + response.add(dto); + } + + return response; + } + + @Override + public BlogDetailResponse getBlogBySlug(String slug) { + + Blog blog = blogRepository + .findBySlugAndStatus(slug, BlogStatus.PUBLISHED) + .orElseThrow(() -> new RuntimeException("Blog not found")); + + BlogDetailResponse res = new BlogDetailResponse(); + res.setTitle(blog.getTitle()); + res.setContent(blog.getContent()); + res.setAuthorName(blog.getAuthorName()); + res.setAuthorRole(blog.getAuthorRole()); + res.setCreatedAt(blog.getCreatedAt()); + + return res; + } + + // ---------- SLUG GENERATOR ---------- + private String generateSlug(String input) { + String slug = Normalizer.normalize(input, Normalizer.Form.NFD) + .replaceAll("[^\\w\\s-]", "") + .trim() + .replaceAll("\\s+", "-") + .toLowerCase(Locale.ENGLISH); + + return slug; + } +} From bb5f63e2738b034a5b8d1f3069d2542724419a2b Mon Sep 17 00:00:00 2001 From: Neha Date: Mon, 26 Jan 2026 12:17:07 +0530 Subject: [PATCH 06/23] Added an event-ticker --- .../admin/AdminEventController.java | 44 ++++++++++ .../publicapi/PublicEventController.java | 26 ++++++ .../com/divinespark/dto/EventRequest.java | 57 ++++++++++++ .../divinespark/dto/EventTickerResponse.java | 53 ++++++++++++ .../java/com/divinespark/entity/Event.java | 84 ++++++++++++++++++ .../repository/EventRepository.java | 23 +++++ .../divinespark/security/SecurityConfig.java | 35 +++++--- .../com/divinespark/service/EventService.java | 20 +++++ .../service/impl/EventServiceImpl.java | 86 +++++++++++++++++++ 9 files changed, 418 insertions(+), 10 deletions(-) create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminEventController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/publicapi/PublicEventController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/EventRequest.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/EventTickerResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/entity/Event.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/repository/EventRepository.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/EventService.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/impl/EventServiceImpl.java diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminEventController.java b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminEventController.java new file mode 100644 index 0000000..59d4295 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminEventController.java @@ -0,0 +1,44 @@ +package com.divinespark.controller.admin; + +import com.divinespark.dto.EventRequest; +import com.divinespark.service.EventService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/admin/events") +@PreAuthorize("hasRole('ADMIN')") +public class AdminEventController { + + private final EventService service; + + public AdminEventController(EventService service) { + this.service = service; + } + + @PostMapping + public ResponseEntity create(@Valid @RequestBody EventRequest request) { + return ResponseEntity.ok(service.create(request)); + } + + @PutMapping("/{id}") + public ResponseEntity update( + @PathVariable Long id, + @Valid @RequestBody EventRequest request) { + return ResponseEntity.ok(service.update(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping + public ResponseEntity list() { + return ResponseEntity.ok(service.getAllAdmin()); + } +} + diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/publicapi/PublicEventController.java b/divinespark-backend/src/main/java/com/divinespark/controller/publicapi/PublicEventController.java new file mode 100644 index 0000000..25d22d0 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/publicapi/PublicEventController.java @@ -0,0 +1,26 @@ +package com.divinespark.controller.publicapi; + +import com.divinespark.dto.EventTickerResponse; +import com.divinespark.service.EventService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/public/events") +public class PublicEventController { + + private final EventService service; + + public PublicEventController(EventService service) { + this.service = service; + } + + @GetMapping("/ticker") + public ResponseEntity> getTicker() { + return ResponseEntity.ok(service.getTickerEvents()); + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/EventRequest.java b/divinespark-backend/src/main/java/com/divinespark/dto/EventRequest.java new file mode 100644 index 0000000..5cae862 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/EventRequest.java @@ -0,0 +1,57 @@ +package com.divinespark.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.time.OffsetDateTime; + +public class EventRequest { + + @NotBlank + private String title; + + @NotBlank + @Size(max = 300) + private String description; + + @NotNull + private OffsetDateTime startTime; + + @NotNull + @Min(1) + private Integer durationMinutes; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getDurationMinutes() { + return durationMinutes; + } + + public void setDurationMinutes(Integer durationMinutes) { + this.durationMinutes = durationMinutes; + } + + public OffsetDateTime getStartTime() { + return startTime; + } + + public void setStartTime(OffsetDateTime startTime) { + this.startTime = startTime; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/EventTickerResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/EventTickerResponse.java new file mode 100644 index 0000000..8585a07 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/EventTickerResponse.java @@ -0,0 +1,53 @@ +package com.divinespark.dto; + +import java.time.OffsetDateTime; + +public class EventTickerResponse { + + private Long id; + private String title; + private String description; + private OffsetDateTime startTime; + private Integer durationMinutes; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public OffsetDateTime getStartTime() { + return startTime; + } + + public void setStartTime(OffsetDateTime startTime) { + this.startTime = startTime; + } + + public Integer getDurationMinutes() { + return durationMinutes; + } + + public void setDurationMinutes(Integer durationMinutes) { + this.durationMinutes = durationMinutes; + } +} + diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/Event.java b/divinespark-backend/src/main/java/com/divinespark/entity/Event.java new file mode 100644 index 0000000..334707c --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/entity/Event.java @@ -0,0 +1,84 @@ +package com.divinespark.entity; + +import jakarta.persistence.*; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "events") +public class Event { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false, length = 300) + private String description; + // 1โ€“2 lines max + + @Column(nullable = false) + private OffsetDateTime startTime; + + @Column(nullable = false) + private Integer durationMinutes; + // auto-expiry logic uses this + + @Column(nullable = false, updatable = false) + private OffsetDateTime createdAt; + + @PrePersist + public void onCreate() { + this.createdAt = OffsetDateTime.now(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public OffsetDateTime getStartTime() { + return startTime; + } + + public void setStartTime(OffsetDateTime startTime) { + this.startTime = startTime; + } + + public Integer getDurationMinutes() { + return durationMinutes; + } + + public void setDurationMinutes(Integer durationMinutes) { + this.durationMinutes = durationMinutes; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/EventRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/EventRepository.java new file mode 100644 index 0000000..30dc8bb --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/repository/EventRepository.java @@ -0,0 +1,23 @@ +package com.divinespark.repository; + +import com.divinespark.entity.Event; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.OffsetDateTime; +import java.util.List; + +public interface EventRepository + extends JpaRepository { + + @Query(""" + SELECT w FROM Event w + WHERE (w.startTime + (w.durationMinutes * 1 minute)) > :now + ORDER BY w.startTime ASC + """) + List findActiveForTicker(@Param("now") OffsetDateTime now); + + List findAllByOrderByCreatedAtDesc(); +} + diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index 9ddb3da..34cdb3b 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -50,27 +50,41 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/v3/api-docs/**", "/api/v1/auth/**", "/api/v1/payments/webhook", - "/api/v1/user/**", - "api/v1/public/**" + "/api/v1/public/**" ).permitAll() - // Public session browsing - .requestMatchers(HttpMethod.GET, "/api/v1/sessions/**", "/api/v1/admin/thumbnail/").permitAll() + // User APIs + .requestMatchers("/api/v1/user/**").hasRole("USER") - // Protected session actions - .requestMatchers(HttpMethod.POST, "/api/v1/sessions/**").hasRole("USER") + // Public session browsing + .requestMatchers(HttpMethod.GET, "/api/v1/sessions/**").permitAll() - // Admin + // Admin APIs .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) - - .oauth2Login(oauth -> oauth - .successHandler(oAuth2SuccessHandler) + // THIS PREVENTS GOOGLE REDIRECTS +// .exceptionHandling(ex -> ex +// .authenticationEntryPoint((request, response, authException) -> { +// response.setStatus(401); +// response.setContentType("application/json"); +// response.getWriter().write(""" +// { +// "error": "Unauthorized", +// "message": "JWT token missing or invalid" +// } +// """); +// }) +// ) + + // OAuth ONLY for browser login + .oauth2Login(oauth -> + oauth.successHandler(oAuth2SuccessHandler) ) + // JWT filter .addFilterBefore( jwtAuthFilter, UsernamePasswordAuthenticationFilter.class @@ -79,6 +93,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } + @Bean public CorsConfigurationSource corsConfigurationSource() { diff --git a/divinespark-backend/src/main/java/com/divinespark/service/EventService.java b/divinespark-backend/src/main/java/com/divinespark/service/EventService.java new file mode 100644 index 0000000..5f2957b --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/EventService.java @@ -0,0 +1,20 @@ +package com.divinespark.service; + +import com.divinespark.dto.EventRequest; +import com.divinespark.dto.EventTickerResponse; +import com.divinespark.entity.Event; + +import java.util.List; + +public interface EventService { + + Event create(EventRequest request); + + Event update(Long id, EventRequest request); + + void delete(Long id); + + List getAllAdmin(); + + List getTickerEvents(); +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/EventServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/EventServiceImpl.java new file mode 100644 index 0000000..6153231 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/EventServiceImpl.java @@ -0,0 +1,86 @@ +package com.divinespark.service.impl; + +import com.divinespark.dto.EventRequest; +import com.divinespark.dto.EventTickerResponse; +import com.divinespark.entity.Event; +import com.divinespark.repository.EventRepository; +import com.divinespark.service.EventService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.OffsetDateTime; +import java.util.Comparator; +import java.util.List; + +@Service +@Transactional +public class EventServiceImpl implements EventService { + + private final EventRepository repository; + + public EventServiceImpl(EventRepository repository) { + this.repository = repository; + } + + @Override + public Event create(EventRequest request) { + Event event = new Event(); + mapRequest(event, request); + return repository.save(event); + } + + @Override + public Event update(Long id, EventRequest request) { + Event event = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Workshop/Event not found")); + + mapRequest(event, request); + return repository.save(event); + } + + @Override + public void delete(Long id) { + repository.deleteById(id); + } + + @Override + @Transactional(readOnly = true) + public List getAllAdmin() { + return repository.findAllByOrderByCreatedAtDesc(); + } + + @Override + @Transactional(readOnly = true) + public List getTickerEvents() { + + OffsetDateTime now = OffsetDateTime.now(); + + return repository.findAll().stream() + .filter(event -> + event.getStartTime() + .plusMinutes(event.getDurationMinutes()) + .isAfter(now) + ) + .sorted(Comparator.comparing(Event::getStartTime)) + .limit(10) + .map(this::mapToTicker) + .toList(); + } + + private void mapRequest(Event event, EventRequest request) { + event.setTitle(request.getTitle()); + event.setDescription(request.getDescription()); + event.setStartTime(request.getStartTime()); + event.setDurationMinutes(request.getDurationMinutes()); + } + + private EventTickerResponse mapToTicker(Event event) { + EventTickerResponse dto = new EventTickerResponse(); + dto.setId(event.getId()); + dto.setTitle(event.getTitle()); + dto.setDescription(event.getDescription()); + dto.setStartTime(event.getStartTime()); + dto.setDurationMinutes(event.getDurationMinutes()); + return dto; + } +} From 4d95f59605044d1578b3693a7ea9e91d521ad228 Mon Sep 17 00:00:00 2001 From: sujaljain272004 <160271734+sujaljain272004@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:38:16 +0530 Subject: [PATCH 07/23] Blog issue ! --- .../controller/admin/AdminBlogController.java | 30 +++++++-- .../com/divinespark/dto/BlogListResponse.java | 11 ++++ .../dto/SessionListItemResponse.java | 61 +++++++++++++++++++ .../repository/BlogRepository.java | 8 +++ .../divinespark/security/SecurityConfig.java | 2 +- .../com/divinespark/service/BlogService.java | 5 ++ .../service/impl/BlogServiceImpl.java | 40 ++++++++++++ 7 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/SessionListItemResponse.java diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminBlogController.java b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminBlogController.java index 076d9e9..173fd79 100644 --- a/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminBlogController.java +++ b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminBlogController.java @@ -1,14 +1,17 @@ package com.divinespark.controller.admin; - import com.divinespark.dto.BlogCreateRequest; -import com.divinespark.entity.Blog; +import com.divinespark.dto.BlogDetailResponse; +import com.divinespark.dto.BlogListResponse; import com.divinespark.service.BlogService; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequestMapping("/api/v1/admin/blogs") +@PreAuthorize("hasRole('ADMIN')") public class AdminBlogController { private final BlogService blogService; @@ -17,23 +20,40 @@ public AdminBlogController(BlogService blogService) { this.blogService = blogService; } - @PreAuthorize("hasRole('ADMIN')") + // ๐Ÿ”น GET all blogs (admin view: includes drafts + published) + @GetMapping + public List getAllBlogs() { + return blogService.getAllBlogsForAdmin(); + } + + // ๐Ÿ”น GET single blog by ID (admin edit view) + @GetMapping("/{id}") + public BlogDetailResponse getBlog(@PathVariable Long id) { + return blogService.getBlogByIdForAdmin(id); + } + + // ๐Ÿ”น CREATE blog @PostMapping public void createBlog(@RequestBody BlogCreateRequest request) { blogService.createBlog(request); } - @PreAuthorize("hasRole('ADMIN')") + // ๐Ÿ”น UPDATE blog @PutMapping("/{id}") - public void updateBlog(@PathVariable Long id, @RequestBody BlogCreateRequest request) { + public void updateBlog( + @PathVariable Long id, + @RequestBody BlogCreateRequest request + ) { blogService.updateBlog(id, request); } + // ๐Ÿ”น PUBLISH / UNPUBLISH blog @PatchMapping("/{id}/publish") public void publishBlog(@PathVariable Long id) { blogService.publishBlog(id); } + // ๐Ÿ”น DELETE blog @DeleteMapping("/{id}") public void deleteBlog(@PathVariable Long id) { blogService.deleteBlog(id); diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java index 29d29c2..da4c3a4 100644 --- a/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java +++ b/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java @@ -9,6 +9,8 @@ public class BlogListResponse { private String authorName; private String authorRole; private OffsetDateTime createdAt; + private Long id; + public String getAuthorName() { return authorName; @@ -17,6 +19,15 @@ public String getAuthorName() { public void setAuthorName(String authorName) { this.authorName = authorName; } + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getAuthorRole() { return authorRole; diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/SessionListItemResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/SessionListItemResponse.java new file mode 100644 index 0000000..c531a97 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/SessionListItemResponse.java @@ -0,0 +1,61 @@ +package com.divinespark.dto; + +import com.divinespark.entity.enums.SessionStatus; +import com.divinespark.entity.enums.SessionType; + +import java.time.OffsetDateTime; + +public class SessionListItemResponse { + + private Long id; + private String title; + private String description; + private SessionType type; + private double price; + private OffsetDateTime startTime; + private OffsetDateTime endTime; + private SessionStatus status; + private int maxSeats; + private int availableSeats; + private String guideName; + private boolean hasThumbnail; + + + // ---------- getters & setters ---------- + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public SessionType getType() { return type; } + public void setType(SessionType type) { this.type = type; } + + public double getPrice() { return price; } + public void setPrice(double price) { this.price = price; } + + public OffsetDateTime getStartTime() { return startTime; } + public void setStartTime(OffsetDateTime startTime) { this.startTime = startTime; } + + public OffsetDateTime getEndTime() { return endTime; } + public void setEndTime(OffsetDateTime endTime) { this.endTime = endTime; } + + public SessionStatus getStatus() { return status; } + public void setStatus(SessionStatus status) { this.status = status; } + + public int getMaxSeats() { return maxSeats; } + public void setMaxSeats(int maxSeats) { this.maxSeats = maxSeats; } + + public int getAvailableSeats() { return availableSeats; } + public void setAvailableSeats(int availableSeats) { this.availableSeats = availableSeats; } + + public String getGuideName() { return guideName; } + public void setGuideName(String guideName) { this.guideName = guideName; } + + public boolean isHasThumbnail() { return hasThumbnail; } + public void setHasThumbnail(boolean hasThumbnail) { this.hasThumbnail = hasThumbnail; } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/BlogRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/BlogRepository.java index 21544bf..47f6707 100644 --- a/divinespark-backend/src/main/java/com/divinespark/repository/BlogRepository.java +++ b/divinespark-backend/src/main/java/com/divinespark/repository/BlogRepository.java @@ -17,4 +17,12 @@ public interface BlogRepository extends JpaRepository { // Admin checks boolean existsBySlug(String slug); + + List findAllByOrderByCreatedAtDesc(); + + + + ; + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index 34cdb3b..fcf1a63 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -121,4 +121,4 @@ public CorsConfigurationSource corsConfigurationSource() { // public PasswordEncoder passwordEncoder() { // return new BCryptPasswordEncoder(); // } -} +} \ No newline at end of file diff --git a/divinespark-backend/src/main/java/com/divinespark/service/BlogService.java b/divinespark-backend/src/main/java/com/divinespark/service/BlogService.java index 20f7c4a..72fec4a 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/BlogService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/BlogService.java @@ -19,4 +19,9 @@ public interface BlogService { List getPublishedBlogs(); BlogDetailResponse getBlogBySlug(String slug); + + List getAllBlogsForAdmin(); + + BlogDetailResponse getBlogByIdForAdmin(Long id); + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java index c0794c2..d8d22e2 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java @@ -123,4 +123,44 @@ private String generateSlug(String input) { return slug; } + + @Override + public List getAllBlogsForAdmin() { + + List blogs = blogRepository.findAllByOrderByCreatedAtDesc(); + List response = new ArrayList<>(); + + for (Blog b : blogs) { + BlogListResponse dto = new BlogListResponse(); + dto.setId(b.getId()); // โญ THIS WAS MISSING + dto.setTitle(b.getTitle()); + dto.setSlug(b.getSlug()); + dto.setExcerpt(b.getExcerpt()); + dto.setAuthorName(b.getAuthorName()); + dto.setAuthorRole(b.getAuthorRole()); + dto.setCreatedAt(b.getCreatedAt()); + response.add(dto); + } + + return response; + } + + + @Override + public BlogDetailResponse getBlogByIdForAdmin(Long id) { + + Blog blog = blogRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Blog not found")); + + BlogDetailResponse res = new BlogDetailResponse(); + res.setTitle(blog.getTitle()); + res.setContent(blog.getContent()); + res.setAuthorName(blog.getAuthorName()); + res.setAuthorRole(blog.getAuthorRole()); + res.setCreatedAt(blog.getCreatedAt()); + + return res; + } + + } From dcaa15fb8f61a9c2e354bacd8919b078dbfc3ab5 Mon Sep 17 00:00:00 2001 From: Neha Date: Mon, 26 Jan 2026 18:24:10 +0530 Subject: [PATCH 08/23] gallery section implementation with imageurl for future scaling --- .../admin/AdminProgramController.java | 44 +++++++++ .../publicapi/PublicProgramController.java | 25 +++++ .../com/divinespark/dto/ProgramRequest.java | 54 +++++++++++ .../com/divinespark/dto/ProgramResponse.java | 41 ++++++++ .../java/com/divinespark/entity/Program.java | 95 +++++++++++++++++++ .../repository/ProgramRepository.java | 13 +++ .../divinespark/service/ProgramService.java | 20 ++++ .../service/impl/ProgramServiceImpl.java | 75 +++++++++++++++ 8 files changed, 367 insertions(+) create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminProgramController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/publicapi/PublicProgramController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/ProgramRequest.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/ProgramResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/entity/Program.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/repository/ProgramRepository.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/ProgramService.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/impl/ProgramServiceImpl.java diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminProgramController.java b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminProgramController.java new file mode 100644 index 0000000..c881c9b --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminProgramController.java @@ -0,0 +1,44 @@ +package com.divinespark.controller.admin; + +import com.divinespark.dto.ProgramRequest; +import com.divinespark.service.ProgramService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/admin/programs") +@PreAuthorize("hasRole('ADMIN')") +public class AdminProgramController { + + private final ProgramService service; + + public AdminProgramController(ProgramService service) { + this.service = service; + } + + @PostMapping + public ResponseEntity create(@Valid @RequestBody ProgramRequest request) { + return ResponseEntity.ok(service.create(request)); + } + + @PutMapping("/{id}") + public ResponseEntity update( + @PathVariable Long id, + @Valid @RequestBody ProgramRequest request) { + return ResponseEntity.ok(service.update(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping + public ResponseEntity list() { + return ResponseEntity.ok(service.getAllAdmin()); + } +} + diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/publicapi/PublicProgramController.java b/divinespark-backend/src/main/java/com/divinespark/controller/publicapi/PublicProgramController.java new file mode 100644 index 0000000..91202c7 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/publicapi/PublicProgramController.java @@ -0,0 +1,25 @@ +package com.divinespark.controller.publicapi; + +import com.divinespark.service.ProgramService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/public/programs") +public class PublicProgramController { + + private final ProgramService service; + + public PublicProgramController(ProgramService service) { + this.service = service; + } + + @GetMapping + public ResponseEntity getByCategory( + @RequestParam String category) { + return ResponseEntity.ok(service.getByCategory(category)); + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/ProgramRequest.java b/divinespark-backend/src/main/java/com/divinespark/dto/ProgramRequest.java new file mode 100644 index 0000000..bfbcf48 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/ProgramRequest.java @@ -0,0 +1,54 @@ +package com.divinespark.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class ProgramRequest { + + @NotBlank + private String title; + + @NotBlank + @Size(max = 300) + private String description; + + @NotBlank + private String category; + // ENERGY_WORKSHOPS / SPIRITUAL_TRIPS + + @NotBlank + private String imageUrl; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} + diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/ProgramResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/ProgramResponse.java new file mode 100644 index 0000000..0d8d4b4 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/ProgramResponse.java @@ -0,0 +1,41 @@ +package com.divinespark.dto; + +public class ProgramResponse { + + private Long id; + private String title; + private String description; + private String imageUrl; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/Program.java b/divinespark-backend/src/main/java/com/divinespark/entity/Program.java new file mode 100644 index 0000000..6de14af --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/entity/Program.java @@ -0,0 +1,95 @@ +package com.divinespark.entity; + +import jakarta.persistence.*; + +import java.time.OffsetDateTime; + +@Entity +@Table(name = "programs") +public class Program { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false, length = 300) + private String description; + + @Column(nullable = false) + private String category; + // ENERGY_WORKSHOPS or SPIRITUAL_TRIPS + + @Column(nullable = false) + private String imageUrl; + // e.g. /programs/energy/sound-healing.jpg + + @Column(nullable = false) + private boolean active = true; + + @Column(nullable = false, updatable = false) + private OffsetDateTime createdAt; + + @PrePersist + public void onCreate() { + this.createdAt = OffsetDateTime.now(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/ProgramRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/ProgramRepository.java new file mode 100644 index 0000000..9432240 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/repository/ProgramRepository.java @@ -0,0 +1,13 @@ +package com.divinespark.repository; + +import com.divinespark.entity.Program; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ProgramRepository extends JpaRepository { + + List findByCategoryAndActiveTrueOrderByCreatedAtDesc(String category); + + List findAllByOrderByCreatedAtDesc(); +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/ProgramService.java b/divinespark-backend/src/main/java/com/divinespark/service/ProgramService.java new file mode 100644 index 0000000..540e6fb --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/ProgramService.java @@ -0,0 +1,20 @@ +package com.divinespark.service; + +import com.divinespark.dto.ProgramRequest; +import com.divinespark.dto.ProgramResponse; +import com.divinespark.entity.Program; + +import java.util.List; + +public interface ProgramService { + + Program create(ProgramRequest request); + + Program update(Long id, ProgramRequest request); + + void delete(Long id); + + List getAllAdmin(); + + List getByCategory(String category); +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/ProgramServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/ProgramServiceImpl.java new file mode 100644 index 0000000..b5a3a62 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/ProgramServiceImpl.java @@ -0,0 +1,75 @@ +package com.divinespark.service.impl; + +import com.divinespark.dto.ProgramRequest; +import com.divinespark.dto.ProgramResponse; +import com.divinespark.entity.Program; +import com.divinespark.repository.ProgramRepository; +import com.divinespark.service.ProgramService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +public class ProgramServiceImpl implements ProgramService { + + private final ProgramRepository repository; + + public ProgramServiceImpl(ProgramRepository repository) { + this.repository = repository; + } + + @Override + public Program create(ProgramRequest request) { + Program program = new Program(); + mapRequest(program, request); + return repository.save(program); + } + + @Override + public Program update(Long id, ProgramRequest request) { + Program program = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Program not found")); + + mapRequest(program, request); + return repository.save(program); + } + + @Override + public void delete(Long id) { + repository.deleteById(id); + } + + @Override + @Transactional(readOnly = true) + public List getAllAdmin() { + return repository.findAllByOrderByCreatedAtDesc(); + } + + @Override + @Transactional(readOnly = true) + public List getByCategory(String category) { + return repository + .findByCategoryAndActiveTrueOrderByCreatedAtDesc(category) + .stream() + .map(this::mapToResponse) + .toList(); + } + + private void mapRequest(Program program, ProgramRequest request) { + program.setTitle(request.getTitle()); + program.setDescription(request.getDescription()); + program.setCategory(request.getCategory()); + program.setImageUrl(request.getImageUrl()); + } + + private ProgramResponse mapToResponse(Program program) { + ProgramResponse res = new ProgramResponse(); + res.setId(program.getId()); + res.setTitle(program.getTitle()); + res.setDescription(program.getDescription()); + res.setImageUrl(program.getImageUrl()); + return res; + } +} From 3d2a2ae7b4a1685a9084d50655353c224177bfd3 Mon Sep 17 00:00:00 2001 From: rucha Date: Mon, 26 Jan 2026 23:47:15 +0530 Subject: [PATCH 09/23] added installment logic --- .../user/PaymentWebhookController.java | 2 + .../controller/user/UserController.java | 16 ++ .../dto/AdminSessionBookingResponse.java | 19 ++- .../dto/AdminSessionUserResponse.java | 8 +- .../dto/DonationCreateRequest.java | 6 +- .../InstallmentPaymentInitiateResponse.java | 46 +++++ .../divinespark/dto/UserBookingResponse.java | 13 +- .../java/com/divinespark/entity/Booking.java | 77 ++++++++- .../com/divinespark/entity/Installment.java | 111 ++++++++++++ .../entity/enums/BookingStatus.java | 8 + .../entity/enums/InstallmentStatus.java | 6 + .../divinespark/entity/enums/PaymentType.java | 7 + .../repository/BookingRepository.java | 97 +++++------ .../repository/InstallmentRepository.java | 41 +++++ .../service/BookingCleanupService.java | 10 +- .../divinespark/service/RazorpayService.java | 2 +- .../divinespark/service/SessionService.java | 6 + .../service/impl/BookingServiceImpl.java | 17 +- .../service/impl/PaymentServiceImpl.java | 132 +++++++++++---- .../service/impl/RazorpayServiceImpl.java | 8 +- .../service/impl/SessionServiceImpl.java | 158 ++++++++++++++---- 21 files changed, 633 insertions(+), 157 deletions(-) create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/InstallmentPaymentInitiateResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/entity/Installment.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/entity/enums/BookingStatus.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/entity/enums/InstallmentStatus.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/entity/enums/PaymentType.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/repository/InstallmentRepository.java diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentWebhookController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentWebhookController.java index c4efdeb..f7bc0b0 100644 --- a/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentWebhookController.java +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentWebhookController.java @@ -44,6 +44,8 @@ public ResponseEntity handleWebhook( @RequestBody String payload, @RequestHeader("X-Razorpay-Signature") String signature) { + System.out.println("๐Ÿ”ฅ๐Ÿ”ฅ RAZORPAY WEBHOOK HIT ๐Ÿ”ฅ๐Ÿ”ฅ"); + try { razorpaySignatureUtil.verify(payload, signature, webhookSecret); } catch (Exception e) { diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/UserController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/UserController.java index c62315d..588f33b 100644 --- a/divinespark-backend/src/main/java/com/divinespark/controller/user/UserController.java +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/UserController.java @@ -67,6 +67,22 @@ public ResponseEntity initiatePayment( ); } + @PreAuthorize("hasRole('USER')") + @PostMapping("/sessions/{sessionId}/pay/installments") + public ResponseEntity payInInstallments( + @PathVariable Long sessionId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + + InstallmentPaymentInitiateResponse response = + sessionService.initiateInstallmentPayment( + sessionId, + userDetails.getId() + ); + + return ResponseEntity.ok(response); + } + @GetMapping("/profile") public ResponseEntity getMyProfile( @AuthenticationPrincipal CustomUserDetails userDetails) { diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionBookingResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionBookingResponse.java index c95fbde..119a6fa 100644 --- a/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionBookingResponse.java +++ b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionBookingResponse.java @@ -1,6 +1,8 @@ package com.divinespark.dto; -import java.time.LocalDateTime; +import com.divinespark.entity.enums.BookingStatus; + +import java.time.OffsetDateTime; public class AdminSessionBookingResponse { @@ -9,8 +11,8 @@ public class AdminSessionBookingResponse { private String email; private String username; private String phoneNumber; - private String bookingStatus; - private LocalDateTime bookedAt; + private BookingStatus bookingStatus; + private OffsetDateTime bookedAt; public AdminSessionBookingResponse( Long bookingId, @@ -18,8 +20,8 @@ public AdminSessionBookingResponse( String username, String phoneNumber, String email, - String bookingStatus, - LocalDateTime bookedAt) { + BookingStatus bookingStatus, + OffsetDateTime bookedAt) { this.bookingId = bookingId; this.userId = userId; @@ -30,12 +32,15 @@ public AdminSessionBookingResponse( this.bookedAt = bookedAt; } + + + public Long getBookingId() { return bookingId; } public Long getUserId() { return userId; } public String getEmail() { return email; } public String getUsername() { return username; } - public String getBookingStatus() { return bookingStatus; } - public LocalDateTime getBookedAt() { return bookedAt; } + public BookingStatus getBookingStatus() { return bookingStatus; } + public OffsetDateTime getBookedAt() { return bookedAt; } public String getPhoneNumber() { return phoneNumber; } } diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserResponse.java index 5a8f9fe..3edf7d7 100644 --- a/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserResponse.java +++ b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserResponse.java @@ -1,14 +1,16 @@ package com.divinespark.dto; +import com.divinespark.entity.enums.BookingStatus; + public class AdminSessionUserResponse { private Long userId; private String email; private String username; private String contactNumber; - private String bookingStatus; + private BookingStatus bookingStatus; - public AdminSessionUserResponse(Long userId, String email, String bookingStatus, String username, String contactNumber) { + public AdminSessionUserResponse(Long userId, String email, BookingStatus bookingStatus, String username, String contactNumber) { this.userId = userId; this.email = email; this.bookingStatus = bookingStatus; @@ -24,7 +26,7 @@ public String getEmail() { return email; } - public String getBookingStatus() { + public BookingStatus getBookingStatus() { return bookingStatus; } diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/DonationCreateRequest.java b/divinespark-backend/src/main/java/com/divinespark/dto/DonationCreateRequest.java index e0f7238..b9a7c6d 100644 --- a/divinespark-backend/src/main/java/com/divinespark/dto/DonationCreateRequest.java +++ b/divinespark-backend/src/main/java/com/divinespark/dto/DonationCreateRequest.java @@ -1,11 +1,11 @@ package com.divinespark.dto; public class DonationCreateRequest { - private double amount; + private int amount; private String note; - public double getAmount() { return amount; } - public void setAmount(double amount) { this.amount = amount; } + public int getAmount() { return amount; } + public void setAmount(int amount) { this.amount = amount; } public String getNote() { return note; } public void setNote(String note) { this.note = note; } } diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentPaymentInitiateResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentPaymentInitiateResponse.java new file mode 100644 index 0000000..af2db92 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentPaymentInitiateResponse.java @@ -0,0 +1,46 @@ +package com.divinespark.dto; + +public class InstallmentPaymentInitiateResponse { + + private Long bookingId; + private Long installmentId; + private String razorpayOrderId; + private double amount; + private String currency = "INR"; + + public Long getBookingId() { + return bookingId; + } + + public void setBookingId(Long bookingId) { + this.bookingId = bookingId; + } + + public Long getInstallmentId() { + return installmentId; + } + + public void setInstallmentId(Long installmentId) { + this.installmentId = installmentId; + } + + public String getRazorpayOrderId() { + return razorpayOrderId; + } + + public void setRazorpayOrderId(String razorpayOrderId) { + this.razorpayOrderId = razorpayOrderId; + } + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } + + public String getCurrency() { + return currency; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/UserBookingResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/UserBookingResponse.java index b154034..4550a34 100644 --- a/divinespark-backend/src/main/java/com/divinespark/dto/UserBookingResponse.java +++ b/divinespark-backend/src/main/java/com/divinespark/dto/UserBookingResponse.java @@ -1,5 +1,7 @@ package com.divinespark.dto; +import com.divinespark.entity.enums.BookingStatus; + import java.time.LocalDateTime; public class UserBookingResponse { @@ -8,7 +10,7 @@ public class UserBookingResponse { private Long sessionId; private String sessionTitle; private String sessionType; - private String bookingStatus; + private BookingStatus status; private LocalDateTime startTime; private LocalDateTime endTime; private String whatsLink; // nullable @@ -41,11 +43,12 @@ public void setSessionType(String sessionType) { this.sessionType = sessionType; } - public String getBookingStatus() { - return bookingStatus; + public BookingStatus getStatus() { + return status; } - public void setBookingStatus(String bookingStatus) { - this.bookingStatus = bookingStatus; + + public void setStatus(BookingStatus status) { + this.status = status; } public LocalDateTime getStartTime() { diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/Booking.java b/divinespark-backend/src/main/java/com/divinespark/entity/Booking.java index dd37218..1182cde 100644 --- a/divinespark-backend/src/main/java/com/divinespark/entity/Booking.java +++ b/divinespark-backend/src/main/java/com/divinespark/entity/Booking.java @@ -1,7 +1,10 @@ package com.divinespark.entity; +import com.divinespark.entity.enums.BookingStatus; +import com.divinespark.entity.enums.PaymentType; import jakarta.persistence.*; -import java.time.LocalDateTime; + +import java.time.OffsetDateTime; @Entity @Table(name = "bookings", @@ -25,15 +28,32 @@ public class Booking { private String whatsappLink; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PaymentType paymentType; + + @Column(nullable = false) + private double totalAmount; + + @Column(nullable = false) + private double paidAmount; + + @Column(nullable = false) + private double remainingAmount; + + @Enumerated(EnumType.STRING) @Column(nullable = false) - private String status; // CONFIRMED + private BookingStatus bookingStatus; + @Column(nullable = false, updatable = false) - private LocalDateTime createdAt; + private OffsetDateTime createdAt = OffsetDateTime.now(); + + @PrePersist void onCreate() { - createdAt = LocalDateTime.now(); + createdAt = OffsetDateTime.now(); } public Long getId() { return id; } @@ -41,9 +61,54 @@ void onCreate() { public void setUser(User user) { this.user = user; } public Session getSession() { return session; } public void setSession(Session session) { this.session = session; } - public String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } + public BookingStatus getBookingStatus() { + return bookingStatus; + } + + public void setBookingStatus(BookingStatus bookingStatus) { + this.bookingStatus = bookingStatus; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public double getPaidAmount() { + return paidAmount; + } + + public void setPaidAmount(double paidAmount) { + this.paidAmount = paidAmount; + } + + public PaymentType getPaymentType() { + return paymentType; + } + + public void setPaymentType(PaymentType paymentType) { + this.paymentType = paymentType; + } + + public double getRemainingAmount() { + return remainingAmount; + } + + public void setRemainingAmount(double remainingAmount) { + this.remainingAmount = remainingAmount; + } + + public double getTotalAmount() { + return totalAmount; + } + + public void setTotalAmount(double totalAmount) { + this.totalAmount = totalAmount; + } public String getWhatsappLink() { return whatsappLink; diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/Installment.java b/divinespark-backend/src/main/java/com/divinespark/entity/Installment.java new file mode 100644 index 0000000..b5c0760 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/entity/Installment.java @@ -0,0 +1,111 @@ +package com.divinespark.entity; + +import com.divinespark.entity.enums.InstallmentStatus; +import jakarta.persistence.*; + +import java.time.OffsetDateTime; + +@Entity +@Table( + name = "installments", + indexes = { + @Index(name = "idx_installment_booking", columnList = "booking_id"), + @Index(name = "idx_installment_status", columnList = "status") + } +) +public class Installment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booking_id", nullable = false) + private Booking booking; + + @Column(nullable = false) + private int installmentNumber; // 1,2,3... + + @Column(nullable = false) + private double amount; + + @Column(nullable = false) + private OffsetDateTime dueDate; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private InstallmentStatus status = InstallmentStatus.PENDING; + + private OffsetDateTime paidAt; + + private String razorpayOrderId; + + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } + + public Booking getBooking() { + return booking; + } + + public void setBooking(Booking booking) { + this.booking = booking; + } + + public OffsetDateTime getDueDate() { + return dueDate; + } + + public void setDueDate(OffsetDateTime dueDate) { + this.dueDate = dueDate; + } + + public int getInstallmentNumber() { + return installmentNumber; + } + + public void setInstallmentNumber(int installmentNumber) { + this.installmentNumber = installmentNumber; + } + + public OffsetDateTime getPaidAt() { + return paidAt; + } + + public void setPaidAt(OffsetDateTime paidAt) { + this.paidAt = paidAt; + } + + public String getRazorpayOrderId() { + return razorpayOrderId; + } + + public void setRazorpayOrderId(String razorpayOrderId) { + this.razorpayOrderId = razorpayOrderId; + } + + public InstallmentStatus getStatus() { + return status; + } + + public void setStatus(InstallmentStatus status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public void setAmount(int amount) { + this.amount = amount; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/enums/BookingStatus.java b/divinespark-backend/src/main/java/com/divinespark/entity/enums/BookingStatus.java new file mode 100644 index 0000000..cb70bbd --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/entity/enums/BookingStatus.java @@ -0,0 +1,8 @@ +package com.divinespark.entity.enums; + +public enum BookingStatus { + PENDING, + PARTIALLY_PAID, + CONFIRMED, + CANCELLED +} diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/enums/InstallmentStatus.java b/divinespark-backend/src/main/java/com/divinespark/entity/enums/InstallmentStatus.java new file mode 100644 index 0000000..fb5d6a5 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/entity/enums/InstallmentStatus.java @@ -0,0 +1,6 @@ +package com.divinespark.entity.enums; + +public enum InstallmentStatus { + PENDING, + PAID +} diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/enums/PaymentType.java b/divinespark-backend/src/main/java/com/divinespark/entity/enums/PaymentType.java new file mode 100644 index 0000000..0f41302 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/entity/enums/PaymentType.java @@ -0,0 +1,7 @@ +package com.divinespark.entity.enums; + +public enum PaymentType { + FULL, + INSTALLMENT, + FREE +} diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java index feddd56..bd8470d 100644 --- a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java +++ b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java @@ -3,23 +3,19 @@ import com.divinespark.dto.AdminSessionBookingResponse; import com.divinespark.dto.AdminSessionUserResponse; import com.divinespark.entity.Booking; +import com.divinespark.entity.enums.BookingStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; public interface BookingRepository extends JpaRepository { - boolean existsByUserIdAndSessionIdAndStatus( - Long userId, - Long sessionId, - String status - ); - - + // ---------- USER BOOKINGS ---------- @Query(""" SELECT b FROM Booking b JOIN FETCH b.session @@ -28,64 +24,61 @@ boolean existsByUserIdAndSessionIdAndStatus( """) List findUserBookingsWithSession(Long userId); - Optional findById(Long id); + Optional findByUser_IdAndSession_Id( + Long userId, + Long sessionId + ); + + boolean existsByUser_IdAndSession_Id( + Long userId, + Long sessionId + ); + boolean existsByUser_IdAndSession_IdAndBookingStatus( + Long userId, + Long sessionId, + BookingStatus bookingStatus + ); + // ---------- ADMIN ---------- @Query(""" - SELECT new com.divinespark.dto.AdminSessionUserResponse( - u.id, - u.email, - b.status, - u.username, - u.contactNumber - ) - FROM Booking b - JOIN b.user u - WHERE b.session.id = :sessionId -""") + SELECT new com.divinespark.dto.AdminSessionUserResponse( + u.id, + u.email, + b.bookingStatus, + u.username, + u.contactNumber + ) + FROM Booking b + JOIN b.user u + WHERE b.session.id = :sessionId + """) List findUsersBySessionId( @Param("sessionId") Long sessionId ); - @Query(""" - SELECT new com.divinespark.dto.AdminSessionBookingResponse( - b.id, - u.id, - u.username, - u.contactNumber, - u.email, - b.status, - b.createdAt - ) - FROM Booking b - JOIN b.user u - WHERE b.session.id = :sessionId -""") + SELECT new com.divinespark.dto.AdminSessionBookingResponse( + b.id, + u.id, + u.username, + u.contactNumber, + u.email, + b.bookingStatus, + b.createdAt + ) + FROM Booking b + JOIN b.user u + WHERE b.session.id = :sessionId + """) List findBookingsBySessionId( @Param("sessionId") Long sessionId ); - Optional findByUserIdAndSessionIdAndStatus( - Long userId, - Long sessionId, - String status + List findByBookingStatusAndCreatedAtBefore( + BookingStatus status, + OffsetDateTime cutoffTime ); - List findByStatusAndCreatedAtBefore( - String status, - LocalDateTime cutoffTime - ); - - boolean existsByUserIdAndSessionId(Long userId, Long sessionId); - - boolean existsByUserIdAndSessionIdAndStatusNot( - Long userId, - Long sessionId, - String status - ); - - - } diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/InstallmentRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/InstallmentRepository.java new file mode 100644 index 0000000..694b352 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/repository/InstallmentRepository.java @@ -0,0 +1,41 @@ +package com.divinespark.repository; + +import java.util.List; +import java.util.Optional; + +import com.divinespark.entity.Installment; +import com.divinespark.entity.enums.InstallmentStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface InstallmentRepository extends JpaRepository { + + List findByBookingIdOrderByInstallmentNumber(Long bookingId); + + Optional findByBookingIdAndInstallmentNumber(Long bookingId, InstallmentStatus status); + + boolean existsByBookingIdAndStatus(Long bookingId, InstallmentStatus status); + + Optional findByRazorpayOrderId(String razorpayOrderId); + + + List findByBooking_IdAndStatus( + Long bookingId, + InstallmentStatus status + ); + + @Modifying + @Query(""" + update Installment i + set i.razorpayOrderId = :orderId + where i.id = :id +""") + void updateOrderId( + @Param("id") Long id, + @Param("orderId") String orderId + ); + + +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/BookingCleanupService.java b/divinespark-backend/src/main/java/com/divinespark/service/BookingCleanupService.java index 263adc4..af7c0fe 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/BookingCleanupService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/BookingCleanupService.java @@ -2,6 +2,7 @@ import com.divinespark.entity.Booking; +import com.divinespark.entity.enums.BookingStatus; import com.divinespark.repository.BookingRepository; import com.divinespark.repository.PaymentRepository; import org.springframework.scheduling.annotation.Scheduled; @@ -9,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; @Service @@ -26,16 +28,16 @@ public BookingCleanupService(BookingRepository bookingRepository, PaymentReposit @Scheduled(fixedRate = 5 * 60 * 1000) //5 min public void cleanupStalePendingBookings() { - LocalDateTime cutoff = LocalDateTime.now().minusMinutes(15); + OffsetDateTime cutoff = OffsetDateTime.now().minusMinutes(15); - List staleBookings = bookingRepository.findByStatusAndCreatedAtBefore( - "PENDING" , + List staleBookings = bookingRepository.findByBookingStatusAndCreatedAtBefore( + BookingStatus.PENDING, cutoff ); for(Booking booking : staleBookings) { - booking.setStatus("PENDING"); + booking.setBookingStatus(BookingStatus.PENDING); paymentRepository .findTopByBookingIdOrderByCreatedAtDesc(booking.getId()) diff --git a/divinespark-backend/src/main/java/com/divinespark/service/RazorpayService.java b/divinespark-backend/src/main/java/com/divinespark/service/RazorpayService.java index b3c956d..44870e3 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/RazorpayService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/RazorpayService.java @@ -3,5 +3,5 @@ import com.divinespark.dto.RazorpayOrderResponse; public interface RazorpayService { - RazorpayOrderResponse createOrder(double amount, Long bookingId); + RazorpayOrderResponse createOrder(int amount, Long bookingId); } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/SessionService.java b/divinespark-backend/src/main/java/com/divinespark/service/SessionService.java index f45eaca..9d17c34 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/SessionService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/SessionService.java @@ -42,4 +42,10 @@ public interface SessionService { List getBookingsBySession(Long sessionId); public String getWhatsappLinkIfConfirmed(Long sessionId, Long userId); + + public InstallmentPaymentInitiateResponse initiateInstallmentPayment( + Long sessionId, + Long userId + ); } + diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java index ad33785..0d3b751 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java @@ -4,6 +4,8 @@ import com.divinespark.dto.UserBookingResponse; import com.divinespark.entity.Booking; import com.divinespark.entity.Session; +import com.divinespark.entity.enums.BookingStatus; +import com.divinespark.entity.enums.SessionStatus; import com.divinespark.repository.BookingRepository; import com.divinespark.service.BookingService; import com.divinespark.service.EmailService; @@ -45,15 +47,16 @@ public List getUserBookings(Long userId) { dto.setSessionId(session.getId()); dto.setSessionTitle(session.getTitle()); dto.setSessionType(session.getType().name()); - dto.setBookingStatus(booking.getStatus()); + dto.setStatus(booking.getBookingStatus()); dto.setStartTime(session.getStartTime().toLocalDateTime()); dto.setEndTime(session.getEndTime().toLocalDateTime()); - if ( - "CONFIRMED".equals(booking.getStatus()) && - "UPCOMING".equals(session.getStatus().name()) - ) { + (booking.getBookingStatus() == BookingStatus.CONFIRMED || + booking.getBookingStatus() == BookingStatus.PARTIALLY_PAID) && + session.getStatus() == SessionStatus.UPCOMING + ) + { dto.setJoinLink(session.getWhatsLink()); } @@ -75,11 +78,11 @@ public void cancelBooking(Long bookingId, Long userId) { throw new RuntimeException("Unauthorized cancellation"); } - if ("CANCELLED".equals(booking.getStatus())) { + if ("CANCELLED".equals(booking.getBookingStatus())) { throw new RuntimeException("Booking already cancelled"); } - booking.setStatus("CANCELLED"); + booking.setBookingStatus(BookingStatus.CANCELLED); Session session = booking.getSession(); session.getAvailableSeats().incrementAndGet(); diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java index 7f27f5e..f7e8c2f 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java @@ -1,17 +1,19 @@ package com.divinespark.service.impl; import com.divinespark.dto.AdminPaymentResponse; -import com.divinespark.entity.Booking; -import com.divinespark.entity.Payment; -import com.divinespark.entity.Session; -import com.divinespark.entity.User; +import com.divinespark.entity.*; +import com.divinespark.entity.enums.BookingStatus; +import com.divinespark.entity.enums.InstallmentStatus; +import com.divinespark.entity.enums.PaymentType; import com.divinespark.repository.BookingRepository; +import com.divinespark.repository.InstallmentRepository; import com.divinespark.repository.PaymentRepository; import com.divinespark.service.EmailService; import com.divinespark.service.PaymentService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.OffsetDateTime; import java.util.List; @Service @@ -20,26 +22,30 @@ public class PaymentServiceImpl implements PaymentService { private final PaymentRepository paymentRepository; private final BookingRepository bookingRepo; private final EmailService emailService; + private final InstallmentRepository installmentRepository; public PaymentServiceImpl( PaymentRepository paymentRepository, BookingRepository bookingRepo, - EmailService emailService) { + EmailService emailService, + InstallmentRepository installmentRepository) { this.paymentRepository = paymentRepository; this.bookingRepo = bookingRepo; this.emailService = emailService; + this.installmentRepository = installmentRepository; } + // ================= FAILURE HANDLING ================= + @Transactional public void handlePaymentFailure(String gatewayOrderId) { - Payment payment = paymentRepository - .findByGatewayOrderId(gatewayOrderId); + Payment payment = + paymentRepository.findByGatewayOrderId(gatewayOrderId); - if ("FAILED".equals(payment.getStatus())) { - return; - } + if (payment == null) return; + if ("FAILED".equals(payment.getStatus())) return; payment.setStatus("FAILED"); paymentRepository.save(payment); @@ -48,65 +54,117 @@ public void handlePaymentFailure(String gatewayOrderId) { .findById(payment.getBookingId()) .orElseThrow(() -> new RuntimeException("Booking not found")); - booking.setStatus("FAILED"); + booking.setBookingStatus(BookingStatus.CANCELLED); bookingRepo.save(booking); } + // ================= SUCCESS HANDLING (FULL PAYMENT) ================= + @Transactional public void handlePaymentSuccess(String gatewayOrderId) { - Payment payment = paymentRepository - .findByGatewayOrderId(gatewayOrderId); + Payment payment = + paymentRepository.findByGatewayOrderId(gatewayOrderId); - if ("SUCCESS".equals(payment.getStatus())) { - return; - } + if (payment == null) return; + if ("SUCCESS".equals(payment.getStatus())) return; payment.setStatus("SUCCESS"); paymentRepository.save(payment); Booking booking = bookingRepo .findById(payment.getBookingId()) - .orElseThrow(); + .orElseThrow(() -> new RuntimeException("Booking not found")); - booking.setStatus("CONFIRMED"); - bookingRepo.save(booking); + if (booking.getPaymentType() == PaymentType.FULL) { + booking.setBookingStatus(BookingStatus.CONFIRMED); + bookingRepo.save(booking); + } } + // ================= ADMIN ================= + @Override public List getAllPaymentsForAdmin() { return paymentRepository.fetchAdminPayments(); } + // ================= WEBHOOK (FULL + INSTALLMENT) ================= + @Override @Transactional public boolean handlePaymentCaptured(String razorpayOrderId, int amount) { - Payment payment = paymentRepository - .findByGatewayOrderId(razorpayOrderId); + // FULL PAYMENT + Payment payment = paymentRepository.findByGatewayOrderId(razorpayOrderId); - if (payment == null) { - throw new RuntimeException("Payment not found"); - } + if (payment != null) { - if ("SUCCESS".equals(payment.getStatus())) { - return false; - } + if ("SUCCESS".equals(payment.getStatus())) return true; - payment.setStatus("SUCCESS"); - paymentRepository.save(payment); + payment.setStatus("SUCCESS"); + paymentRepository.save(payment); - Booking booking = bookingRepo - .findById(payment.getBookingId()) - .orElseThrow(() -> new RuntimeException("Booking not found")); + Booking booking = bookingRepo.findById(payment.getBookingId()) + .orElseThrow(); - booking.setStatus("CONFIRMED"); + booking.setBookingStatus(BookingStatus.CONFIRMED); + bookingRepo.save(booking); - // Zoom registration for access control (no join_url dependency) - Session session = booking.getSession(); - User user = booking.getUser(); + return true; + } + + // 1๏ธINSTALLMENT FIRST + Installment installment = + installmentRepository.findByRazorpayOrderId(razorpayOrderId) + .orElse(null); + + if (installment != null) { + + if (installment.getStatus() == InstallmentStatus.PAID) { + return true; + } + + installment.setStatus(InstallmentStatus.PAID); + installment.setPaidAt(OffsetDateTime.now()); + installmentRepository.save(installment); + + Booking booking = installment.getBooking(); + + double paidAmount = installmentRepository + .findByBooking_IdAndStatus( + booking.getId(), + InstallmentStatus.PAID + ) + .stream() + .mapToDouble(Installment::getAmount) + .sum(); + + booking.setPaidAmount(paidAmount); + booking.setRemainingAmount( + Math.max(booking.getTotalAmount() - paidAmount, 0) + ); + + if (booking.getRemainingAmount() <= 0) { + booking.setBookingStatus(BookingStatus.CONFIRMED); + } else { + booking.setBookingStatus(BookingStatus.PARTIALLY_PAID); + } + + Payment p = new Payment(); + p.setBookingId(booking.getId()); + p.setAmount(installment.getAmount()); + p.setGatewayOrderId(razorpayOrderId); + p.setStatus("SUCCESS"); + paymentRepository.save(p); + + + bookingRepo.save(booking); + return true; + } - bookingRepo.save(booking); return false; + } + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/RazorpayServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/RazorpayServiceImpl.java index 8a384d4..4b2c1a1 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/RazorpayServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/RazorpayServiceImpl.java @@ -18,7 +18,7 @@ public RazorpayServiceImpl(RazorpayConfig razorpayConfig) { } @Override - public RazorpayOrderResponse createOrder(double amount, Long bookingId) { + public RazorpayOrderResponse createOrder(int amountInPaise, Long referenceId) { try { RazorpayClient client = new RazorpayClient( @@ -27,16 +27,15 @@ public RazorpayOrderResponse createOrder(double amount, Long bookingId) { ); JSONObject orderRequest = new JSONObject(); - orderRequest.put("amount", (int) (amount * 100)); // paise + orderRequest.put("amount", amountInPaise); // already paise orderRequest.put("currency", razorpayConfig.getCurrency()); - orderRequest.put("receipt", "booking_" + bookingId); + orderRequest.put("receipt", "ref_" + referenceId); orderRequest.put("payment_capture", 1); Order order = client.orders.create(orderRequest); RazorpayOrderResponse response = new RazorpayOrderResponse(); response.setOrderId(order.get("id")); - System.out.println("RAZORPAY ORDER ID => " + order.get("id")); return response; @@ -44,4 +43,5 @@ public RazorpayOrderResponse createOrder(double amount, Long bookingId) { throw new RuntimeException("Failed to create Razorpay order", e); } } + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java index 44b1c8f..6e86b32 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java @@ -2,6 +2,8 @@ import com.divinespark.dto.*; import com.divinespark.entity.*; +import com.divinespark.entity.enums.BookingStatus; +import com.divinespark.entity.enums.PaymentType; import com.divinespark.entity.enums.SessionStatus; import com.divinespark.entity.enums.SessionType; import com.divinespark.exception.BusinessException; @@ -15,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -29,6 +32,7 @@ public class SessionServiceImpl implements SessionService { private final PaymentRepository paymentRepository; private final SessionRepository sessionRepository; private final RazorpayService razorpayService; + private final InstallmentRepository installmentRepository; public SessionServiceImpl( SessionRepository repo, @@ -36,7 +40,8 @@ public SessionServiceImpl( UserRepository userRepo, PaymentRepository paymentRepository, SessionRepository sessionRepository, - RazorpayService razorpayService) { + RazorpayService razorpayService, + InstallmentRepository installmentRepository) { this.repo = repo; this.bookingRepository = bookingRepository; @@ -44,6 +49,7 @@ public SessionServiceImpl( this.paymentRepository = paymentRepository; this.sessionRepository = sessionRepository; this.razorpayService = razorpayService; + this.installmentRepository = installmentRepository; } // ================= ADMIN ================= @@ -165,12 +171,14 @@ public void joinFreeSession(Long sessionId, Long userId) { Booking booking = new Booking(); booking.setSession(session); booking.setUser(user); - booking.setStatus("CONFIRMED"); - bookingRepository.save(booking); + booking.setPaymentType(PaymentType.FREE); + booking.setBookingStatus(BookingStatus.CONFIRMED); + bookingRepository.save(booking); session.getAvailableSeats().decrementAndGet(); } + @Override public PaymentInitiateResponse initiatePaidSession(Long sessionId, Long userId) { @@ -187,34 +195,135 @@ public PaymentInitiateResponse initiatePaidSession(Long sessionId, Long userId) .orElseThrow(() -> new RuntimeException("User not found")); Booking booking = bookingRepository - .findByUserIdAndSessionIdAndStatus(userId, sessionId, "PENDING") + .findByUser_IdAndSession_Id(userId, sessionId) .orElseGet(() -> { Booking b = new Booking(); b.setSession(session); b.setUser(user); - b.setStatus("PENDING"); - return bookingRepository.save(b); + b.setPaymentType(PaymentType.FULL); + b.setBookingStatus(BookingStatus.PENDING); + bookingRepository.save(b); + session.getAvailableSeats().decrementAndGet(); + return b; }); + // BLOCK DUPLICATE PAYMENTS + if (booking.getBookingStatus() == BookingStatus.CONFIRMED) { + throw new BusinessException("Session already paid"); + } + RazorpayOrderResponse order = - razorpayService.createOrder(session.getPrice(), booking.getId()); + razorpayService.createOrder( + (int) Math.round(session.getPrice() * 100), + booking.getId() + ); Payment payment = new Payment(); payment.setBookingId(booking.getId()); payment.setAmount(session.getPrice()); payment.setGatewayOrderId(order.getOrderId()); payment.setStatus("CREATED"); + paymentRepository.save(payment); PaymentInitiateResponse res = new PaymentInitiateResponse(); res.setBookingId(booking.getId()); res.setOrderId(order.getOrderId()); - res.setAmount(session.getPrice() * 100); + res.setAmount((int) Math.round(session.getPrice())); res.setCurrency("INR"); return res; } + + + @Override + @Transactional + public InstallmentPaymentInitiateResponse initiateInstallmentPayment( + Long sessionId, + Long userId + ) { + + Session session = sessionRepository.findById(sessionId) + .orElseThrow(() -> new RuntimeException("Session not found")); + + User user = userRepo.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + // Create booking + Booking booking = new Booking(); + booking.setUser(user); + booking.setSession(session); + booking.setPaymentType(PaymentType.INSTALLMENT); + booking.setTotalAmount(session.getPrice()); + booking.setPaidAmount(0.0); + booking.setRemainingAmount(session.getPrice()); + booking.setBookingStatus(BookingStatus.PENDING); + booking.setWhatsappLink(session.getWhatsLink()); + + + + bookingRepository.save(booking); + session.getAvailableSeats().decrementAndGet(); + + // Create installments (3 parts) + double totalAmount = session.getPrice(); + double installmentAmount = totalAmount / 3.0; + + List installments = new ArrayList<>(); + + for (int i = 1; i <= 3; i++) { + Installment inst = new Installment(); + inst.setBooking(booking); + inst.setInstallmentNumber(i); + inst.setAmount(installmentAmount); + inst.setPaidAt(OffsetDateTime.now()); + inst.setDueDate(OffsetDateTime.now().plusMonths(i - 1)); + installments.add(inst); + } + + installmentRepository.saveAll(installments); + + // Pick first installment + Installment firstInstallment = installments.get(0); + + // Create Razorpay order + RazorpayOrderResponse order = + razorpayService.createOrder( + (int) Math.round(session.getPrice() * 100), + booking.getId() + ); + + + firstInstallment.setRazorpayOrderId(order.getOrderId()); + installmentRepository.save(firstInstallment); + + installmentRepository.updateOrderId( + firstInstallment.getId(), + order.getOrderId() + ); + + // Response + InstallmentPaymentInitiateResponse res = + new InstallmentPaymentInitiateResponse(); + + res.setBookingId(booking.getId()); + res.setInstallmentId(firstInstallment.getId()); + res.setRazorpayOrderId(order.getOrderId()); + res.setAmount(firstInstallment.getAmount() * 100); + + Payment payment = new Payment(); + payment.setBookingId(booking.getId()); + payment.setAmount(firstInstallment.getAmount()); + payment.setGatewayOrderId(order.getOrderId()); + payment.setStatus("CREATED"); + paymentRepository.save(payment); + + + return res; + } + + @Override public List getUsersBySession(Long sessionId) { return bookingRepository.findUsersBySessionId(sessionId); @@ -309,30 +418,23 @@ public SessionDetailResponse getSessionDetails(Long sessionId) { @Override public String getWhatsappLinkIfConfirmed(Long sessionId, Long userId) { - // 1. Fetch CONFIRMED booking for user + session Booking booking = bookingRepository - .findByUserIdAndSessionIdAndStatus( - userId, - sessionId, - "CONFIRMED" - ) - .orElseThrow(() -> new ExpressionException( - "You are not allowed to access this session" - )); - - // 2. Fetch session from booking (already mapped) - String whatsappLink = booking - .getSession() - .getWhatsLink(); - - // 3. Safety check (admin forgot to add link) + .findByUser_IdAndSession_Id(userId, sessionId) + .orElseThrow(() -> new ExpressionException("Access denied")); + + if ( + booking.getBookingStatus() != BookingStatus.CONFIRMED && + booking.getBookingStatus() != BookingStatus.PARTIALLY_PAID + ) { + throw new ExpressionException("Access denied"); + } + + String whatsappLink = booking.getSession().getWhatsLink(); + if (whatsappLink == null || whatsappLink.isBlank()) { - throw new IllegalStateException( - "WhatsApp group link not configured for this session" - ); + throw new IllegalStateException("WhatsApp link not configured"); } - // 4. Return link return whatsappLink; } From 83b85b8eefa633eae2b6b7558b77bddef70a0221 Mon Sep 17 00:00:00 2001 From: rucha Date: Tue, 27 Jan 2026 15:14:49 +0530 Subject: [PATCH 10/23] Installment implementation --- .../controller/user/BookingController.java | 16 +++ .../controller/user/UserController.java | 7 +- .../user/UserInstallmentController.java | 54 ++++++++ .../dto/InstallmentPayResponse.java | 24 ++++ .../divinespark/dto/InstallmentResponse.java | 34 +++++ .../dto/UserSessionBookingResponse.java | 37 +++++ .../divinespark/security/SecurityConfig.java | 2 +- .../divinespark/service/BookingService.java | 4 + .../service/InstallementService.java | 15 ++ .../service/impl/BookingServiceImpl.java | 22 +++ .../service/impl/InstallmentServiceImpl.java | 130 ++++++++++++++++++ .../service/impl/SessionServiceImpl.java | 41 ++---- 12 files changed, 356 insertions(+), 30 deletions(-) create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/user/UserInstallmentController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/InstallmentPayResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/InstallmentResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/UserSessionBookingResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/BookingController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/BookingController.java index 75b6490..c5793c3 100644 --- a/divinespark-backend/src/main/java/com/divinespark/controller/user/BookingController.java +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/BookingController.java @@ -2,6 +2,7 @@ import com.divinespark.dto.ReviewCreateRequest; import com.divinespark.dto.UserBookingResponse; +import com.divinespark.dto.UserSessionBookingResponse; import com.divinespark.security.CustomUserDetails; import com.divinespark.service.BookingService; import com.divinespark.service.ReviewService; @@ -49,5 +50,20 @@ public ResponseEntity cancelBooking( return ResponseEntity.noContent().build(); } + @PreAuthorize("hasRole('USER')") + @GetMapping("/session/{sessionId}/me") + public ResponseEntity getMyBookingForSession( + @PathVariable Long sessionId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + return ResponseEntity.ok( + bookingService.getMyBookingForSession( + userDetails.getId(), + sessionId + ) + ); + } + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/UserController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/UserController.java index 588f33b..7df30ef 100644 --- a/divinespark-backend/src/main/java/com/divinespark/controller/user/UserController.java +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/UserController.java @@ -2,6 +2,7 @@ import com.divinespark.dto.*; import com.divinespark.security.CustomUserDetails; +import com.divinespark.service.InstallementService; import com.divinespark.service.SessionService; import com.divinespark.service.UserService; import org.springframework.http.ResponseEntity; @@ -17,10 +18,12 @@ public class UserController { private final SessionService sessionService; private final UserService userService; + private final InstallementService installementService; - public UserController(SessionService sessionService, UserService userService) { + public UserController(SessionService sessionService, UserService userService, InstallementService installementService) { this.sessionService = sessionService; this.userService = userService; + this.installementService = installementService; } @GetMapping("/sessions") @@ -83,6 +86,8 @@ public ResponseEntity payInInstallments( return ResponseEntity.ok(response); } + + @GetMapping("/profile") public ResponseEntity getMyProfile( @AuthenticationPrincipal CustomUserDetails userDetails) { diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/UserInstallmentController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/UserInstallmentController.java new file mode 100644 index 0000000..6ffd7fb --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/UserInstallmentController.java @@ -0,0 +1,54 @@ +package com.divinespark.controller.user; + +import com.divinespark.dto.InstallmentPayResponse; +import com.divinespark.dto.InstallmentResponse; +import com.divinespark.security.CustomUserDetails; + +import com.divinespark.service.InstallementService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/installments") +public class UserInstallmentController { + + private final InstallementService installmentService; + + public UserInstallmentController(InstallementService installmentService) { + this.installmentService = installmentService; + } + + @PreAuthorize("hasRole('USER')") + @GetMapping("/booking/{bookingId}") + public ResponseEntity> getInstallmentsByBooking( + @PathVariable Long bookingId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + + return ResponseEntity.ok( + installmentService.getInstallmentsByBooking( + bookingId, + userDetails.getId() + ) + ); + } + + @PreAuthorize("hasRole('USER')") + @PostMapping("/{installmentId}/pay") + public ResponseEntity payInstallment( + @PathVariable Long installmentId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + return ResponseEntity.ok( + installmentService.payInstallment( + installmentId, + userDetails.getId() + ) + ); + } + +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentPayResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentPayResponse.java new file mode 100644 index 0000000..9041b4f --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentPayResponse.java @@ -0,0 +1,24 @@ +package com.divinespark.dto; + +public class InstallmentPayResponse { + + private Long bookingId; + private Long installmentId; + private String razorpayOrderId; + private int amount; // in paise + private String currency = "INR"; + + public Long getBookingId() { return bookingId; } + public void setBookingId(Long bookingId) { this.bookingId = bookingId; } + + public Long getInstallmentId() { return installmentId; } + public void setInstallmentId(Long installmentId) { this.installmentId = installmentId; } + + public String getRazorpayOrderId() { return razorpayOrderId; } + public void setRazorpayOrderId(String razorpayOrderId) { this.razorpayOrderId = razorpayOrderId; } + + public int getAmount() { return amount; } + public void setAmount(int amount) { this.amount = amount; } + + public String getCurrency() { return currency; } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentResponse.java new file mode 100644 index 0000000..f167c72 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/InstallmentResponse.java @@ -0,0 +1,34 @@ +package com.divinespark.dto; + +import com.divinespark.entity.enums.InstallmentStatus; + +import java.time.OffsetDateTime; + +public class InstallmentResponse { + + private Long id; + private int installmentNumber; + private double amount; + private InstallmentStatus status; + private OffsetDateTime paidAt; + + public InstallmentResponse( + Long id, + int installmentNumber, + double amount, + InstallmentStatus status, + OffsetDateTime paidAt + ) { + this.id = id; + this.installmentNumber = installmentNumber; + this.amount = amount; + this.status = status; + this.paidAt = paidAt; + } + + public Long getId() { return id; } + public int getInstallmentNumber() { return installmentNumber; } + public double getAmount() { return amount; } + public InstallmentStatus getStatus() { return status; } + public OffsetDateTime getPaidAt() { return paidAt; } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/UserSessionBookingResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/UserSessionBookingResponse.java new file mode 100644 index 0000000..c9330f4 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/UserSessionBookingResponse.java @@ -0,0 +1,37 @@ +package com.divinespark.dto; + +import com.divinespark.entity.enums.BookingStatus; +import com.divinespark.entity.enums.PaymentType; + +public class UserSessionBookingResponse { + + private Long bookingId; + private BookingStatus bookingStatus; + private PaymentType paymentType; + private double totalAmount; + private double paidAmount; + private double remainingAmount; + + public UserSessionBookingResponse( + Long bookingId, + BookingStatus bookingStatus, + PaymentType paymentType, + double totalAmount, + double paidAmount, + double remainingAmount + ) { + this.bookingId = bookingId; + this.bookingStatus = bookingStatus; + this.paymentType = paymentType; + this.totalAmount = totalAmount; + this.paidAmount = paidAmount; + this.remainingAmount = remainingAmount; + } + + public Long getBookingId() { return bookingId; } + public BookingStatus getBookingStatus() { return bookingStatus; } + public PaymentType getPaymentType() { return paymentType; } + public double getTotalAmount() { return totalAmount; } + public double getPaidAmount() { return paidAmount; } + public double getRemainingAmount() { return remainingAmount; } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index fcf1a63..1523df7 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -54,7 +54,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ).permitAll() // User APIs - .requestMatchers("/api/v1/user/**").hasRole("USER") + .requestMatchers("/api/v1/user/**", "/api/v1/installments/**").hasRole("USER") // Public session browsing .requestMatchers(HttpMethod.GET, "/api/v1/sessions/**").permitAll() diff --git a/divinespark-backend/src/main/java/com/divinespark/service/BookingService.java b/divinespark-backend/src/main/java/com/divinespark/service/BookingService.java index 1f3868d..b16625f 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/BookingService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/BookingService.java @@ -1,6 +1,7 @@ package com.divinespark.service; import com.divinespark.dto.UserBookingResponse; +import com.divinespark.dto.UserSessionBookingResponse; import jakarta.servlet.http.HttpServletResponse; import java.util.List; @@ -11,4 +12,7 @@ public interface BookingService { void cancelBooking(Long bookingId, Long userId); long getTotalBookings(); public void downloadSessionUsers(Long sessionId, HttpServletResponse response); + + UserSessionBookingResponse getMyBookingForSession(Long userId, Long sessionId); + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java b/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java new file mode 100644 index 0000000..20e4ccf --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java @@ -0,0 +1,15 @@ +package com.divinespark.service; + +import com.divinespark.dto.InstallmentPayResponse; +import com.divinespark.dto.InstallmentResponse; + +import java.util.List; + +public interface InstallementService { + + InstallmentPayResponse payInstallment(Long installmentId, Long userId); + + List getInstallmentsByBooking(Long bookingId, Long userId); + + +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java index 0d3b751..2084333 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java @@ -2,6 +2,7 @@ import com.divinespark.dto.AdminSessionUserResponse; import com.divinespark.dto.UserBookingResponse; +import com.divinespark.dto.UserSessionBookingResponse; import com.divinespark.entity.Booking; import com.divinespark.entity.Session; import com.divinespark.entity.enums.BookingStatus; @@ -131,4 +132,25 @@ public void downloadSessionUsers(Long sessionId, HttpServletResponse response) { } } + @Override + public UserSessionBookingResponse getMyBookingForSession( + Long userId, + Long sessionId + ) { + + Booking booking = bookingRepository + .findByUser_IdAndSession_Id(userId, sessionId) + .orElseThrow(() -> new RuntimeException("No booking found")); + + return new UserSessionBookingResponse( + booking.getId(), + booking.getBookingStatus(), + booking.getPaymentType(), + booking.getTotalAmount(), + booking.getPaidAmount(), + booking.getRemainingAmount() + ); + } + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java new file mode 100644 index 0000000..b1dc5af --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java @@ -0,0 +1,130 @@ +package com.divinespark.service.impl; + + +import com.divinespark.dto.InstallmentPayResponse; +import com.divinespark.dto.InstallmentResponse; +import com.divinespark.dto.RazorpayOrderResponse; +import com.divinespark.entity.Booking; +import com.divinespark.entity.Installment; +import com.divinespark.entity.Payment; +import com.divinespark.entity.enums.InstallmentStatus; +import com.divinespark.repository.BookingRepository; +import com.divinespark.repository.InstallmentRepository; +import com.divinespark.repository.PaymentRepository; +import com.divinespark.service.InstallementService; +import com.divinespark.service.RazorpayService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + + +@Service +public class InstallmentServiceImpl implements InstallementService { + + private final InstallmentRepository installmentRepository; + private final BookingRepository bookingRepository; + private final PaymentRepository paymentRepository; + private final RazorpayService razorpayService; + + public InstallmentServiceImpl( + InstallmentRepository installmentRepository, + BookingRepository bookingRepository, + PaymentRepository paymentRepository, + RazorpayService razorpayService + ) { + this.installmentRepository = installmentRepository; + this.bookingRepository = bookingRepository; + this.paymentRepository = paymentRepository; + this.razorpayService = razorpayService; + } + + @Override + @Transactional + public InstallmentPayResponse payInstallment(Long installmentId, Long userId) { + + Installment installment = installmentRepository.findById(installmentId) + .orElseThrow(() -> new RuntimeException("Installment not found")); + + Booking booking = installment.getBooking(); + + // Security check + if (!booking.getUser().getId().equals(userId)) { + throw new RuntimeException("Access denied"); + } + + // Already paid + if (installment.getStatus() == InstallmentStatus.PAID) { + throw new RuntimeException("Installment already paid"); + } + + // Enforce order: previous installment must be PAID + if (installment.getInstallmentNumber() > 1) { + installmentRepository.findByBooking_IdAndStatus( + booking.getId(), + InstallmentStatus.PENDING + ).stream() + .filter(i -> i.getInstallmentNumber() < installment.getInstallmentNumber()) + .findAny() + .ifPresent(i -> { + throw new RuntimeException("Previous installment not paid"); + }); + } + + // Create Razorpay order for THIS installment only + int amountInPaise = (int) Math.round(installment.getAmount() * 100); + + RazorpayOrderResponse order = + razorpayService.createOrder(amountInPaise, booking.getId()); + + installment.setRazorpayOrderId(order.getOrderId()); + installmentRepository.save(installment); + + // Track payment (important for admin + webhook) + Payment payment = new Payment(); + payment.setBookingId(booking.getId()); + payment.setAmount(installment.getAmount()); + payment.setGatewayOrderId(order.getOrderId()); + payment.setStatus("CREATED"); + paymentRepository.save(payment); + + InstallmentPayResponse res = new InstallmentPayResponse(); + res.setBookingId(booking.getId()); + res.setInstallmentId(installment.getId()); + res.setRazorpayOrderId(order.getOrderId()); + res.setAmount(amountInPaise); + + return res; + } + + @Override + public List getInstallmentsByBooking( + Long bookingId, + Long userId + ) { + + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new RuntimeException("Booking not found")); + + // SECURITY CHECK (VERY IMPORTANT) + if (!booking.getUser().getId().equals(userId)) { + throw new RuntimeException("Access denied"); + } + + List installments = + installmentRepository.findByBookingIdOrderByInstallmentNumber(bookingId); + + return installments.stream() + .map(i -> new InstallmentResponse( + i.getId(), + i.getInstallmentNumber(), + i.getAmount(), + i.getStatus(), + i.getPaidAt() + )) + .collect(Collectors.toList()); + } + + +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java index 6e86b32..d48a342 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java @@ -2,10 +2,7 @@ import com.divinespark.dto.*; import com.divinespark.entity.*; -import com.divinespark.entity.enums.BookingStatus; -import com.divinespark.entity.enums.PaymentType; -import com.divinespark.entity.enums.SessionStatus; -import com.divinespark.entity.enums.SessionType; +import com.divinespark.entity.enums.*; import com.divinespark.exception.BusinessException; import com.divinespark.repository.*; import com.divinespark.service.*; @@ -247,6 +244,10 @@ public InstallmentPaymentInitiateResponse initiateInstallmentPayment( Session session = sessionRepository.findById(sessionId) .orElseThrow(() -> new RuntimeException("Session not found")); + if (session.getType() != SessionType.PAID) { + throw new RuntimeException("Not a paid session"); + } + User user = userRepo.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); @@ -261,14 +262,11 @@ public InstallmentPaymentInitiateResponse initiateInstallmentPayment( booking.setBookingStatus(BookingStatus.PENDING); booking.setWhatsappLink(session.getWhatsLink()); - - bookingRepository.save(booking); session.getAvailableSeats().decrementAndGet(); - // Create installments (3 parts) - double totalAmount = session.getPrice(); - double installmentAmount = totalAmount / 3.0; + // Create installments (3 parts) + double installmentAmount = session.getPrice() / 3.0; List installments = new ArrayList<>(); @@ -277,32 +275,26 @@ public InstallmentPaymentInitiateResponse initiateInstallmentPayment( inst.setBooking(booking); inst.setInstallmentNumber(i); inst.setAmount(installmentAmount); - inst.setPaidAt(OffsetDateTime.now()); inst.setDueDate(OffsetDateTime.now().plusMonths(i - 1)); + inst.setStatus(InstallmentStatus.PENDING); installments.add(inst); } installmentRepository.saveAll(installments); - // Pick first installment + // First installment ONLY Installment firstInstallment = installments.get(0); - // Create Razorpay order + // Charge ONLY installment amount RazorpayOrderResponse order = razorpayService.createOrder( - (int) Math.round(session.getPrice() * 100), + (int) Math.round(firstInstallment.getAmount() * 100), booking.getId() ); - firstInstallment.setRazorpayOrderId(order.getOrderId()); installmentRepository.save(firstInstallment); - installmentRepository.updateOrderId( - firstInstallment.getId(), - order.getOrderId() - ); - // Response InstallmentPaymentInitiateResponse res = new InstallmentPaymentInitiateResponse(); @@ -310,20 +302,13 @@ public InstallmentPaymentInitiateResponse initiateInstallmentPayment( res.setBookingId(booking.getId()); res.setInstallmentId(firstInstallment.getId()); res.setRazorpayOrderId(order.getOrderId()); - res.setAmount(firstInstallment.getAmount() * 100); - - Payment payment = new Payment(); - payment.setBookingId(booking.getId()); - payment.setAmount(firstInstallment.getAmount()); - payment.setGatewayOrderId(order.getOrderId()); - payment.setStatus("CREATED"); - paymentRepository.save(payment); - + res.setAmount((int) Math.round(firstInstallment.getAmount() * 100)); return res; } + @Override public List getUsersBySession(Long sessionId) { return bookingRepository.findUsersBySessionId(sessionId); From 3ec7111e41ae194b07931127e08b80c2d73b374e Mon Sep 17 00:00:00 2001 From: rucha Date: Tue, 27 Jan 2026 22:38:27 +0530 Subject: [PATCH 11/23] Fixed installment issue completly --- .../user/PaymentVerificationController.java | 27 +++++++++ .../divinespark/security/SecurityConfig.java | 2 +- .../service/InstallementService.java | 2 +- .../service/impl/InstallmentServiceImpl.java | 40 +++++++++++++ .../service/impl/PaymentServiceImpl.java | 60 ++++++++++--------- 5 files changed, 102 insertions(+), 29 deletions(-) create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentVerificationController.java diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentVerificationController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentVerificationController.java new file mode 100644 index 0000000..06b776c --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentVerificationController.java @@ -0,0 +1,27 @@ +package com.divinespark.controller.user; + +import com.divinespark.service.InstallementService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/payments") +public class PaymentVerificationController { + + private final InstallementService installmentService; + + public PaymentVerificationController(InstallementService installmentService) { + this.installmentService = installmentService; + } + + @PostMapping("/installment/verify") + public ResponseEntity verifyInstallment( + @RequestParam String razorpayOrderId + ) { + installmentService.markInstallmentPaid(razorpayOrderId); + return ResponseEntity.ok().build(); + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index 1523df7..afecfa3 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -54,7 +54,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ).permitAll() // User APIs - .requestMatchers("/api/v1/user/**", "/api/v1/installments/**").hasRole("USER") + .requestMatchers("/api/v1/user/**", "/api/v1/installments/**, ", "/api/v1/payments").hasRole("USER") // Public session browsing .requestMatchers(HttpMethod.GET, "/api/v1/sessions/**").permitAll() diff --git a/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java b/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java index 20e4ccf..0c6b2ee 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java @@ -11,5 +11,5 @@ public interface InstallementService { List getInstallmentsByBooking(Long bookingId, Long userId); - + void markInstallmentPaid(String razorpayOrderId); } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java index b1dc5af..0bd71bf 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java @@ -7,6 +7,7 @@ import com.divinespark.entity.Booking; import com.divinespark.entity.Installment; import com.divinespark.entity.Payment; +import com.divinespark.entity.enums.BookingStatus; import com.divinespark.entity.enums.InstallmentStatus; import com.divinespark.repository.BookingRepository; import com.divinespark.repository.InstallmentRepository; @@ -16,6 +17,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.OffsetDateTime; import java.util.List; import java.util.stream.Collectors; @@ -127,4 +129,42 @@ public List getInstallmentsByBooking( } + @Transactional + public void markInstallmentPaid(String razorpayOrderId) { + + Installment installment = installmentRepository + .findByRazorpayOrderId(razorpayOrderId) + .orElseThrow(() -> new RuntimeException("Installment not found")); + + if (installment.getStatus() == InstallmentStatus.PAID) { + return; + } + + // Mark installment paid + installment.setStatus(InstallmentStatus.PAID); + installment.setPaidAt(OffsetDateTime.now()); + installmentRepository.save(installment); + + Booking booking = installment.getBooking(); + + // Update booking amounts + booking.setPaidAmount( + booking.getPaidAmount() + installment.getAmount() + ); + booking.setRemainingAmount( + booking.getTotalAmount() - booking.getPaidAmount() + ); + + // Update booking status + boolean allPaid = installmentRepository + .findByBookingIdOrderByInstallmentNumber(booking.getId()) + .stream() + .allMatch(i -> i.getStatus() == InstallmentStatus.PAID); + + booking.setBookingStatus( + allPaid ? BookingStatus.CONFIRMED : BookingStatus.PARTIALLY_PAID + ); + } + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java index f7e8c2f..066fef0 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java @@ -95,26 +95,7 @@ public List getAllPaymentsForAdmin() { @Transactional public boolean handlePaymentCaptured(String razorpayOrderId, int amount) { - // FULL PAYMENT - Payment payment = paymentRepository.findByGatewayOrderId(razorpayOrderId); - - if (payment != null) { - - if ("SUCCESS".equals(payment.getStatus())) return true; - - payment.setStatus("SUCCESS"); - paymentRepository.save(payment); - - Booking booking = bookingRepo.findById(payment.getBookingId()) - .orElseThrow(); - - booking.setBookingStatus(BookingStatus.CONFIRMED); - bookingRepo.save(booking); - - return true; - } - - // 1๏ธINSTALLMENT FIRST + // 1๏ธโƒฃ INSTALLMENT PAYMENT (CHECK FIRST) Installment installment = installmentRepository.findByRazorpayOrderId(razorpayOrderId) .orElse(null); @@ -122,7 +103,7 @@ public boolean handlePaymentCaptured(String razorpayOrderId, int amount) { if (installment != null) { if (installment.getStatus() == InstallmentStatus.PAID) { - return true; + return true; // idempotent } installment.setStatus(InstallmentStatus.PAID); @@ -145,12 +126,15 @@ public boolean handlePaymentCaptured(String razorpayOrderId, int amount) { Math.max(booking.getTotalAmount() - paidAmount, 0) ); - if (booking.getRemainingAmount() <= 0) { - booking.setBookingStatus(BookingStatus.CONFIRMED); - } else { - booking.setBookingStatus(BookingStatus.PARTIALLY_PAID); - } + booking.setBookingStatus( + booking.getRemainingAmount() == 0 + ? BookingStatus.CONFIRMED + : BookingStatus.PARTIALLY_PAID + ); + + bookingRepo.save(booking); + // Payment record (for admin / audit) Payment p = new Payment(); p.setBookingId(booking.getId()); p.setAmount(installment.getAmount()); @@ -158,13 +142,35 @@ public boolean handlePaymentCaptured(String razorpayOrderId, int amount) { p.setStatus("SUCCESS"); paymentRepository.save(p); + return true; + } + + // 2๏ธโƒฃ FULL PAYMENT (CHECK AFTER) + Payment payment = + paymentRepository.findByGatewayOrderId(razorpayOrderId); + + if (payment != null) { + + if ("SUCCESS".equals(payment.getStatus())) { + return true; + } + + payment.setStatus("SUCCESS"); + paymentRepository.save(payment); + + Booking booking = bookingRepo.findById(payment.getBookingId()) + .orElseThrow(); + booking.setPaidAmount(booking.getTotalAmount()); + booking.setRemainingAmount(0); + booking.setBookingStatus(BookingStatus.CONFIRMED); bookingRepo.save(booking); + return true; } return false; - } + } From 7d34a5005fd4045b7c9fc6947b424083dfcc89c5 Mon Sep 17 00:00:00 2001 From: sujaljain272004 <160271734+sujaljain272004@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:27:57 +0530 Subject: [PATCH 12/23] InstallmentAdmin --- .../AdminSessionInstallmentController.java | 30 ++++ .../admin/AdminSessionPaymentController.java | 32 ++++ .../controller/user/BookingController.java | 5 + .../dto/AdminInstallmentResponse.java | 33 +++++ .../dto/AdminSessionOverviewResponse.java | 105 +++++++++++++ .../AdminSessionUserInstallmentResponse.java | 56 +++++++ .../dto/AdminSessionUserPaymentResponse.java | 105 +++++++++++++ .../repository/BookingRepository.java | 21 +++ .../divinespark/security/SecurityConfig.java | 90 ++++++----- .../service/AdminInstallmentService.java | 8 + .../service/AdminSessionPaymentService.java | 7 + .../service/InstallementService.java | 4 + .../impl/AdminInstallmentServiceImpl.java | 64 ++++++++ .../impl/AdminSessionPaymentServiceImpl.java | 90 +++++++++++ .../service/impl/EventServiceImpl.java | 140 +++++++++--------- 15 files changed, 685 insertions(+), 105 deletions(-) create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminSessionInstallmentController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminSessionPaymentController.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/AdminInstallmentResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionOverviewResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserInstallmentResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserPaymentResponse.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/AdminInstallmentService.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/AdminSessionPaymentService.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/impl/AdminInstallmentServiceImpl.java create mode 100644 divinespark-backend/src/main/java/com/divinespark/service/impl/AdminSessionPaymentServiceImpl.java diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminSessionInstallmentController.java b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminSessionInstallmentController.java new file mode 100644 index 0000000..12d93cd --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminSessionInstallmentController.java @@ -0,0 +1,30 @@ +package com.divinespark.controller.admin; + +import com.divinespark.dto.AdminSessionUserInstallmentResponse; +import com.divinespark.service.AdminInstallmentService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/admin/sessions") +public class AdminSessionInstallmentController { + + private final AdminInstallmentService adminInstallmentService; + + public AdminSessionInstallmentController(AdminInstallmentService adminInstallmentService) { + this.adminInstallmentService = adminInstallmentService; + } + + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/{sessionId}/installments") + public ResponseEntity> getSessionInstallments( + @PathVariable Long sessionId + ) { + return ResponseEntity.ok( + adminInstallmentService.getSessionInstallments(sessionId) + ); + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminSessionPaymentController.java b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminSessionPaymentController.java new file mode 100644 index 0000000..e1f7088 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/controller/admin/AdminSessionPaymentController.java @@ -0,0 +1,32 @@ +package com.divinespark.controller.admin; + + + +import com.divinespark.dto.AdminSessionOverviewResponse; +import com.divinespark.service.AdminSessionPaymentService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/admin/sessions") +@PreAuthorize("hasRole('ADMIN')") +public class AdminSessionPaymentController { + + private final AdminSessionPaymentService service; + + public AdminSessionPaymentController( + AdminSessionPaymentService service + ) { + this.service = service; + } + + @GetMapping("/{sessionId}/overview") + public ResponseEntity overview( + @PathVariable Long sessionId + ) { + return ResponseEntity.ok( + service.getSessionOverview(sessionId) + ); + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/BookingController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/BookingController.java index c5793c3..d540353 100644 --- a/divinespark-backend/src/main/java/com/divinespark/controller/user/BookingController.java +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/BookingController.java @@ -3,10 +3,12 @@ import com.divinespark.dto.ReviewCreateRequest; import com.divinespark.dto.UserBookingResponse; import com.divinespark.dto.UserSessionBookingResponse; +import com.divinespark.entity.Booking; import com.divinespark.security.CustomUserDetails; import com.divinespark.service.BookingService; import com.divinespark.service.ReviewService; import com.divinespark.service.UserService; +import org.springframework.data.jpa.repository.Query; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; @@ -66,4 +68,7 @@ public ResponseEntity getMyBookingForSession( + + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/AdminInstallmentResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/AdminInstallmentResponse.java new file mode 100644 index 0000000..b648b28 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/AdminInstallmentResponse.java @@ -0,0 +1,33 @@ +package com.divinespark.dto; + +import com.divinespark.entity.enums.InstallmentStatus; +import java.time.OffsetDateTime; + +public class AdminInstallmentResponse { + + private Long installmentId; + private int installmentNumber; + private double amount; + private InstallmentStatus status; + private OffsetDateTime paidAt; + + public AdminInstallmentResponse( + Long installmentId, + int installmentNumber, + double amount, + InstallmentStatus status, + OffsetDateTime paidAt + ) { + this.installmentId = installmentId; + this.installmentNumber = installmentNumber; + this.amount = amount; + this.status = status; + this.paidAt = paidAt; + } + + public Long getInstallmentId() { return installmentId; } + public int getInstallmentNumber() { return installmentNumber; } + public double getAmount() { return amount; } + public InstallmentStatus getStatus() { return status; } + public OffsetDateTime getPaidAt() { return paidAt; } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionOverviewResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionOverviewResponse.java new file mode 100644 index 0000000..6e26d61 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionOverviewResponse.java @@ -0,0 +1,105 @@ +package com.divinespark.dto; + +import java.util.List; + +public class AdminSessionOverviewResponse { + + private Long sessionId; + private String sessionTitle; + + private int totalBookings; + private int fullyPaid; + private int partiallyPaid; + private int pending; + + private double totalCollected; + private double expectedRevenue; + + private List users; + + public Long getSessionId() { + return sessionId; + } + + public void setSessionId(Long sessionId) { + this.sessionId = sessionId; + } + + public String getSessionTitle() { + return sessionTitle; + } + + public void setSessionTitle(String sessionTitle) { + this.sessionTitle = sessionTitle; + } + + public int getTotalBookings() { + return totalBookings; + } + + public void setTotalBookings(int totalBookings) { + this.totalBookings = totalBookings; + } + + public int getFullyPaid() { + return fullyPaid; + } + + public void setFullyPaid(int fullyPaid) { + this.fullyPaid = fullyPaid; + } + + public int getPartiallyPaid() { + return partiallyPaid; + } + + public void setPartiallyPaid(int partiallyPaid) { + this.partiallyPaid = partiallyPaid; + } + + public int getPending() { + return pending; + } + + public void setPending(int pending) { + this.pending = pending; + } + + public double getTotalCollected() { + return totalCollected; + } + + public void setTotalCollected(double totalCollected) { + this.totalCollected = totalCollected; + } + + public double getExpectedRevenue() { + return expectedRevenue; + } + + public void setExpectedRevenue(double expectedRevenue) { + this.expectedRevenue = expectedRevenue; + } + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public AdminSessionOverviewResponse(Long sessionId, String sessionTitle, int totalBookings, int fullyPaid, int partiallyPaid, int pending, double totalCollected, double expectedRevenue, List users) { + this.sessionId = sessionId; + this.sessionTitle = sessionTitle; + this.totalBookings = totalBookings; + this.fullyPaid = fullyPaid; + this.partiallyPaid = partiallyPaid; + this.pending = pending; + this.totalCollected = totalCollected; + this.expectedRevenue = expectedRevenue; + this.users = users; + } + + public AdminSessionOverviewResponse() {} +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserInstallmentResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserInstallmentResponse.java new file mode 100644 index 0000000..3df881e --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserInstallmentResponse.java @@ -0,0 +1,56 @@ +package com.divinespark.dto; + + +import com.divinespark.entity.enums.BookingStatus; +import java.util.List; + +public class AdminSessionUserInstallmentResponse { + + private Long bookingId; + private Long userId; + private String username; + private String email; + private String contactNumber; + + private double totalAmount; + private double paidAmount; + private double remainingAmount; + + private BookingStatus bookingStatus; + private List installments; + + public AdminSessionUserInstallmentResponse( + Long bookingId, + Long userId, + String username, + String email, + String contactNumber, + double totalAmount, + double paidAmount, + double remainingAmount, + BookingStatus bookingStatus, + List installments + ) { + this.bookingId = bookingId; + this.userId = userId; + this.username = username; + this.email = email; + this.contactNumber = contactNumber; + this.totalAmount = totalAmount; + this.paidAmount = paidAmount; + this.remainingAmount = remainingAmount; + this.bookingStatus = bookingStatus; + this.installments = installments; + } + + public Long getBookingId() { return bookingId; } + public Long getUserId() { return userId; } + public String getUsername() { return username; } + public String getEmail() { return email; } + public String getContactNumber() { return contactNumber; } + public double getTotalAmount() { return totalAmount; } + public double getPaidAmount() { return paidAmount; } + public double getRemainingAmount() { return remainingAmount; } + public BookingStatus getBookingStatus() { return bookingStatus; } + public List getInstallments() { return installments; } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserPaymentResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserPaymentResponse.java new file mode 100644 index 0000000..50d0b3c --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/dto/AdminSessionUserPaymentResponse.java @@ -0,0 +1,105 @@ +package com.divinespark.dto; + +public class AdminSessionUserPaymentResponse { + + private Long bookingId; + private String userEmail; + private String username; + private String contactNumber; + + private String paymentType; // FULL / INSTALLMENT + private String bookingStatus; // CONFIRMED / PARTIALLY_PAID / PENDING + + private double totalAmount; + private double paidAmount; + private double remainingAmount; + + public Long getBookingId() { + return bookingId; + } + + public void setBookingId(Long bookingId) { + this.bookingId = bookingId; + } + + public String getUserEmail() { + return userEmail; + } + + public void setUserEmail(String userEmail) { + this.userEmail = userEmail; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getContactNumber() { + return contactNumber; + } + + public void setContactNumber(String contactNumber) { + this.contactNumber = contactNumber; + } + + public String getPaymentType() { + return paymentType; + } + + public void setPaymentType(String paymentType) { + this.paymentType = paymentType; + } + + public String getBookingStatus() { + return bookingStatus; + } + + public void setBookingStatus(String bookingStatus) { + this.bookingStatus = bookingStatus; + } + + public double getTotalAmount() { + return totalAmount; + } + + public void setTotalAmount(double totalAmount) { + this.totalAmount = totalAmount; + } + + public double getPaidAmount() { + return paidAmount; + } + + public void setPaidAmount(double paidAmount) { + this.paidAmount = paidAmount; + } + + public double getRemainingAmount() { + return remainingAmount; + } + + public void setRemainingAmount(double remainingAmount) { + this.remainingAmount = remainingAmount; + } + + public AdminSessionUserPaymentResponse(Long bookingId, String userEmail, String username, String contactNumber, String paymentType, String bookingStatus, double paidAmount, double totalAmount, double remainingAmount) { + this.bookingId = bookingId; + this.userEmail = userEmail; + this.username = username; + this.contactNumber = contactNumber; + this.paymentType = paymentType; + this.bookingStatus = bookingStatus; + this.paidAmount = paidAmount; + this.totalAmount = totalAmount; + this.remainingAmount = remainingAmount; + } + + public AdminSessionUserPaymentResponse() {} + + + +} diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java index bd8470d..7055b98 100644 --- a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java +++ b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java @@ -81,4 +81,25 @@ List findByBookingStatusAndCreatedAtBefore( ); boolean existsByUserIdAndSessionId(Long userId, Long sessionId); + + + @Query(""" +SELECT b FROM Booking b +JOIN FETCH b.user +JOIN FETCH b.session +WHERE b.session.id = :sessionId +""") + List findDetailedBookingsBySessionId( + @Param("sessionId") Long sessionId + ); + @Query(""" + SELECT b FROM Booking b + JOIN FETCH b.user + WHERE b.session.id = :sessionId +""") + List findBookingEntitiesBySessionId( + @Param("sessionId") Long sessionId + ); + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index afecfa3..f586b28 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -1,13 +1,12 @@ package com.divinespark.security; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; -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; import org.springframework.web.cors.CorsConfiguration; @@ -35,56 +34,81 @@ public SecurityConfig( public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + // ------------------------------------ + // CORS & CSRF + // ------------------------------------ .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) + + // ------------------------------------ + // Stateless session (JWT) + // ------------------------------------ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) + // ------------------------------------ + // Authorization Rules + // ------------------------------------ .authorizeHttpRequests(auth -> auth + + // Allow preflight .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - // Public APIs + // Public endpoints .requestMatchers( "/swagger-ui/**", "/v3/api-docs/**", "/api/v1/auth/**", - "/api/v1/payments/webhook", - "/api/v1/public/**" + "/api/v1/public/**", + "/api/v1/payments/webhook" ).permitAll() - // User APIs - .requestMatchers("/api/v1/user/**", "/api/v1/installments/**, ", "/api/v1/payments").hasRole("USER") - // Public session browsing .requestMatchers(HttpMethod.GET, "/api/v1/sessions/**").permitAll() - // Admin APIs + // USER APIs + .requestMatchers( + "/api/v1/user/**", + "/api/v1/installments/**", + "/api/v1/payments/**" + ).hasRole("USER") + + // ADMIN APIs .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") + // Everything else .anyRequest().authenticated() ) - // THIS PREVENTS GOOGLE REDIRECTS -// .exceptionHandling(ex -> ex -// .authenticationEntryPoint((request, response, authException) -> { -// response.setStatus(401); -// response.setContentType("application/json"); -// response.getWriter().write(""" -// { -// "error": "Unauthorized", -// "message": "JWT token missing or invalid" -// } -// """); -// }) -// ) + // ------------------------------------ + // VERY IMPORTANT: Prevent OAuth Redirect for APIs + // ------------------------------------ + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.getWriter().write(""" + { + "error": "Unauthorized", + "message": "JWT token missing or invalid" + } + """); + }) + ) + + // ------------------------------------ // OAuth ONLY for browser login - .oauth2Login(oauth -> - oauth.successHandler(oAuth2SuccessHandler) + // ------------------------------------ + .oauth2Login(oauth -> oauth + .loginPage("/api/v1/auth/oauth2") + .successHandler(oAuth2SuccessHandler) ) - // JWT filter + // ------------------------------------ + // JWT Filter + // ------------------------------------ .addFilterBefore( jwtAuthFilter, UsernamePasswordAuthenticationFilter.class @@ -93,18 +117,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } - + // ------------------------------------ + // CORS Configuration + // ------------------------------------ @Bean public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( - "http://localhost:5173" + "http://localhost:5173" // Vite frontend )); config.setAllowedMethods(List.of( - "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS" + "GET", "POST", "PUT", "DELETE", "OPTIONS" )); config.setAllowedHeaders(List.of("*")); @@ -116,9 +141,4 @@ public CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", config); return source; } -// -// @Bean -// public PasswordEncoder passwordEncoder() { -// return new BCryptPasswordEncoder(); -// } -} \ No newline at end of file +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/AdminInstallmentService.java b/divinespark-backend/src/main/java/com/divinespark/service/AdminInstallmentService.java new file mode 100644 index 0000000..adb71e4 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/AdminInstallmentService.java @@ -0,0 +1,8 @@ +package com.divinespark.service; + +import com.divinespark.dto.AdminSessionUserInstallmentResponse; +import java.util.List; + +public interface AdminInstallmentService { + List getSessionInstallments(Long sessionId); +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/AdminSessionPaymentService.java b/divinespark-backend/src/main/java/com/divinespark/service/AdminSessionPaymentService.java new file mode 100644 index 0000000..5b06ea5 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/AdminSessionPaymentService.java @@ -0,0 +1,7 @@ +package com.divinespark.service; + +import com.divinespark.dto.AdminSessionOverviewResponse; + +public interface AdminSessionPaymentService { + AdminSessionOverviewResponse getSessionOverview(Long sessionId); +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java b/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java index 0c6b2ee..fbe7acf 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java @@ -1,5 +1,6 @@ package com.divinespark.service; +import com.divinespark.dto.AdminSessionOverviewResponse; import com.divinespark.dto.InstallmentPayResponse; import com.divinespark.dto.InstallmentResponse; @@ -12,4 +13,7 @@ public interface InstallementService { List getInstallmentsByBooking(Long bookingId, Long userId); void markInstallmentPaid(String razorpayOrderId); + + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/AdminInstallmentServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/AdminInstallmentServiceImpl.java new file mode 100644 index 0000000..c46e0b9 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/AdminInstallmentServiceImpl.java @@ -0,0 +1,64 @@ +package com.divinespark.service.impl; + +import com.divinespark.dto.AdminInstallmentResponse; +import com.divinespark.dto.AdminSessionUserInstallmentResponse; +import com.divinespark.entity.Booking; +import com.divinespark.repository.BookingRepository; +import com.divinespark.repository.InstallmentRepository; +import com.divinespark.service.AdminInstallmentService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class AdminInstallmentServiceImpl implements AdminInstallmentService { + + private final BookingRepository bookingRepository; + private final InstallmentRepository installmentRepository; + + public AdminInstallmentServiceImpl( + BookingRepository bookingRepository, + InstallmentRepository installmentRepository + ) { + this.bookingRepository = bookingRepository; + this.installmentRepository = installmentRepository; + } + + @Override + public List getSessionInstallments(Long sessionId) { + + List bookings = + bookingRepository.findBookingEntitiesBySessionId(sessionId); + + + return bookings.stream().map(booking -> { + + List installments = + installmentRepository + .findByBookingIdOrderByInstallmentNumber(booking.getId()) + .stream() + .map(i -> new AdminInstallmentResponse( + i.getId(), + i.getInstallmentNumber(), + i.getAmount(), + i.getStatus(), + i.getPaidAt() + )) + .collect(Collectors.toList()); + + return new AdminSessionUserInstallmentResponse( + booking.getId(), + booking.getUser().getId(), + booking.getUser().getUsername(), + booking.getUser().getEmail(), + booking.getUser().getContactNumber(), + booking.getTotalAmount(), + booking.getPaidAmount(), + booking.getRemainingAmount(), + booking.getBookingStatus(), + installments + ); + }).collect(Collectors.toList()); + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/AdminSessionPaymentServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/AdminSessionPaymentServiceImpl.java new file mode 100644 index 0000000..f781618 --- /dev/null +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/AdminSessionPaymentServiceImpl.java @@ -0,0 +1,90 @@ +package com.divinespark.service.impl; + + + +import com.divinespark.dto.*; +import com.divinespark.entity.Booking; +import com.divinespark.entity.enums.BookingStatus; +import com.divinespark.repository.BookingRepository; +import com.divinespark.repository.SessionRepository; +import com.divinespark.service.AdminSessionPaymentService; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class AdminSessionPaymentServiceImpl + implements AdminSessionPaymentService { + + private final BookingRepository bookingRepository; + private final SessionRepository sessionRepository; + + public AdminSessionPaymentServiceImpl( + BookingRepository bookingRepository, + SessionRepository sessionRepository + ) { + this.bookingRepository = bookingRepository; + this.sessionRepository = sessionRepository; + } + + @Override + public AdminSessionOverviewResponse getSessionOverview(Long sessionId) { + + var session = sessionRepository.findById(sessionId) + .orElseThrow(() -> new RuntimeException("Session not found")); + + List bookings = + bookingRepository.findDetailedBookingsBySessionId(sessionId); + + AdminSessionOverviewResponse res = + new AdminSessionOverviewResponse(); + + res.setSessionId(sessionId); + res.setSessionTitle(session.getTitle()); + + int fullyPaid = 0, partial = 0, pending = 0; + double collected = 0, expected = 0; + + List users = + new ArrayList<>(); + + for (Booking b : bookings) { + + expected += b.getTotalAmount(); + collected += b.getPaidAmount(); + + if (b.getBookingStatus() == BookingStatus.CONFIRMED) + fullyPaid++; + else if (b.getBookingStatus() == BookingStatus.PARTIALLY_PAID) + partial++; + else + pending++; + + AdminSessionUserPaymentResponse u = + new AdminSessionUserPaymentResponse(); + + u.setBookingId(b.getId()); + u.setUserEmail(b.getUser().getEmail()); + u.setUsername(b.getUser().getUsername()); + u.setContactNumber(b.getUser().getContactNumber()); + u.setPaymentType(b.getPaymentType().name()); + u.setBookingStatus(b.getBookingStatus().name()); + u.setTotalAmount(b.getTotalAmount()); + u.setPaidAmount(b.getPaidAmount()); + u.setRemainingAmount(b.getRemainingAmount()); + + users.add(u); + } + + res.setTotalBookings(bookings.size()); + res.setFullyPaid(fullyPaid); + res.setPartiallyPaid(partial); + res.setPending(pending); + res.setTotalCollected(collected); + res.setExpectedRevenue(expected); + res.setUsers(users); + + return res; + } +} diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/EventServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/EventServiceImpl.java index 6153231..4267333 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/EventServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/EventServiceImpl.java @@ -1,86 +1,86 @@ -package com.divinespark.service.impl; + package com.divinespark.service.impl; -import com.divinespark.dto.EventRequest; -import com.divinespark.dto.EventTickerResponse; -import com.divinespark.entity.Event; -import com.divinespark.repository.EventRepository; -import com.divinespark.service.EventService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; + import com.divinespark.dto.EventRequest; + import com.divinespark.dto.EventTickerResponse; + import com.divinespark.entity.Event; + import com.divinespark.repository.EventRepository; + import com.divinespark.service.EventService; + import org.springframework.stereotype.Service; + import org.springframework.transaction.annotation.Transactional; -import java.time.OffsetDateTime; -import java.util.Comparator; -import java.util.List; + import java.time.OffsetDateTime; + import java.util.Comparator; + import java.util.List; -@Service -@Transactional -public class EventServiceImpl implements EventService { + @Service + @Transactional + public class EventServiceImpl implements EventService { - private final EventRepository repository; + private final EventRepository repository; - public EventServiceImpl(EventRepository repository) { - this.repository = repository; - } + public EventServiceImpl(EventRepository repository) { + this.repository = repository; + } - @Override - public Event create(EventRequest request) { - Event event = new Event(); - mapRequest(event, request); - return repository.save(event); - } + @Override + public Event create(EventRequest request) { + Event event = new Event(); + mapRequest(event, request); + return repository.save(event); + } - @Override - public Event update(Long id, EventRequest request) { - Event event = repository.findById(id) - .orElseThrow(() -> new RuntimeException("Workshop/Event not found")); + @Override + public Event update(Long id, EventRequest request) { + Event event = repository.findById(id) + .orElseThrow(() -> new RuntimeException("Workshop/Event not found")); - mapRequest(event, request); - return repository.save(event); - } + mapRequest(event, request); + return repository.save(event); + } - @Override - public void delete(Long id) { - repository.deleteById(id); - } + @Override + public void delete(Long id) { + repository.deleteById(id); + } - @Override - @Transactional(readOnly = true) - public List getAllAdmin() { - return repository.findAllByOrderByCreatedAtDesc(); - } + @Override + @Transactional(readOnly = true) + public List getAllAdmin() { + return repository.findAllByOrderByCreatedAtDesc(); + } - @Override - @Transactional(readOnly = true) - public List getTickerEvents() { + @Override + @Transactional(readOnly = true) + public List getTickerEvents() { - OffsetDateTime now = OffsetDateTime.now(); + OffsetDateTime now = OffsetDateTime.now(); - return repository.findAll().stream() - .filter(event -> - event.getStartTime() - .plusMinutes(event.getDurationMinutes()) - .isAfter(now) - ) - .sorted(Comparator.comparing(Event::getStartTime)) - .limit(10) - .map(this::mapToTicker) - .toList(); - } + return repository.findAll().stream() + .filter(event -> + event.getStartTime() + .plusMinutes(event.getDurationMinutes()) + .isAfter(now) + ) + .sorted(Comparator.comparing(Event::getStartTime)) + .limit(10) + .map(this::mapToTicker) + .toList(); + } - private void mapRequest(Event event, EventRequest request) { - event.setTitle(request.getTitle()); - event.setDescription(request.getDescription()); - event.setStartTime(request.getStartTime()); - event.setDurationMinutes(request.getDurationMinutes()); - } + private void mapRequest(Event event, EventRequest request) { + event.setTitle(request.getTitle()); + event.setDescription(request.getDescription()); + event.setStartTime(request.getStartTime()); + event.setDurationMinutes(request.getDurationMinutes()); + } - private EventTickerResponse mapToTicker(Event event) { - EventTickerResponse dto = new EventTickerResponse(); - dto.setId(event.getId()); - dto.setTitle(event.getTitle()); - dto.setDescription(event.getDescription()); - dto.setStartTime(event.getStartTime()); - dto.setDurationMinutes(event.getDurationMinutes()); - return dto; + private EventTickerResponse mapToTicker(Event event) { + EventTickerResponse dto = new EventTickerResponse(); + dto.setId(event.getId()); + dto.setTitle(event.getTitle()); + dto.setDescription(event.getDescription()); + dto.setStartTime(event.getStartTime()); + dto.setDurationMinutes(event.getDurationMinutes()); + return dto; + } } -} From 7af94c02cb03a0a12debff41742daf5c7b018186 Mon Sep 17 00:00:00 2001 From: RuchaChavan21 Date: Wed, 28 Jan 2026 19:07:21 +0530 Subject: [PATCH 13/23] Refactor SecurityConfig for improved authorization rules --- .../divinespark/security/SecurityConfig.java | 88 +++++++------------ 1 file changed, 34 insertions(+), 54 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index f586b28..495beda 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -1,12 +1,13 @@ package com.divinespark.security; -import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; +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; import org.springframework.web.cors.CorsConfiguration; @@ -34,81 +35,56 @@ public SecurityConfig( public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - // ------------------------------------ - // CORS & CSRF - // ------------------------------------ .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) - - // ------------------------------------ - // Stateless session (JWT) - // ------------------------------------ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) - // ------------------------------------ - // Authorization Rules - // ------------------------------------ .authorizeHttpRequests(auth -> auth - - // Allow preflight .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - // Public endpoints + // Public APIs .requestMatchers( "/swagger-ui/**", "/v3/api-docs/**", "/api/v1/auth/**", - "/api/v1/public/**", - "/api/v1/payments/webhook" + "/api/v1/payments/webhook", + "/api/v1/public/**" ).permitAll() + // User APIs + .requestMatchers("/api/v1/user/**", "/api/v1/installments/**, ", "/api/v1/payments").hasRole("USER") + // Public session browsing .requestMatchers(HttpMethod.GET, "/api/v1/sessions/**").permitAll() - // USER APIs - .requestMatchers( - "/api/v1/user/**", - "/api/v1/installments/**", - "/api/v1/payments/**" - ).hasRole("USER") - - // ADMIN APIs + // Admin APIs .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") - // Everything else .anyRequest().authenticated() ) - // ------------------------------------ - // VERY IMPORTANT: Prevent OAuth Redirect for APIs - // ------------------------------------ - .exceptionHandling(ex -> ex - .authenticationEntryPoint((request, response, authException) -> { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.getWriter().write(""" - { - "error": "Unauthorized", - "message": "JWT token missing or invalid" - } - """); - }) - ) - + // THIS PREVENTS GOOGLE REDIRECTS +// .exceptionHandling(ex -> ex +// .authenticationEntryPoint((request, response, authException) -> { +// response.setStatus(401); +// response.setContentType("application/json"); +// response.getWriter().write(""" +// { +// "error": "Unauthorized", +// "message": "JWT token missing or invalid" +// } +// """); +// }) +// ) - // ------------------------------------ // OAuth ONLY for browser login - // ------------------------------------ - .oauth2Login(oauth -> oauth - .loginPage("/api/v1/auth/oauth2") - .successHandler(oAuth2SuccessHandler) + .oauth2Login(oauth -> + oauth.successHandler(oAuth2SuccessHandler) ) - // ------------------------------------ - // JWT Filter - // ------------------------------------ + // JWT filter .addFilterBefore( jwtAuthFilter, UsernamePasswordAuthenticationFilter.class @@ -117,19 +93,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } - // ------------------------------------ - // CORS Configuration - // ------------------------------------ + @Bean public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( - "http://localhost:5173" // Vite frontend + "http://localhost:5173" )); config.setAllowedMethods(List.of( - "GET", "POST", "PUT", "DELETE", "OPTIONS" + "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS" )); config.setAllowedHeaders(List.of("*")); @@ -141,4 +116,9 @@ public CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", config); return source; } +// +// @Bean +// public PasswordEncoder passwordEncoder() { +// return new BCryptPasswordEncoder(); +// } } From aa3522f353c3bf4cd1ebe8346ad8138a6612063f Mon Sep 17 00:00:00 2001 From: nehan1311 Date: Wed, 28 Jan 2026 22:00:05 +0530 Subject: [PATCH 14/23] added the blog and review endpoints --- .../main/java/com/divinespark/security/SecurityConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index 495beda..ef9dd34 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -50,7 +50,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/v3/api-docs/**", "/api/v1/auth/**", "/api/v1/payments/webhook", - "/api/v1/public/**" + "/api/v1/public/**", + "/api/v1/blogs/**", + "/api/v1/user/review/**" ).permitAll() // User APIs From 9656042b707dcd02fe6c3f228738caead4a4a9bd Mon Sep 17 00:00:00 2001 From: rucha Date: Sun, 1 Feb 2026 16:29:58 +0530 Subject: [PATCH 15/23] implement installment payments with booking and admin improvement --- .../java/com/divinespark/entity/Booking.java | 3 +-- .../repository/BookingRepository.java | 6 ++++++ .../service/impl/SessionServiceImpl.java | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/Booking.java b/divinespark-backend/src/main/java/com/divinespark/entity/Booking.java index 1182cde..cec4bef 100644 --- a/divinespark-backend/src/main/java/com/divinespark/entity/Booking.java +++ b/divinespark-backend/src/main/java/com/divinespark/entity/Booking.java @@ -7,8 +7,7 @@ import java.time.OffsetDateTime; @Entity -@Table(name = "bookings", - uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "session_id"})) +@Table(name = "bookings") public class Booking { @Id diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java index 7055b98..e3fe02f 100644 --- a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java +++ b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java @@ -101,5 +101,11 @@ List findBookingEntitiesBySessionId( @Param("sessionId") Long sessionId ); + boolean existsByUserIdAndSessionIdAndBookingStatusIn( + Long userId, + Long sessionId, + List statuses + ); + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java index d48a342..98819ee 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java @@ -248,6 +248,24 @@ public InstallmentPaymentInitiateResponse initiateInstallmentPayment( throw new RuntimeException("Not a paid session"); } + boolean alreadyActiveBooking = + bookingRepository.existsByUserIdAndSessionIdAndBookingStatusIn( + userId, + sessionId, + List.of( + BookingStatus.PENDING, + BookingStatus.CONFIRMED, + BookingStatus.PARTIALLY_PAID + ) + ); + + if (alreadyActiveBooking) { + throw new BusinessException( + "You already have an active booking for this session" + ); + } + + User user = userRepo.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); From 8e42da3953ed0d0db3313298d8e0760acebeb182 Mon Sep 17 00:00:00 2001 From: rucha Date: Sun, 1 Feb 2026 18:19:58 +0530 Subject: [PATCH 16/23] fixed whatapp link issue --- .../repository/BookingRepository.java | 16 ++++++---------- .../service/impl/BookingServiceImpl.java | 2 +- .../service/impl/SessionServiceImpl.java | 12 ++++++++++-- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java index e3fe02f..8a0b048 100644 --- a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java +++ b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java @@ -24,22 +24,18 @@ public interface BookingRepository extends JpaRepository { """) List findUserBookingsWithSession(Long userId); - Optional findByUser_IdAndSession_Id( - Long userId, - Long sessionId - ); + Optional findByUserIdAndSessionId(Long userId, Long sessionId); - boolean existsByUser_IdAndSession_Id( - Long userId, - Long sessionId - ); - boolean existsByUser_IdAndSession_IdAndBookingStatus( + Optional findFirstByUser_IdAndSession_IdAndBookingStatusInOrderByCreatedAtDesc( Long userId, Long sessionId, - BookingStatus bookingStatus + List statuses ); + + + // ---------- ADMIN ---------- @Query(""" SELECT new com.divinespark.dto.AdminSessionUserResponse( diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java index 2084333..c8c9474 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/BookingServiceImpl.java @@ -139,7 +139,7 @@ public UserSessionBookingResponse getMyBookingForSession( ) { Booking booking = bookingRepository - .findByUser_IdAndSession_Id(userId, sessionId) + .findByUserIdAndSessionId(userId, sessionId) .orElseThrow(() -> new RuntimeException("No booking found")); return new UserSessionBookingResponse( diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java index 98819ee..7098cbf 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java @@ -192,7 +192,7 @@ public PaymentInitiateResponse initiatePaidSession(Long sessionId, Long userId) .orElseThrow(() -> new RuntimeException("User not found")); Booking booking = bookingRepository - .findByUser_IdAndSession_Id(userId, sessionId) + .findByUserIdAndSessionId(userId, sessionId) .orElseGet(() -> { Booking b = new Booking(); b.setSession(session); @@ -422,9 +422,17 @@ public SessionDetailResponse getSessionDetails(Long sessionId) { public String getWhatsappLinkIfConfirmed(Long sessionId, Long userId) { Booking booking = bookingRepository - .findByUser_IdAndSession_Id(userId, sessionId) + .findFirstByUser_IdAndSession_IdAndBookingStatusInOrderByCreatedAtDesc( + userId, + sessionId, + List.of( + BookingStatus.CONFIRMED, + BookingStatus.PARTIALLY_PAID + ) + ) .orElseThrow(() -> new ExpressionException("Access denied")); + if ( booking.getBookingStatus() != BookingStatus.CONFIRMED && booking.getBookingStatus() != BookingStatus.PARTIALLY_PAID From a153cb074be5612206373a7593255b2e802d8655 Mon Sep 17 00:00:00 2001 From: rucha Date: Wed, 4 Feb 2026 12:47:49 +0530 Subject: [PATCH 17/23] fixed razorpay, already booked and installment issue --- .../user/PaymentVerificationController.java | 19 +++++- .../repository/BookingRepository.java | 1 + .../repository/InstallmentRepository.java | 4 ++ .../repository/PaymentRepository.java | 2 +- .../divinespark/security/SecurityConfig.java | 6 +- .../service/InstallementService.java | 6 +- .../divinespark/service/RazorpayService.java | 6 ++ .../service/impl/InstallmentServiceImpl.java | 66 ++++++++++++++----- .../service/impl/PaymentServiceImpl.java | 13 ++-- .../service/impl/RazorpayServiceImpl.java | 48 ++++++++++++++ .../service/impl/SessionServiceImpl.java | 59 +++++++++-------- 11 files changed, 177 insertions(+), 53 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentVerificationController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentVerificationController.java index 06b776c..9d8bbc4 100644 --- a/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentVerificationController.java +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/PaymentVerificationController.java @@ -19,9 +19,24 @@ public PaymentVerificationController(InstallementService installmentService) { @PostMapping("/installment/verify") public ResponseEntity verifyInstallment( - @RequestParam String razorpayOrderId + @RequestParam String razorpayOrderId, + @RequestParam String razorpayPaymentId, + @RequestParam String razorpaySignature ) { - installmentService.markInstallmentPaid(razorpayOrderId); + System.out.println(">>> VERIFY API HIT <<<"); + System.out.println("orderId = " + razorpayOrderId); + System.out.println("paymentId = " + razorpayPaymentId); + System.out.println("signature = " + razorpaySignature); + + installmentService.verifyAndMarkInstallmentPaid( + razorpayOrderId, + razorpayPaymentId, + razorpaySignature + ); + return ResponseEntity.ok().build(); } + + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java index 8a0b048..744190b 100644 --- a/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java +++ b/divinespark-backend/src/main/java/com/divinespark/repository/BookingRepository.java @@ -104,4 +104,5 @@ boolean existsByUserIdAndSessionIdAndBookingStatusIn( ); + } diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/InstallmentRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/InstallmentRepository.java index 694b352..6ecf59f 100644 --- a/divinespark-backend/src/main/java/com/divinespark/repository/InstallmentRepository.java +++ b/divinespark-backend/src/main/java/com/divinespark/repository/InstallmentRepository.java @@ -37,5 +37,9 @@ void updateOrderId( @Param("orderId") String orderId ); + @Modifying + @Query("delete from Installment i where i.booking.id = :bookingId") + void deleteByBookingId(@Param("bookingId") Long bookingId); + } diff --git a/divinespark-backend/src/main/java/com/divinespark/repository/PaymentRepository.java b/divinespark-backend/src/main/java/com/divinespark/repository/PaymentRepository.java index 3a73a9a..6978a29 100644 --- a/divinespark-backend/src/main/java/com/divinespark/repository/PaymentRepository.java +++ b/divinespark-backend/src/main/java/com/divinespark/repository/PaymentRepository.java @@ -10,7 +10,7 @@ public interface PaymentRepository extends JpaRepository { - Payment findByGatewayOrderId(String gatewayOrderId); + Optional findByGatewayOrderId(String gatewayOrderId); Optional findTopByBookingIdOrderByCreatedAtDesc(Long bookingId); diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index 495beda..fd7162a 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -54,7 +54,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ).permitAll() // User APIs - .requestMatchers("/api/v1/user/**", "/api/v1/installments/**, ", "/api/v1/payments").hasRole("USER") + .requestMatchers( + "/api/v1/user/**", + "/api/v1/installments/**", + "/api/v1/payments/**" + ).hasRole("USER") // Public session browsing .requestMatchers(HttpMethod.GET, "/api/v1/sessions/**").permitAll() diff --git a/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java b/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java index fbe7acf..143fadc 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/InstallementService.java @@ -12,8 +12,8 @@ public interface InstallementService { List getInstallmentsByBooking(Long bookingId, Long userId); - void markInstallmentPaid(String razorpayOrderId); - - + boolean verifyAndMarkInstallmentPaid (String orderId, + String paymentId, + String signature); } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/RazorpayService.java b/divinespark-backend/src/main/java/com/divinespark/service/RazorpayService.java index 44870e3..e2dba56 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/RazorpayService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/RazorpayService.java @@ -4,4 +4,10 @@ public interface RazorpayService { RazorpayOrderResponse createOrder(int amount, Long bookingId); + + boolean verifySignature( + String razorpayOrderId, + String razorpayPaymentId, + String razorpaySignature + ); } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java index 0bd71bf..88f74b3 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/InstallmentServiceImpl.java @@ -9,9 +9,11 @@ import com.divinespark.entity.Payment; import com.divinespark.entity.enums.BookingStatus; import com.divinespark.entity.enums.InstallmentStatus; +import com.divinespark.entity.enums.PaymentType; import com.divinespark.repository.BookingRepository; import com.divinespark.repository.InstallmentRepository; import com.divinespark.repository.PaymentRepository; +import com.divinespark.repository.UserRepository; import com.divinespark.service.InstallementService; import com.divinespark.service.RazorpayService; import org.springframework.stereotype.Service; @@ -19,6 +21,7 @@ import java.time.OffsetDateTime; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @@ -29,17 +32,20 @@ public class InstallmentServiceImpl implements InstallementService { private final BookingRepository bookingRepository; private final PaymentRepository paymentRepository; private final RazorpayService razorpayService; + private final UserRepository userRepository; public InstallmentServiceImpl( InstallmentRepository installmentRepository, BookingRepository bookingRepository, PaymentRepository paymentRepository, - RazorpayService razorpayService + RazorpayService razorpayService, + UserRepository userRepository ) { this.installmentRepository = installmentRepository; this.bookingRepository = bookingRepository; this.paymentRepository = paymentRepository; this.razorpayService = razorpayService; + this.userRepository = userRepository; } @Override @@ -129,42 +135,72 @@ public List getInstallmentsByBooking( } + + @Override @Transactional - public void markInstallmentPaid(String razorpayOrderId) { + public boolean verifyAndMarkInstallmentPaid( + String razorpayOrderId, + String razorpayPaymentId, + String razorpaySignature + ) { + // 1 Find installment by Razorpay order ID Installment installment = installmentRepository .findByRazorpayOrderId(razorpayOrderId) .orElseThrow(() -> new RuntimeException("Installment not found")); + Booking booking = installment.getBooking(); + + // 2 Verify Razorpay signature + System.out.println(">>> VERIFY SERVICE CALLED <<<"); + boolean verified = razorpayService.verifySignature( + razorpayOrderId, + razorpayPaymentId, + razorpaySignature + ); + + System.out.println("SIGNATURE VERIFIED = " + verified); + + + if (!verified) { + throw new RuntimeException("Payment verification failed"); + } + + // 3 Idempotency (avoid double update) if (installment.getStatus() == InstallmentStatus.PAID) { - return; + return true; } - // Mark installment paid + // 4 Mark FIRST installment as PAID installment.setStatus(InstallmentStatus.PAID); installment.setPaidAt(OffsetDateTime.now()); installmentRepository.save(installment); - Booking booking = installment.getBooking(); - - // Update booking amounts + // 5๏ธ Update booking amounts booking.setPaidAmount( booking.getPaidAmount() + installment.getAmount() ); + booking.setRemainingAmount( booking.getTotalAmount() - booking.getPaidAmount() ); - // Update booking status - boolean allPaid = installmentRepository - .findByBookingIdOrderByInstallmentNumber(booking.getId()) - .stream() - .allMatch(i -> i.getStatus() == InstallmentStatus.PAID); + // 6๏ธ Update booking status + booking.setBookingStatus(BookingStatus.PARTIALLY_PAID); + bookingRepository.save(booking); - booking.setBookingStatus( - allPaid ? BookingStatus.CONFIRMED : BookingStatus.PARTIALLY_PAID - ); + // 7๏ธ Update payment record + paymentRepository.findByGatewayOrderId(razorpayOrderId) + .ifPresent(p -> { + p.setStatus("SUCCESS"); + paymentRepository.save(p); + }); + + return true; } + + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java index 066fef0..a6b9fde 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/PaymentServiceImpl.java @@ -42,7 +42,8 @@ public PaymentServiceImpl( public void handlePaymentFailure(String gatewayOrderId) { Payment payment = - paymentRepository.findByGatewayOrderId(gatewayOrderId); + paymentRepository.findByGatewayOrderId(gatewayOrderId) + .orElseThrow(() -> new RuntimeException("Payment not found")); if (payment == null) return; if ("FAILED".equals(payment.getStatus())) return; @@ -64,7 +65,8 @@ public void handlePaymentFailure(String gatewayOrderId) { public void handlePaymentSuccess(String gatewayOrderId) { Payment payment = - paymentRepository.findByGatewayOrderId(gatewayOrderId); + paymentRepository.findByGatewayOrderId(gatewayOrderId) + .orElseThrow(() -> new RuntimeException("Payment not found")); if (payment == null) return; if ("SUCCESS".equals(payment.getStatus())) return; @@ -95,7 +97,7 @@ public List getAllPaymentsForAdmin() { @Transactional public boolean handlePaymentCaptured(String razorpayOrderId, int amount) { - // 1๏ธโƒฃ INSTALLMENT PAYMENT (CHECK FIRST) + // 1๏ธ INSTALLMENT PAYMENT (CHECK FIRST) Installment installment = installmentRepository.findByRazorpayOrderId(razorpayOrderId) .orElse(null); @@ -145,9 +147,10 @@ public boolean handlePaymentCaptured(String razorpayOrderId, int amount) { return true; } - // 2๏ธโƒฃ FULL PAYMENT (CHECK AFTER) + // 2 FULL PAYMENT (CHECK AFTER) Payment payment = - paymentRepository.findByGatewayOrderId(razorpayOrderId); + paymentRepository.findByGatewayOrderId(razorpayOrderId) + .orElseThrow(() -> new RuntimeException("Payment not found")); if (payment != null) { diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/RazorpayServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/RazorpayServiceImpl.java index 4b2c1a1..1ad0948 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/RazorpayServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/RazorpayServiceImpl.java @@ -6,12 +6,21 @@ import com.razorpay.Order; import com.razorpay.RazorpayClient; import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + @Service public class RazorpayServiceImpl implements RazorpayService { private final RazorpayConfig razorpayConfig; + @Value("${razorpay.key.secret}") + private String razorpaySecret; + public RazorpayServiceImpl(RazorpayConfig razorpayConfig) { this.razorpayConfig = razorpayConfig; @@ -44,4 +53,43 @@ public RazorpayOrderResponse createOrder(int amountInPaise, Long referenceId) { } } + @Override + public boolean verifySignature( + String razorpayOrderId, + String razorpayPaymentId, + String razorpaySignature + ) { + try { + String payload = razorpayOrderId + "|" + razorpayPaymentId; + + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKey = + new SecretKeySpec( + razorpaySecret.getBytes(StandardCharsets.UTF_8), + "HmacSHA256" + ); + + mac.init(secretKey); + byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); + + // ๐Ÿ”ฅ HEX encoding (NOT Base64) + String generatedSignature = bytesToHex(hash); + + return generatedSignature.equals(razorpaySignature); + + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + private String bytesToHex(byte[] bytes) { + StringBuilder hex = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } + + } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java index 7098cbf..95ae56f 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/SessionServiceImpl.java @@ -248,28 +248,29 @@ public InstallmentPaymentInitiateResponse initiateInstallmentPayment( throw new RuntimeException("Not a paid session"); } - boolean alreadyActiveBooking = + if (session.getAvailableSeats().get() <= 0) { + throw new BusinessException("No seats available"); + } + + User user = userRepo.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + // block only real bookings + boolean alreadyBooked = bookingRepository.existsByUserIdAndSessionIdAndBookingStatusIn( userId, sessionId, List.of( - BookingStatus.PENDING, BookingStatus.CONFIRMED, BookingStatus.PARTIALLY_PAID ) ); - if (alreadyActiveBooking) { - throw new BusinessException( - "You already have an active booking for this session" - ); + if (alreadyBooked) { + throw new BusinessException("You already booked this session"); } - - User user = userRepo.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); - - // Create booking + // Create booking (PENDING) Booking booking = new Booking(); booking.setUser(user); booking.setSession(session); @@ -281,12 +282,11 @@ public InstallmentPaymentInitiateResponse initiateInstallmentPayment( booking.setWhatsappLink(session.getWhatsLink()); bookingRepository.save(booking); - session.getAvailableSeats().decrementAndGet(); - // Create installments (3 parts) + // Create installments double installmentAmount = session.getPrice() / 3.0; - List installments = new ArrayList<>(); + Installment firstInstallment = null; for (int i = 1; i <= 3; i++) { Installment inst = new Installment(); @@ -295,38 +295,45 @@ public InstallmentPaymentInitiateResponse initiateInstallmentPayment( inst.setAmount(installmentAmount); inst.setDueDate(OffsetDateTime.now().plusMonths(i - 1)); inst.setStatus(InstallmentStatus.PENDING); - installments.add(inst); - } + installmentRepository.save(inst); - installmentRepository.saveAll(installments); + if (i == 1) firstInstallment = inst; + } - // First installment ONLY - Installment firstInstallment = installments.get(0); + // Create Razorpay order for FIRST installment + int amountInPaise = (int) Math.round(installmentAmount * 100); - // Charge ONLY installment amount RazorpayOrderResponse order = - razorpayService.createOrder( - (int) Math.round(firstInstallment.getAmount() * 100), - booking.getId() - ); + razorpayService.createOrder(amountInPaise, booking.getId()); firstInstallment.setRazorpayOrderId(order.getOrderId()); installmentRepository.save(firstInstallment); - // Response + // Create payment (bookingId REQUIRED) + Payment payment = new Payment(); + payment.setBookingId(booking.getId()); + payment.setAmount(installmentAmount); + payment.setGatewayOrderId(order.getOrderId()); + payment.setStatus("CREATED"); + paymentRepository.save(payment); + + // Seat is reserved but NOT confirmed yet + InstallmentPaymentInitiateResponse res = new InstallmentPaymentInitiateResponse(); res.setBookingId(booking.getId()); res.setInstallmentId(firstInstallment.getId()); res.setRazorpayOrderId(order.getOrderId()); - res.setAmount((int) Math.round(firstInstallment.getAmount() * 100)); + res.setAmount(amountInPaise); return res; } + + @Override public List getUsersBySession(Long sessionId) { return bookingRepository.findUsersBySessionId(sessionId); From 17efe60614b2a039c53ff6a6c92b9938f50db878 Mon Sep 17 00:00:00 2001 From: rucha Date: Wed, 4 Feb 2026 19:04:52 +0530 Subject: [PATCH 18/23] Removed duration compulsion in event --- .../src/main/java/com/divinespark/entity/Event.java | 2 +- .../src/main/java/com/divinespark/security/SecurityConfig.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/Event.java b/divinespark-backend/src/main/java/com/divinespark/entity/Event.java index 334707c..bd790a4 100644 --- a/divinespark-backend/src/main/java/com/divinespark/entity/Event.java +++ b/divinespark-backend/src/main/java/com/divinespark/entity/Event.java @@ -22,7 +22,7 @@ public class Event { @Column(nullable = false) private OffsetDateTime startTime; - @Column(nullable = false) + @Column private Integer durationMinutes; // auto-expiry logic uses this diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index fd7162a..ffa94bc 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -6,8 +6,6 @@ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; -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; import org.springframework.web.cors.CorsConfiguration; From 3197a53c8958c3c63cdd4b9f454b100eac76d081 Mon Sep 17 00:00:00 2001 From: Rucha Chavan Date: Wed, 4 Feb 2026 19:12:44 +0530 Subject: [PATCH 19/23] Removed duration compulsion in event (#88) --- .../src/main/java/com/divinespark/entity/Event.java | 2 +- .../src/main/java/com/divinespark/security/SecurityConfig.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/entity/Event.java b/divinespark-backend/src/main/java/com/divinespark/entity/Event.java index 334707c..bd790a4 100644 --- a/divinespark-backend/src/main/java/com/divinespark/entity/Event.java +++ b/divinespark-backend/src/main/java/com/divinespark/entity/Event.java @@ -22,7 +22,7 @@ public class Event { @Column(nullable = false) private OffsetDateTime startTime; - @Column(nullable = false) + @Column private Integer durationMinutes; // auto-expiry logic uses this diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index 1090c04..8d57938 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -6,8 +6,6 @@ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; -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; import org.springframework.web.cors.CorsConfiguration; From 4ab74eec8154fe39e3ca1bbda4212a5447fa15a0 Mon Sep 17 00:00:00 2001 From: rucha Date: Sat, 7 Feb 2026 09:42:50 +0530 Subject: [PATCH 20/23] allow public access to get review and blog --- .../controller/user/ReviewController.java | 1 - .../com/divinespark/security/SecurityConfig.java | 14 ++++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/controller/user/ReviewController.java b/divinespark-backend/src/main/java/com/divinespark/controller/user/ReviewController.java index db2733e..1f30f15 100644 --- a/divinespark-backend/src/main/java/com/divinespark/controller/user/ReviewController.java +++ b/divinespark-backend/src/main/java/com/divinespark/controller/user/ReviewController.java @@ -31,7 +31,6 @@ public ResponseEntity submitReview( return ResponseEntity.ok().build(); } - @PreAuthorize("hasRole('USER')") @GetMapping public ResponseEntity getMyReview( @AuthenticationPrincipal CustomUserDetails userDetails diff --git a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java index ffa94bc..d625157 100644 --- a/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java +++ b/divinespark-backend/src/main/java/com/divinespark/security/SecurityConfig.java @@ -51,22 +51,28 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/v1/public/**" ).permitAll() - // User APIs + // PUBLIC: session browsing & reviews (NO LOGIN) + .requestMatchers( + HttpMethod.GET, + "/api/v1/sessions/**", + "/api/v1/user/review/**", + "/api/v1/blogs/**" + ).permitAll() + + // User APIs (LOGIN REQUIRED) .requestMatchers( "/api/v1/user/**", "/api/v1/installments/**", "/api/v1/payments/**" ).hasRole("USER") - // Public session browsing - .requestMatchers(HttpMethod.GET, "/api/v1/sessions/**").permitAll() - // Admin APIs .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) + // THIS PREVENTS GOOGLE REDIRECTS // .exceptionHandling(ex -> ex // .authenticationEntryPoint((request, response, authException) -> { From 1af23e7913a3d282b3395c03711ad19eb3203429 Mon Sep 17 00:00:00 2001 From: Rucha Chavan Date: Tue, 10 Feb 2026 08:21:49 +0530 Subject: [PATCH 21/23] Add README for DivineSpark platform Added comprehensive documentation for the DivineSpark platform, detailing features, tech stack, architecture, and project status. --- README.md | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbb3992 --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +# ๐ŸŒŸ DivineSpark โ€” Live Session Booking & Payments Platform + +DivineSpark is a **production-ready, client-facing web platform** for managing and booking live spiritual and wellness sessions. +The system is designed with real-world requirements such as **secure authentication, role-based access, installment payments, admin control, and operational automation**. + +--- + +## ๐Ÿš€ Key Features + +### ๐Ÿ” Authentication & Security +- Email-based authentication with JWT +- Role-based access control (USER / ADMIN) +- Secured APIs using Spring Security +- Controlled access to session join links based on payment eligibility + +--- + +### ๐Ÿ“… Session Management +- Admin can create, update, and delete sessions +- Supports both **Free** and **Paid** sessions +- Seat availability management +- Users can view and book upcoming sessions + +--- + +### ๐Ÿ’ณ Payments & Installments +- Integrated Razorpay for secure online payments +- Supports: + - One-time full payments + - Installment-based payments +- Tracks: + - Paid amount + - Remaining amount + - Installment status per booking +- Booking lifecycle states: + - `PENDING` + - `PARTIALLY_PAID` + - `CONFIRMED` + - `CANCELLED` + +--- + +### ๐Ÿ”— Controlled Session Access +- Session join (WhatsApp) links are exposed only when eligibility is verified +- Access allowed only for `CONFIRMED` or `PARTIALLY_PAID` users +- Prevents unauthorized sharing or misuse of session links + +--- + +### ๐Ÿ›  Admin Features +- View users registered for each session +- Monitor installment payments per user +- Approve or reject user reviews +- Manage blogs, reviews, and events +- Export user, booking, and payment data as CSV for reporting + +--- + +### โš™๏ธ Automation & Maintenance +- Designed to support scheduled CRON jobs +- Periodic database cleanup and archival (e.g., every 3 months) +- Helps reduce storage cost and maintain long-term performance + +--- + +## ๐Ÿงฑ Tech Stack + +### Backend +- Java 17 +- Spring Boot +- Spring Security (JWT) +- Spring Data JPA +- MySQL +- Razorpay Payment Gateway + +### Frontend +- React +- TypeScript +- Axios +- Mobile-friendly and responsive UI + +### Tools & Concepts +- REST APIs +- Webhooks for payment events +- Cron jobs for automation +- CSV export for admin reporting + +--- + +## ๐Ÿ— Architecture Overview + +- Stateless backend with JWT-based authentication +- Clear separation of layers: + - Controllers + - Services + - Repositories +- Payment handling via: + - Frontend verification (primary) + - Webhook-based confirmation (fallback) +- Booking records preserved as immutable financial history +- Business rules enforced at the service layer + +--- + +## ๐Ÿ”„ Booking & Payment Flow + +1. User browses available sessions +2. Selects free or paid session +3. For paid sessions: + - Chooses full payment or installment option +4. Payment processed via Razorpay +5. Booking status updated based on payment progress +6. Session access granted only to eligible users + +--- + +## ๐Ÿงช Testing & Validation + +- Manual testing of: + - Free session booking + - Full payment flow + - Installment payments + - Cancelled booking re-booking + - Admin workflows +- Payment edge cases handled: + - Webhook retries + - Partial payments + - Duplicate payment prevention + +--- + +## ๐Ÿ“ฆ Project Status + +- Core features implemented +- Backend is production-ready +- Continuous enhancements in progress + +--- + +## ๐Ÿ“Œ Planned Enhancements +- Automated installment reminder notifications +- Admin payment reconciliation dashboard +- Advanced analytics and reporting +- Full CRON-based archival pipeline +- Cloud deployment with monitoring + +--- + +## ๐Ÿ‘ฉโ€๐Ÿ’ป Project Context + +DivineSpark is a **client-facing production project**, built with a strong focus on real-world payment flows, secure access control, operational automation, and long-term maintainability. + +--- + +## ๐Ÿ“„ License + +This project is proprietary and developed for a client use case. +Reuse or redistribution requires prior permission. From 6ea25b81d9e3d55b559b03e517fed79e8f93e144 Mon Sep 17 00:00:00 2001 From: sujaljain272004 <160271734+sujaljain272004@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:15:51 +0530 Subject: [PATCH 22/23] Blog error fix --- .../divinespark/dto/BlogDetailResponse.java | 10 +++++++ .../com/divinespark/dto/BlogListResponse.java | 10 +++++++ .../service/impl/BlogServiceImpl.java | 27 ++++++++++++++----- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/BlogDetailResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/BlogDetailResponse.java index 6b8a3e5..6d86c21 100644 --- a/divinespark-backend/src/main/java/com/divinespark/dto/BlogDetailResponse.java +++ b/divinespark-backend/src/main/java/com/divinespark/dto/BlogDetailResponse.java @@ -10,6 +10,16 @@ public class BlogDetailResponse { private String authorRole; private OffsetDateTime createdAt; + + private Boolean isPublished; + + public Boolean getIsPublished() { + return isPublished; + } + public void setIsPublished(Boolean isPublished) { + this.isPublished = isPublished; + } + public String getTitle() { return title; } diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java b/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java index da4c3a4..3b39f06 100644 --- a/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java +++ b/divinespark-backend/src/main/java/com/divinespark/dto/BlogListResponse.java @@ -12,6 +12,16 @@ public class BlogListResponse { private Long id; + private Boolean isPublished; + + public Boolean getIsPublished() { + return isPublished; + } + public void setIsPublished(Boolean isPublished) { + this.isPublished = isPublished; + } + + public String getAuthorName() { return authorName; } diff --git a/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java b/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java index d8d22e2..bb428fa 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/impl/BlogServiceImpl.java @@ -41,6 +41,7 @@ public void createBlog(BlogCreateRequest request) { blog.setAuthorName(request.getAuthorName()); blog.setAuthorRole(request.getAuthorRole()); blog.setStatus(BlogStatus.DRAFT); + // Removed setCreatedAt and setUpdatedAt as they are handled automatically by your Entity blogRepository.save(blog); } @@ -84,12 +85,17 @@ public List getPublishedBlogs() { for (Blog b : blogs) { BlogListResponse dto = new BlogListResponse(); + dto.setId(b.getId()); dto.setTitle(b.getTitle()); dto.setSlug(b.getSlug()); dto.setExcerpt(b.getExcerpt()); dto.setAuthorName(b.getAuthorName()); dto.setAuthorRole(b.getAuthorRole()); dto.setCreatedAt(b.getCreatedAt()); + + // โœ… Set published to true for this list + dto.setIsPublished(true); + response.add(dto); } @@ -110,18 +116,20 @@ public BlogDetailResponse getBlogBySlug(String slug) { res.setAuthorRole(blog.getAuthorRole()); res.setCreatedAt(blog.getCreatedAt()); + // โœ… Set published status + res.setIsPublished(true); + return res; } // ---------- SLUG GENERATOR ---------- private String generateSlug(String input) { - String slug = Normalizer.normalize(input, Normalizer.Form.NFD) + if (input == null) return ""; + return Normalizer.normalize(input, Normalizer.Form.NFD) .replaceAll("[^\\w\\s-]", "") .trim() .replaceAll("\\s+", "-") .toLowerCase(Locale.ENGLISH); - - return slug; } @Override @@ -132,13 +140,18 @@ public List getAllBlogsForAdmin() { for (Blog b : blogs) { BlogListResponse dto = new BlogListResponse(); - dto.setId(b.getId()); // โญ THIS WAS MISSING + dto.setId(b.getId()); dto.setTitle(b.getTitle()); dto.setSlug(b.getSlug()); dto.setExcerpt(b.getExcerpt()); dto.setAuthorName(b.getAuthorName()); dto.setAuthorRole(b.getAuthorRole()); dto.setCreatedAt(b.getCreatedAt()); + + // โœ… CRITICAL FIX: Map the status to the boolean field + // This ensures the frontend sees correct status! + dto.setIsPublished(b.getStatus() == BlogStatus.PUBLISHED); + response.add(dto); } @@ -159,8 +172,10 @@ public BlogDetailResponse getBlogByIdForAdmin(Long id) { res.setAuthorRole(blog.getAuthorRole()); res.setCreatedAt(blog.getCreatedAt()); + // โœ… CRITICAL FIX: Map the status here as well + res.setIsPublished(blog.getStatus() == BlogStatus.PUBLISHED); + return res; } - -} +} \ No newline at end of file From 597adeb98cb29ef117d0c7b7480ecd415b9f591c Mon Sep 17 00:00:00 2001 From: rucha Date: Thu, 19 Mar 2026 15:35:50 +0530 Subject: [PATCH 23/23] fixed contact --- .../com/divinespark/dto/SessionCreateRequest.java | 2 +- .../java/com/divinespark/service/AuthService.java | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/divinespark-backend/src/main/java/com/divinespark/dto/SessionCreateRequest.java b/divinespark-backend/src/main/java/com/divinespark/dto/SessionCreateRequest.java index 436d70b..8bc8684 100644 --- a/divinespark-backend/src/main/java/com/divinespark/dto/SessionCreateRequest.java +++ b/divinespark-backend/src/main/java/com/divinespark/dto/SessionCreateRequest.java @@ -13,7 +13,7 @@ public class SessionCreateRequest { private double price; private String whatsappGroupLink; - // โœ… Frontend sends OffsetDateTime with +05:30 + // Frontend sends OffsetDateTime with +05:30 private OffsetDateTime startTime; private OffsetDateTime endTime; diff --git a/divinespark-backend/src/main/java/com/divinespark/service/AuthService.java b/divinespark-backend/src/main/java/com/divinespark/service/AuthService.java index 74e644f..d1c1d08 100644 --- a/divinespark-backend/src/main/java/com/divinespark/service/AuthService.java +++ b/divinespark-backend/src/main/java/com/divinespark/service/AuthService.java @@ -19,11 +19,10 @@ public class AuthService { private final UserRepository userRepository; - private final OtpRepository otpRepository; // โœ… ADD THIS + private final OtpRepository otpRepository; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; - // โœ… UPDATE CONSTRUCTOR public AuthService( UserRepository userRepository, OtpRepository otpRepository, @@ -43,7 +42,13 @@ public void register(RegisterRequest request) { throw new RuntimeException("Email is already in use"); } - if (!request.getContactNumber().matches("^[6-9]\\d{9}$")) { + String phone = request.getContactNumber().replaceAll("\\D", ""); + + if (phone.startsWith("91") && phone.length() == 12) { + phone = phone.substring(2); + } + + if (!phone.matches("^[6-9]\\d{9}$")) { throw new RuntimeException("Invalid contact number"); } @@ -51,14 +56,13 @@ public void register(RegisterRequest request) { .username(request.getUsername()) .email(request.getEmail()) .password(passwordEncoder.encode(request.getPassword())) - .contactNumber(request.getContactNumber()) + .contactNumber(phone) .role(Role.USER) .isActive(true) .build(); userRepository.save(user); } - // ================= LOGIN ================= public String login(LoginRequest request) {