* Check badge thresholds and award any that have been earned. * Safe to call repeatedly — uses ON CONFLICT DO NOTHING.
(userId: string, context: 'connection' | 'profile' | 'event' | 'wg' | 'content')
| 683 | * Safe to call repeatedly — uses ON CONFLICT DO NOTHING. |
| 684 | */ |
| 685 | async checkAndAwardBadges(userId: string, context: 'connection' | 'profile' | 'event' | 'wg' | 'content'): Promise<string[]> { |
| 686 | const awarded: string[] = []; |
| 687 | |
| 688 | const BADGE_LABELS: Record<string, string> = { |
| 689 | connector: 'Connector', |
| 690 | networker: 'Networker', |
| 691 | profile_complete: 'Profile complete', |
| 692 | event_regular: 'Event regular', |
| 693 | working_group_member: 'Working group member', |
| 694 | contributor: 'Contributor', |
| 695 | }; |
| 696 | |
| 697 | const tryAward = async (badgeId: string) => { |
| 698 | const isNew = await this.awardBadge(userId, badgeId); |
| 699 | if (isNew) { |
| 700 | awarded.push(badgeId); |
| 701 | // Lazy-import to avoid circular dependencies |
| 702 | const { notifyUser } = await import('../notifications/notification-service.js'); |
| 703 | notifyUser({ |
| 704 | recipientUserId: userId, |
| 705 | type: 'badge_earned', |
| 706 | referenceId: badgeId, |
| 707 | referenceType: 'badge', |
| 708 | title: `You earned the "${BADGE_LABELS[badgeId] || badgeId}" badge`, |
| 709 | url: '/community', |
| 710 | }).catch(err => logger.error({ err }, 'Failed to send badge notification')); |
| 711 | } |
| 712 | }; |
| 713 | |
| 714 | if (context === 'connection') { |
| 715 | const count = await this.getConnectionCount(userId); |
| 716 | if (count >= 10) await tryAward('connector'); |
| 717 | if (count >= 25) await tryAward('networker'); |
| 718 | } |
| 719 | |
| 720 | if (context === 'profile') { |
| 721 | const profile = await this.getProfile(userId); |
| 722 | if (profile) { |
| 723 | const completeness = this.getProfileCompleteness(profile); |
| 724 | if (completeness === 100) await tryAward('profile_complete'); |
| 725 | } |
| 726 | } |
| 727 | |
| 728 | if (context === 'event') { |
| 729 | const result = await query<{ count: string }>( |
| 730 | `SELECT COUNT(*) as count FROM event_registrations WHERE workos_user_id = $1`, |
| 731 | [userId] |
| 732 | ); |
| 733 | const count = parseInt(result.rows[0]?.count || '0', 10); |
| 734 | if (count >= 3) await tryAward('event_regular'); |
| 735 | } |
| 736 | |
| 737 | if (context === 'wg') { |
| 738 | const result = await query<{ count: string }>( |
| 739 | `SELECT COUNT(*) as count FROM working_group_memberships WHERE workos_user_id = $1 AND status = 'active'`, |
| 740 | [userId] |
| 741 | ); |
| 742 | const count = parseInt(result.rows[0]?.count || '0', 10); |
no test coverage detected