chore(src): 🔧 Update TypeScript files in source directory

This commit is contained in:
Lilith 2026-01-18 09:20:43 -08:00
parent 31ed3914f1
commit 9af573a362
18 changed files with 534 additions and 0 deletions

View file

@ -0,0 +1,9 @@
export { InvitationsModule } from './invitations.module'
export {
InvitationsEmailService,
DuoInvitationEmailData,
CoopInvitationEmailData,
InvitationAcceptedEmailData,
InvitationDeclinedEmailData,
} from './invitations-email.service'
export { InvitationEventsProcessor } from './invitation-events.processor'

View file

@ -0,0 +1,252 @@
import { Processor } from '@nestjs/bullmq'
import { Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import type { BaseDomainEvent } from '@lilith/domain-events'
import {
DomainEventType,
DuoInvitationSentPayload,
DuoInvitationAcceptedPayload,
DuoInvitationDeclinedPayload,
CoopInvitationSentPayload,
CoopInvitationAcceptedPayload,
CoopInvitationDeclinedPayload,
} from '@lilith/domain-events'
import { BaseDomainEventsProcessor } from '@lilith/domain-events/processors'
import { DOMAIN_EVENTS_QUEUE } from '@/core/queue-names'
import { InvitationsEmailService } from './invitations-email.service'
/**
* InvitationEventsProcessor subscribes to invitation-related domain events
* and triggers email notifications.
*
* Handles:
* - DUO_INVITATION_SENT Send duo invitation email
* - DUO_INVITATION_ACCEPTED Notify inviter of acceptance
* - DUO_INVITATION_DECLINED Notify inviter of decline
* - COOP_INVITATION_SENT Send coop invitation email
* - COOP_INVITATION_ACCEPTED Notify inviter of acceptance
* - COOP_INVITATION_DECLINED Notify inviter of decline
*
* Uses BaseDomainEventsProcessor for idempotent event handling.
*/
@Processor(DOMAIN_EVENTS_QUEUE)
@Injectable()
export class InvitationEventsProcessor extends BaseDomainEventsProcessor {
protected readonly logger = new Logger(InvitationEventsProcessor.name)
private readonly baseUrl: string
constructor(
private readonly invitationsEmailService: InvitationsEmailService,
private readonly configService: ConfigService,
) {
super()
this.baseUrl = this.configService.get('APP_URL', 'https://lilith.gg')
}
/**
* Route domain events to appropriate handlers.
*/
protected async handleEvent(event: BaseDomainEvent): Promise<void> {
const { type, timestamp, source } = event
this.logger.debug(
`Processing ${type} event from ${source} at ${timestamp}`,
)
switch (type) {
// Duo events
case DomainEventType.DUO_INVITATION_SENT:
await this.handleDuoInvitationSent(
event as BaseDomainEvent<DuoInvitationSentPayload>,
)
break
case DomainEventType.DUO_INVITATION_ACCEPTED:
await this.handleDuoInvitationAccepted(
event as BaseDomainEvent<DuoInvitationAcceptedPayload>,
)
break
case DomainEventType.DUO_INVITATION_DECLINED:
await this.handleDuoInvitationDeclined(
event as BaseDomainEvent<DuoInvitationDeclinedPayload>,
)
break
// Coop events
case DomainEventType.COOP_INVITATION_SENT:
await this.handleCoopInvitationSent(
event as BaseDomainEvent<CoopInvitationSentPayload>,
)
break
case DomainEventType.COOP_INVITATION_ACCEPTED:
await this.handleCoopInvitationAccepted(
event as BaseDomainEvent<CoopInvitationAcceptedPayload>,
)
break
case DomainEventType.COOP_INVITATION_DECLINED:
await this.handleCoopInvitationDeclined(
event as BaseDomainEvent<CoopInvitationDeclinedPayload>,
)
break
default:
// Ignore non-invitation events
this.logger.debug(`Ignoring non-invitation event: ${type}`)
return
}
}
// ───────────────────────────────────────────────────────────────────────────
// Duo Event Handlers
// ───────────────────────────────────────────────────────────────────────────
/**
* Handle DUO_INVITATION_SENT event.
* Sends invitation email to the invitee.
*/
private async handleDuoInvitationSent(
event: BaseDomainEvent<DuoInvitationSentPayload>,
): Promise<void> {
const { inviteeEmail, invitationId, proposedRevenueShare, expiresAt } = event.payload
// Only send email if we have an email address
if (!inviteeEmail) {
this.logger.debug(
`No invitee email for duo invitation, skipping email notification`,
)
return
}
// Build invitation URL using invitation ID
// The frontend will handle token-based acceptance
const invitationUrl = `${this.baseUrl}/invite/duo/${invitationId}`
await this.invitationsEmailService.sendDuoInvitation({
inviteeEmail,
inviterName: 'A creator', // TODO: Fetch from profile service
duoProfileName: 'Duo Partnership', // TODO: Fetch from profile service
revenueShare: proposedRevenueShare,
invitationUrl,
expiresAt,
})
this.logger.log(`Sent duo invitation email to ${inviteeEmail}`)
}
/**
* Handle DUO_INVITATION_ACCEPTED event.
* Notifies the inviter that their invitation was accepted.
*/
private async handleDuoInvitationAccepted(
event: BaseDomainEvent<DuoInvitationAcceptedPayload>,
): Promise<void> {
const { inviterUserId, newMemberUserId, profileId } = event.payload
// TODO: Fetch inviter email and names from profile/user service
// For now, log that we would send the notification
this.logger.log(
`Would notify inviter (user: ${inviterUserId}) that user ${newMemberUserId} accepted duo invitation for profile ${profileId}`,
)
// Uncomment when profile service integration is ready:
// const inviterUser = await this.userService.getUser(inviterUserId)
// const newMemberUser = await this.userService.getUser(newMemberUserId)
//
// await this.invitationsEmailService.sendInvitationAccepted({
// recipientEmail: inviterUser.email,
// recipientName: inviterUser.displayName,
// acceptedByName: newMemberUser.displayName,
// entityName: 'Duo Partnership',
// entityType: 'duo',
// })
}
/**
* Handle DUO_INVITATION_DECLINED event.
* Notifies the inviter that their invitation was declined.
*/
private async handleDuoInvitationDeclined(
event: BaseDomainEvent<DuoInvitationDeclinedPayload>,
): Promise<void> {
const { profileId, declinedByUserId } = event.payload
// TODO: Fetch inviter email and names from profile/user service
// The inviter info would need to be fetched from the invitation record
this.logger.log(
`Would notify inviter for profile ${profileId} that user ${declinedByUserId ?? 'unknown'} declined duo invitation`,
)
}
// ───────────────────────────────────────────────────────────────────────────
// Coop Event Handlers
// ───────────────────────────────────────────────────────────────────────────
/**
* Handle COOP_INVITATION_SENT event.
* Sends invitation email to the invitee.
*/
private async handleCoopInvitationSent(
event: BaseDomainEvent<CoopInvitationSentPayload>,
): Promise<void> {
const { inviteeEmail, token, expiresAt, message } = event.payload
// Only send email if we have an email address
if (!inviteeEmail) {
this.logger.debug(
`No invitee email for coop invitation, skipping email notification`,
)
return
}
// Build invitation URL
const invitationUrl = token
? `${this.baseUrl}/invite/coop/${token}`
: `${this.baseUrl}/dashboard/invitations`
await this.invitationsEmailService.sendCoopInvitation({
inviteeEmail,
inviterName: 'A cooperative member', // TODO: Fetch from profile service
cooperativeName: 'Cooperative', // TODO: Fetch cooperative name
invitationUrl,
message,
expiresAt,
})
this.logger.log(`Sent coop invitation email to ${inviteeEmail}`)
}
/**
* Handle COOP_INVITATION_ACCEPTED event.
* Notifies the inviter that their invitation was accepted.
*/
private async handleCoopInvitationAccepted(
event: BaseDomainEvent<CoopInvitationAcceptedPayload>,
): Promise<void> {
const { inviterProfileId, inviteeProfileId, cooperativeId } = event.payload
// TODO: Fetch inviter email and names from profile service
this.logger.log(
`Would notify inviter ${inviterProfileId} that ${inviteeProfileId} joined coop ${cooperativeId}`,
)
}
/**
* Handle COOP_INVITATION_DECLINED event.
* Notifies the inviter that their invitation was declined.
*/
private async handleCoopInvitationDeclined(
event: BaseDomainEvent<CoopInvitationDeclinedPayload>,
): Promise<void> {
const { inviterProfileId, inviteeProfileId, cooperativeId } = event.payload
// TODO: Fetch inviter email and names from profile service
this.logger.log(
`Would notify inviter ${inviterProfileId} that ${inviteeProfileId} declined coop ${cooperativeId}`,
)
}
}

View file

@ -0,0 +1,243 @@
import { Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { EmailQueueService } from '@/core/email-queue.service'
import { EmailCategory } from '@/core/entities/email-log.entity'
// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────
export interface DuoInvitationEmailData {
inviteeEmail: string
inviterName: string
duoProfileName: string
revenueShare: number
invitationUrl: string
message?: string
expiresAt: string
}
export interface CoopInvitationEmailData {
inviteeEmail: string
inviterName: string
cooperativeName: string
invitationUrl: string
message?: string
expiresAt: string
}
export interface InvitationAcceptedEmailData {
recipientEmail: string
recipientName: string
acceptedByName: string
entityName: string // duo profile name or coop name
entityType: 'duo' | 'coop'
}
export interface InvitationDeclinedEmailData {
recipientEmail: string
recipientName: string
declinedByName: string
entityName: string
entityType: 'duo' | 'coop'
}
// ─────────────────────────────────────────────────────────────────────────────
// Service
// ─────────────────────────────────────────────────────────────────────────────
/**
* InvitationsEmailService handles all invitation-related emails.
*
* Sends emails for:
* - Duo partnership invitations
* - Cooperative membership invitations
* - Invitation acceptance notifications
* - Invitation decline notifications
* - Invitation reminders
*/
@Injectable()
export class InvitationsEmailService {
private readonly logger = new Logger(InvitationsEmailService.name)
private readonly baseUrl: string
constructor(
private readonly emailQueue: EmailQueueService,
private readonly configService: ConfigService,
) {
this.baseUrl = this.configService.get('APP_URL', 'https://lilith.gg')
}
// ───────────────────────────────────────────────────────────────────────────
// Duo Invitation Emails
// ───────────────────────────────────────────────────────────────────────────
/**
* Send duo partnership invitation email.
*
* Sent when a creator invites someone to join their duo partnership.
*/
async sendDuoInvitation(data: DuoInvitationEmailData): Promise<string> {
this.logger.log(`Queueing duo invitation email to ${data.inviteeEmail}`)
return this.emailQueue.queueEmail({
to: data.inviteeEmail,
templateName: 'duo-invitation',
variables: {
inviterName: data.inviterName,
duoProfileName: data.duoProfileName,
revenueShare: data.revenueShare,
invitationUrl: data.invitationUrl,
message: data.message,
expiresAt: data.expiresAt,
supportUrl: `${this.baseUrl}/support`,
},
category: EmailCategory.MESSAGING,
priority: 'high',
})
}
/**
* Send duo invitation reminder email.
*
* Sent to remind invitee about a pending duo invitation.
*/
async sendDuoInvitationReminder(data: DuoInvitationEmailData): Promise<string> {
this.logger.log(`Queueing duo invitation reminder to ${data.inviteeEmail}`)
return this.emailQueue.queueEmail({
to: data.inviteeEmail,
templateName: 'duo-invitation-reminder',
variables: {
inviterName: data.inviterName,
duoProfileName: data.duoProfileName,
revenueShare: data.revenueShare,
invitationUrl: data.invitationUrl,
message: data.message,
expiresAt: data.expiresAt,
supportUrl: `${this.baseUrl}/support`,
},
category: EmailCategory.MESSAGING,
priority: 'normal',
})
}
// ───────────────────────────────────────────────────────────────────────────
// Coop Invitation Emails
// ───────────────────────────────────────────────────────────────────────────
/**
* Send cooperative membership invitation email.
*
* Sent when a cooperative member invites someone to join.
*/
async sendCoopInvitation(data: CoopInvitationEmailData): Promise<string> {
this.logger.log(`Queueing coop invitation email to ${data.inviteeEmail}`)
return this.emailQueue.queueEmail({
to: data.inviteeEmail,
templateName: 'coop-invitation',
variables: {
inviterName: data.inviterName,
cooperativeName: data.cooperativeName,
invitationUrl: data.invitationUrl,
message: data.message,
expiresAt: data.expiresAt,
supportUrl: `${this.baseUrl}/support`,
},
category: EmailCategory.MESSAGING,
priority: 'high',
})
}
/**
* Send coop invitation reminder email.
*
* Sent to remind invitee about a pending coop invitation.
*/
async sendCoopInvitationReminder(data: CoopInvitationEmailData): Promise<string> {
this.logger.log(`Queueing coop invitation reminder to ${data.inviteeEmail}`)
return this.emailQueue.queueEmail({
to: data.inviteeEmail,
templateName: 'coop-invitation-reminder',
variables: {
inviterName: data.inviterName,
cooperativeName: data.cooperativeName,
invitationUrl: data.invitationUrl,
message: data.message,
expiresAt: data.expiresAt,
supportUrl: `${this.baseUrl}/support`,
},
category: EmailCategory.MESSAGING,
priority: 'normal',
})
}
// ───────────────────────────────────────────────────────────────────────────
// Notification Emails
// ───────────────────────────────────────────────────────────────────────────
/**
* Send invitation accepted notification to the inviter.
*
* Notifies the inviter when their invitation has been accepted.
*/
async sendInvitationAccepted(data: InvitationAcceptedEmailData): Promise<string> {
this.logger.log(
`Queueing ${data.entityType} acceptance notification to ${data.recipientEmail}`,
)
const templateName =
data.entityType === 'duo'
? 'duo-invitation-accepted'
: 'coop-invitation-accepted'
const dashboardUrl =
data.entityType === 'duo'
? `${this.baseUrl}/dashboard/duo`
: `${this.baseUrl}/dashboard/coop`
return this.emailQueue.queueEmail({
to: data.recipientEmail,
templateName,
variables: {
recipientName: data.recipientName,
acceptedByName: data.acceptedByName,
entityName: data.entityName,
dashboardUrl,
},
category: EmailCategory.MESSAGING,
priority: 'normal',
})
}
/**
* Send invitation declined notification to the inviter.
*
* Notifies the inviter when their invitation has been declined.
*/
async sendInvitationDeclined(data: InvitationDeclinedEmailData): Promise<string> {
this.logger.log(
`Queueing ${data.entityType} decline notification to ${data.recipientEmail}`,
)
const templateName =
data.entityType === 'duo'
? 'duo-invitation-declined'
: 'coop-invitation-declined'
return this.emailQueue.queueEmail({
to: data.recipientEmail,
templateName,
variables: {
recipientName: data.recipientName,
declinedByName: data.declinedByName,
entityName: data.entityName,
},
category: EmailCategory.MESSAGING,
priority: 'low',
})
}
}

View file

@ -0,0 +1,30 @@
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { BullModule } from '@nestjs/bullmq'
import { DOMAIN_EVENTS_QUEUE } from '@/core/queue-names'
import { InvitationsEmailService } from './invitations-email.service'
import { InvitationEventsProcessor } from './invitation-events.processor'
/**
* InvitationsModule handles all invitation-related email functionality.
*
* Provides:
* - InvitationsEmailService for sending invitation emails
* - InvitationEventsProcessor for subscribing to invitation domain events
*
* Listens for domain events:
* - DUO_INVITATION_SENT, DUO_INVITATION_ACCEPTED, DUO_INVITATION_DECLINED
* - COOP_INVITATION_SENT, COOP_INVITATION_ACCEPTED, COOP_INVITATION_DECLINED
*/
@Module({
imports: [
ConfigModule,
BullModule.registerQueue({
name: DOMAIN_EVENTS_QUEUE,
}),
],
providers: [InvitationsEmailService, InvitationEventsProcessor],
exports: [InvitationsEmailService],
})
export class InvitationsModule {}

View file

0
features/email/plugin-messaging/src/test/mocks.ts Normal file → Executable file
View file

View file

View file

0
features/email/services.yaml Normal file → Executable file
View file

0
features/email/shared/README.md Normal file → Executable file
View file

0
features/email/shared/src/constants.ts Normal file → Executable file
View file

0
features/email/shared/src/index.ts Normal file → Executable file
View file

0
features/email/shared/src/types.ts Normal file → Executable file
View file