Skip to content

Commit cfab158

Browse files
committed
fix: first name in email
1 parent 9597bee commit cfab158

File tree

6 files changed

+74
-63
lines changed

6 files changed

+74
-63
lines changed

packages/integrations/src/knowledge.md

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,35 +24,13 @@ To add a new integration (e.g., for a service like Stripe, or another email prov
2424
## Current Integrations
2525

2626
### Loops
27-
- **Purpose**: Handles sending transactional emails via the Loops.so API.
27+
- **Purpose**: Handles sending transactional emails (e.g., invitations, welcome messages) via the Loops.so API.
2828
- **Location**: `packages/integrations/src/loops/`
29-
- **Key Functions**:
30-
- `sendOrganizationInvitationEmail(data: LoopsEmailData)`
31-
- `sendOrganizationWelcomeEmail(data: LoopsEmailData)`
32-
- `sendBasicEmail(email: string, data: { subject: string, message: string })`
29+
- **Key Functions**: Provides functions for sending various types of pre-defined and basic emails.
3330
- **Environment Variables Required**:
3431
- `LOOPS_API_KEY`: Your API key for Loops.so.
3532

36-
## Usage Examples
37-
38-
To use an integration in another package (e.g., `backend` or `web`):
39-
40-
1. Ensure the `packages/integrations` package is listed as a dependency in the `package.json` of the consuming package (using `workspace:*`).
41-
2. Import the required functions or types:
42-
43-
```typescript
44-
import { sendOrganizationInvitationEmail, LoopsEmailData } from '@codebuff/integrations';
45-
46-
// ... later in your code
47-
const emailData: LoopsEmailData = {
48-
email: 'user@example.com',
49-
organizationName: 'Awesome Org',
50-
// ... other data
51-
};
52-
await sendOrganizationInvitationEmail(emailData);
53-
```
54-
5533
## Best Practices
56-
- Keep integration logic focused solely on interacting with the third-party service.
57-
- Avoid including business logic specific to `backend` or `web` within this package.
34+
- Keep external integration logic focused solely on interacting with the third-party service.
35+
- Avoid including business logic specific to `backend` or `web` within this package when dealing with external services.
5836
- Use the shared `logger` from the `common` package for logging.

packages/integrations/src/loops/client.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { logger } from 'common/src/util/logger'
2-
import { LoopsClient, APIError } from 'loops' // Import LoopsClient and APIError
2+
import { LoopsClient, APIError } from 'loops'
3+
import db from 'common/db'
4+
import * as schema from 'common/db/schema'
5+
import { eq } from 'drizzle-orm'
36

47
import type { LoopsEmailData, SendEmailResult } from './types'
58

@@ -114,17 +117,40 @@ export async function sendSignupEventToLoops(
114117
}
115118

116119
export async function sendOrganizationInvitationEmail(
117-
data: LoopsEmailData
120+
data: LoopsEmailData // data no longer contains firstName
118121
): Promise<SendEmailResult> {
122+
let lookedUpFirstName: string = 'there' // Default to 'there'
123+
try {
124+
const inviteeUserRecord = await db
125+
.select({ name: schema.user.name })
126+
.from(schema.user)
127+
.where(eq(schema.user.email, data.email.toLowerCase())) // Compare email case-insensitively
128+
.limit(1)
129+
130+
if (inviteeUserRecord.length > 0 && inviteeUserRecord[0].name) {
131+
lookedUpFirstName = inviteeUserRecord[0].name.split(' ')[0] || 'there'
132+
}
133+
} catch (error) {
134+
logger.error(
135+
{
136+
email: data.email,
137+
error,
138+
source: 'sendOrganizationInvitationEmail-lookup',
139+
},
140+
'Error fetching user by email for invitation, using default name.'
141+
)
142+
// Continue with default name 'there'
143+
}
144+
119145
return sendTransactionalEmail(
120146
ORGANIZATION_INVITATION_TRANSACTIONAL_ID,
121147
data.email,
122148
{
123-
firstName: data.firstName || '',
124-
organizationName: data.organizationName || '',
125-
inviterName: data.inviterName || '',
126-
invitationUrl: data.invitationUrl || '',
127-
role: data.role || 'member',
149+
firstName: lookedUpFirstName, // Use the looked-up or default name
150+
organizationName: data.organizationName || '', // data.organizationName is still expected
151+
inviterName: data.inviterName || '', // data.inviterName is still expected
152+
invitationUrl: data.invitationUrl || '', // data.invitationUrl is still expected
153+
role: data.role || 'member', // data.role is still expected
128154
}
129155
)
130156
}
Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
// Loops related types will go here
22

33
export interface LoopsEmailData {
4-
email: string;
5-
firstName?: string;
6-
lastName?: string;
7-
organizationName?: string;
8-
inviterName?: string;
9-
invitationUrl?: string;
10-
role?: string;
4+
email: string
5+
lastName?: string
6+
organizationName: string
7+
inviterName: string
8+
invitationUrl: string
9+
role?: string
1110
// For generic subject/message emails
12-
subject?: string;
13-
message?: string;
11+
subject?: string
12+
message?: string
1413
}
1514

1615
export interface LoopsResponse {
17-
success: boolean;
18-
id?: string;
19-
message?: string;
16+
success: boolean
17+
id?: string
18+
message?: string
2019
}
2120

2221
export interface SendEmailResult {
23-
success: boolean;
24-
error?: string;
25-
loopsId?: string;
22+
success: boolean
23+
error?: string
24+
loopsId?: string
2625
}

web/src/app/api/orgs/[orgId]/invitations/[email]/resend/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
7373
.where(eq(schema.user.id, session.user.id))
7474
.limit(1)
7575

76-
// Resend invitation email
76+
// Send invitation email
7777
const invitationUrl = `${request.nextUrl.origin}/invites/${inviteRecord.token}`
7878
const emailResult = await sendOrganizationInvitationEmail({
7979
email: inviteRecord.email,

web/src/app/api/orgs/[orgId]/invitations/bulk/route.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
4646
const body: BulkInviteRequest = await request.json()
4747

4848
// Validate input
49-
if (!body.invitations || !Array.isArray(body.invitations) || body.invitations.length === 0) {
49+
if (
50+
!body.invitations ||
51+
!Array.isArray(body.invitations) ||
52+
body.invitations.length === 0
53+
) {
5054
return NextResponse.json(
5155
{ error: 'Invitations array is required and must not be empty' },
5256
{ status: 400 }
@@ -92,7 +96,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
9296
const { organization } = permissionResult
9397

9498
// Get all emails to check for existing members and invitations
95-
const emails = body.invitations.map(inv => inv.email)
99+
const emails = body.invitations.map((inv) => inv.email)
96100

97101
// Check for existing members
98102
const existingMembers = await db
@@ -106,7 +110,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
106110
)
107111
)
108112

109-
const existingMemberEmails = new Set(existingMembers.map(m => m.email))
113+
const existingMemberEmails = new Set(existingMembers.map((m) => m.email))
110114

111115
// Check for existing pending invitations
112116
const existingInvitations = await db
@@ -120,7 +124,9 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
120124
)
121125
)
122126

123-
const existingInvitationEmails = new Set(existingInvitations.map(i => i.email))
127+
const existingInvitationEmails = new Set(
128+
existingInvitations.map((i) => i.email)
129+
)
124130

125131
// Get inviter information
126132
const inviter = await db
@@ -143,7 +149,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
143149
results.push({
144150
email: invitation.email,
145151
success: false,
146-
error: 'User is already a member of this organization'
152+
error: 'User is already a member of this organization',
147153
})
148154
failed++
149155
continue
@@ -154,7 +160,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
154160
results.push({
155161
email: invitation.email,
156162
success: false,
157-
error: 'Invitation already sent to this email'
163+
error: 'Invitation already sent to this email',
158164
})
159165
failed++
160166
continue
@@ -177,6 +183,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
177183
})
178184
.returning()
179185

186+
187+
180188
// Send invitation email
181189
const invitationUrl = `${request.nextUrl.origin}/invites/${token}`
182190
const emailResult = await sendOrganizationInvitationEmail({
@@ -196,7 +204,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
196204
results.push({
197205
email: invitation.email,
198206
success: false,
199-
error: 'Failed to send invitation email'
207+
error: 'Failed to send invitation email',
200208
})
201209
failed++
202210
continue
@@ -205,20 +213,19 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
205213
results.push({
206214
email: invitation.email,
207215
success: true,
208-
invitationId: inviteRecord.id
216+
invitationId: inviteRecord.id,
209217
})
210218
successful++
211-
212219
} catch (error) {
213220
logger.error(
214221
{ organizationId: orgId, email: invitation.email, error },
215222
'Error processing bulk invitation'
216223
)
217-
224+
218225
results.push({
219226
email: invitation.email,
220227
success: false,
221-
error: 'Internal error processing invitation'
228+
error: 'Internal error processing invitation',
222229
})
223230
failed++
224231
}
@@ -242,7 +249,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
242249
total: body.invitations.length,
243250
successful,
244251
failed,
245-
}
252+
},
246253
}
247254

248255
return NextResponse.json(response)
@@ -256,4 +263,4 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
256263
{ status: 500 }
257264
)
258265
}
259-
}
266+
}

web/src/app/api/orgs/[orgId]/invitations/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import db from 'common/db'
55
import * as schema from 'common/db/schema'
66
import { eq, and, isNull } from 'drizzle-orm'
77
import { checkOrganizationPermission } from '@/lib/organization-permissions'
8-
import { sendOrganizationInvitationEmail } from '@codebuff/integrations' // Updated import
8+
import { sendOrganizationInvitationEmail } from '@codebuff/integrations'
99
import { logger } from '@/util/logger'
1010
import crypto from 'crypto'
1111

@@ -16,6 +16,7 @@ interface RouteParams {
1616
interface InviteRequest {
1717
email: string
1818
role: 'admin' | 'member'
19+
// Removed firstName, as we will fetch it from the DB or default it
1920
}
2021

2122
export async function POST(request: NextRequest, { params }: RouteParams) {

0 commit comments

Comments
 (0)