Skip to content

Commit 35a05d0

Browse files
committed
feat(badge): add job post and job application badges
1 parent e6fedbd commit 35a05d0

File tree

12 files changed

+288
-5
lines changed

12 files changed

+288
-5
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.bounswe.jobboardbackend.badge.event;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
/**
7+
* Event published when a job application is approved by an employer.
8+
* Used to trigger badge checks for job acceptance badges.
9+
*/
10+
@Getter
11+
@AllArgsConstructor
12+
public class JobApplicationApprovedEvent {
13+
private final Long jobSeekerId;
14+
private final Long applicationId;
15+
}
16+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.bounswe.jobboardbackend.badge.event;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
/**
7+
* Event published when a job seeker submits a new job application.
8+
* Used to trigger badge checks for job application badges.
9+
*/
10+
@Getter
11+
@AllArgsConstructor
12+
public class JobApplicationCreatedEvent {
13+
private final Long jobSeekerId;
14+
private final Long applicationId;
15+
}
16+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.bounswe.jobboardbackend.badge.event;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
/**
7+
* Event published when an employer creates a new job post.
8+
* Used to trigger badge checks for job posting badges.
9+
*/
10+
@Getter
11+
@AllArgsConstructor
12+
public class JobPostCreatedEvent {
13+
private final Long employerId;
14+
private final Long jobPostId;
15+
}
16+

apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/badge/listener/BadgeEventListener.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import org.bounswe.jobboardbackend.badge.event.CommentCreatedEvent;
66
import org.bounswe.jobboardbackend.badge.event.CommentUpvotedEvent;
77
import org.bounswe.jobboardbackend.badge.event.ForumPostCreatedEvent;
8+
import org.bounswe.jobboardbackend.badge.event.JobPostCreatedEvent;
9+
import org.bounswe.jobboardbackend.badge.event.JobApplicationCreatedEvent;
10+
import org.bounswe.jobboardbackend.badge.event.JobApplicationApprovedEvent;
811
import org.bounswe.jobboardbackend.badge.service.BadgeService;
912
import org.springframework.stereotype.Component;
1013
import org.springframework.transaction.event.TransactionPhase;
@@ -65,5 +68,51 @@ public void onCommentUpvoted(CommentUpvotedEvent event) {
6568
log.error("Badge check failed for upvote on comment by user {}: {}", event.getCommentAuthorId(), e.getMessage());
6669
}
6770
}
71+
72+
// ==================== JOB POST EVENTS ====================
73+
74+
/**
75+
* Handle job post creation - check for job posting badges.
76+
* Only executes after the transaction commits successfully.
77+
*/
78+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
79+
public void onJobPostCreated(JobPostCreatedEvent event) {
80+
try {
81+
log.debug("Job post created by employer {}, checking badges...", event.getEmployerId());
82+
badgeService.checkJobPostBadges(event.getEmployerId());
83+
} catch (Exception e) {
84+
log.error("Badge check failed for job post by employer {}: {}", event.getEmployerId(), e.getMessage());
85+
}
86+
}
87+
88+
// ==================== JOB APPLICATION EVENTS ====================
89+
90+
/**
91+
* Handle job application creation - check for job application badges.
92+
* Only executes after the transaction commits successfully.
93+
*/
94+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
95+
public void onJobApplicationCreated(JobApplicationCreatedEvent event) {
96+
try {
97+
log.debug("Job application created by job seeker {}, checking badges...", event.getJobSeekerId());
98+
badgeService.checkJobApplicationBadges(event.getJobSeekerId());
99+
} catch (Exception e) {
100+
log.error("Badge check failed for job application by job seeker {}: {}", event.getJobSeekerId(), e.getMessage());
101+
}
102+
}
103+
104+
/**
105+
* Handle job application approval - check for job acceptance badges.
106+
* Only executes after the transaction commits successfully.
107+
*/
108+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
109+
public void onJobApplicationApproved(JobApplicationApprovedEvent event) {
110+
try {
111+
log.debug("Job application approved for job seeker {}, checking badges...", event.getJobSeekerId());
112+
badgeService.checkJobAcceptanceBadges(event.getJobSeekerId());
113+
} catch (Exception e) {
114+
log.error("Badge check failed for job application approval for job seeker {}: {}", event.getJobSeekerId(), e.getMessage());
115+
}
116+
}
68117
}
69118

apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/badge/model/BadgeType.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,78 @@ public enum BadgeType {
7272
"⭐",
7373
"Receive 50 upvotes on your comments",
7474
50
75+
),
76+
77+
// ==================== JOB POST BADGES (Employer) ====================
78+
79+
FIRST_LISTING(
80+
"First Listing",
81+
"Posted your first job listing",
82+
"📋",
83+
"Create your first job post",
84+
1
85+
),
86+
ACTIVE_RECRUITER(
87+
"Active Recruiter",
88+
"Posted 5 job listings",
89+
"🎯",
90+
"Create 5 job posts",
91+
5
92+
),
93+
HIRING_MACHINE(
94+
"Hiring Machine",
95+
"Posted 15 job listings",
96+
"🏭",
97+
"Create 15 job posts",
98+
15
99+
),
100+
101+
// ==================== JOB APPLICATION BADGES (Job Seeker) ====================
102+
103+
FIRST_STEP(
104+
"First Step",
105+
"Submitted your first job application",
106+
"👣",
107+
"Apply to your first job",
108+
1
109+
),
110+
ACTIVE_SEEKER(
111+
"Active Seeker",
112+
"Submitted 5 job applications",
113+
"🔍",
114+
"Apply to 5 jobs",
115+
5
116+
),
117+
PERSISTENT(
118+
"Persistent",
119+
"Submitted 15 job applications",
120+
"💪",
121+
"Apply to 15 jobs",
122+
15
123+
),
124+
125+
// ==================== JOB ACCEPTANCE BADGES (Job Seeker) ====================
126+
127+
HIRED(
128+
"Hired!",
129+
"Got your first job offer",
130+
"🎉",
131+
"Get accepted for a job",
132+
1
133+
),
134+
IN_DEMAND(
135+
"In Demand",
136+
"Received 3 job offers",
137+
"🌟",
138+
"Get accepted for 3 jobs",
139+
3
140+
),
141+
CAREER_STAR(
142+
"Career Star",
143+
"Received 5 job offers",
144+
"🏆",
145+
"Get accepted for 5 jobs",
146+
5
75147
);
76148

77149
private final String displayName;

apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/badge/service/BadgeService.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import org.bounswe.jobboardbackend.forum.repository.ForumCommentRepository;
99
import org.bounswe.jobboardbackend.forum.repository.ForumCommentUpvoteRepository;
1010
import org.bounswe.jobboardbackend.forum.repository.ForumPostRepository;
11+
import org.bounswe.jobboardbackend.jobapplication.model.JobApplicationStatus;
12+
import org.bounswe.jobboardbackend.jobapplication.repository.JobApplicationRepository;
13+
import org.bounswe.jobboardbackend.jobpost.repository.JobPostRepository;
1114
import org.springframework.stereotype.Service;
1215
import org.springframework.transaction.annotation.Propagation;
1316
import org.springframework.transaction.annotation.Transactional;
@@ -25,6 +28,8 @@ public class BadgeService {
2528
private final ForumPostRepository forumPostRepository;
2629
private final ForumCommentRepository forumCommentRepository;
2730
private final ForumCommentUpvoteRepository forumCommentUpvoteRepository;
31+
private final JobPostRepository jobPostRepository;
32+
private final JobApplicationRepository jobApplicationRepository;
2833

2934
/**
3035
* Award a badge to a user if they don't already have it.
@@ -117,5 +122,73 @@ public void checkUpvoteBadges(Long userId) {
117122
awardBadge(userId, BadgeType.VALUABLE_CONTRIBUTOR);
118123
}
119124
}
125+
126+
// ==================== JOB POST BADGES (Employer) ====================
127+
128+
/**
129+
* Check if employer qualifies for any job posting badges and award them.
130+
* Called after an employer creates a new job post.
131+
*
132+
* @param employerId The employer's user ID
133+
*/
134+
@Transactional(propagation = Propagation.REQUIRES_NEW)
135+
public void checkJobPostBadges(Long employerId) {
136+
long jobPostCount = jobPostRepository.countByEmployerId(employerId);
137+
138+
if (jobPostCount >= BadgeType.FIRST_LISTING.getThreshold()) {
139+
awardBadge(employerId, BadgeType.FIRST_LISTING);
140+
}
141+
if (jobPostCount >= BadgeType.ACTIVE_RECRUITER.getThreshold()) {
142+
awardBadge(employerId, BadgeType.ACTIVE_RECRUITER);
143+
}
144+
if (jobPostCount >= BadgeType.HIRING_MACHINE.getThreshold()) {
145+
awardBadge(employerId, BadgeType.HIRING_MACHINE);
146+
}
147+
}
148+
149+
// ==================== JOB APPLICATION BADGES (Job Seeker) ====================
150+
151+
/**
152+
* Check if job seeker qualifies for any job application badges and award them.
153+
* Called after a job seeker submits a new application.
154+
*
155+
* @param jobSeekerId The job seeker's user ID
156+
*/
157+
@Transactional(propagation = Propagation.REQUIRES_NEW)
158+
public void checkJobApplicationBadges(Long jobSeekerId) {
159+
long applicationCount = jobApplicationRepository.countByJobSeekerId(jobSeekerId);
160+
161+
if (applicationCount >= BadgeType.FIRST_STEP.getThreshold()) {
162+
awardBadge(jobSeekerId, BadgeType.FIRST_STEP);
163+
}
164+
if (applicationCount >= BadgeType.ACTIVE_SEEKER.getThreshold()) {
165+
awardBadge(jobSeekerId, BadgeType.ACTIVE_SEEKER);
166+
}
167+
if (applicationCount >= BadgeType.PERSISTENT.getThreshold()) {
168+
awardBadge(jobSeekerId, BadgeType.PERSISTENT);
169+
}
170+
}
171+
172+
/**
173+
* Check if job seeker qualifies for any job acceptance badges and award them.
174+
* Called after a job application is approved by an employer.
175+
*
176+
* @param jobSeekerId The job seeker's user ID
177+
*/
178+
@Transactional(propagation = Propagation.REQUIRES_NEW)
179+
public void checkJobAcceptanceBadges(Long jobSeekerId) {
180+
long acceptedCount = jobApplicationRepository.countByJobSeekerIdAndStatus(
181+
jobSeekerId, JobApplicationStatus.APPROVED);
182+
183+
if (acceptedCount >= BadgeType.HIRED.getThreshold()) {
184+
awardBadge(jobSeekerId, BadgeType.HIRED);
185+
}
186+
if (acceptedCount >= BadgeType.IN_DEMAND.getThreshold()) {
187+
awardBadge(jobSeekerId, BadgeType.IN_DEMAND);
188+
}
189+
if (acceptedCount >= BadgeType.CAREER_STAR.getThreshold()) {
190+
awardBadge(jobSeekerId, BadgeType.CAREER_STAR);
191+
}
192+
}
120193
}
121194

apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/jobapplication/repository/JobApplicationRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@ public interface JobApplicationRepository extends JpaRepository<JobApplication,
1919

2020
// needed for stats
2121
long countByStatus(org.bounswe.jobboardbackend.jobapplication.model.JobApplicationStatus status);
22+
23+
// needed for badges
24+
long countByJobSeekerId(Long jobSeekerId);
25+
26+
long countByJobSeekerIdAndStatus(Long jobSeekerId, org.bounswe.jobboardbackend.jobapplication.model.JobApplicationStatus status);
2227
}

apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/jobapplication/service/JobApplicationService.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
import org.bounswe.jobboardbackend.workplace.service.WorkplaceService;
1616
import org.bounswe.jobboardbackend.workplace.repository.EmployerWorkplaceRepository;
1717
import org.bounswe.jobboardbackend.workplace.repository.WorkplaceRepository;
18+
import org.bounswe.jobboardbackend.badge.event.JobApplicationCreatedEvent;
19+
import org.bounswe.jobboardbackend.badge.event.JobApplicationApprovedEvent;
20+
import org.springframework.context.ApplicationEventPublisher;
1821
import org.springframework.beans.factory.annotation.Value;
1922
import org.springframework.security.access.AccessDeniedException;
2023
import org.springframework.security.access.prepost.PreAuthorize;
@@ -43,6 +46,7 @@ public class JobApplicationService {
4346
private final WorkplaceService workplaceService;
4447
private final EmployerWorkplaceRepository employerWorkplaceRepository;
4548
private final WorkplaceRepository workplaceRepository;
49+
private final ApplicationEventPublisher eventPublisher;
4650

4751
// === GCS config ===
4852
@Value("${app.gcs.bucket:bounswe-jobboard}")
@@ -65,13 +69,15 @@ public JobApplicationService(JobApplicationRepository applicationRepository,
6569
JobPostRepository jobPostRepository,
6670
WorkplaceService workplaceService,
6771
EmployerWorkplaceRepository employerWorkplaceRepository,
68-
WorkplaceRepository workplaceRepository) {
72+
WorkplaceRepository workplaceRepository,
73+
ApplicationEventPublisher eventPublisher) {
6974
this.applicationRepository = applicationRepository;
7075
this.userRepository = userRepository;
7176
this.jobPostRepository = jobPostRepository;
7277
this.workplaceService = workplaceService;
7378
this.employerWorkplaceRepository = employerWorkplaceRepository;
7479
this.workplaceRepository = workplaceRepository;
80+
this.eventPublisher = eventPublisher;
7581
}
7682

7783
@Transactional(readOnly = true)
@@ -138,7 +144,12 @@ public JobApplicationResponse create(CreateJobApplicationRequest dto) {
138144
.appliedDate(LocalDateTime.now())
139145
.build();
140146

141-
return toResponseDto(applicationRepository.save(application));
147+
JobApplication savedApplication = applicationRepository.save(application);
148+
149+
// Publish event for badge system
150+
eventPublisher.publishEvent(new JobApplicationCreatedEvent(jobSeeker.getId(), savedApplication.getId()));
151+
152+
return toResponseDto(savedApplication);
142153
}
143154

144155
@Transactional
@@ -158,7 +169,13 @@ public JobApplicationResponse approve(Long id, String feedback) {
158169
application.setFeedback(feedback);
159170
}
160171

161-
return toResponseDto(applicationRepository.save(application));
172+
JobApplication savedApplication = applicationRepository.save(application);
173+
174+
// Publish event for badge system
175+
eventPublisher.publishEvent(new JobApplicationApprovedEvent(
176+
application.getJobSeeker().getId(), savedApplication.getId()));
177+
178+
return toResponseDto(savedApplication);
162179
}
163180

164181
@Transactional

apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/jobpost/repository/JobPostRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,7 @@ List<JobPost> findFiltered(
4545

4646
long countByPostedDateAfter(java.time.LocalDateTime date);
4747

48+
// needed for badges
49+
long countByEmployerId(Long employerId);
50+
4851
}

0 commit comments

Comments
 (0)