Skip to content

Commit 50dff74

Browse files
committed
chore: merge main into release for new releases
2 parents 0960cc8 + 97cebf8 commit 50dff74

5 files changed

Lines changed: 567 additions & 7 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as React from 'react';
2+
import {
3+
Body,
4+
Button,
5+
Container,
6+
Font,
7+
Heading,
8+
Html,
9+
Link,
10+
Preview,
11+
Section,
12+
Tailwind,
13+
Text,
14+
} from '@react-email/components';
15+
import { Footer } from '../components/footer';
16+
import { Logo } from '../components/logo';
17+
import { getUnsubscribeUrl } from '@trycompai/email';
18+
19+
interface Props {
20+
toName: string;
21+
toEmail: string;
22+
taskTitle: string;
23+
failedCount: number;
24+
totalCount: number;
25+
taskStatusChanged: boolean;
26+
organizationName: string;
27+
taskUrl: string;
28+
}
29+
30+
export const AutomationFailuresEmail = ({
31+
toName,
32+
toEmail,
33+
taskTitle,
34+
failedCount,
35+
totalCount,
36+
taskStatusChanged,
37+
organizationName,
38+
taskUrl,
39+
}: Props) => {
40+
const unsubscribeUrl = getUnsubscribeUrl(toEmail);
41+
42+
return (
43+
<Html>
44+
<Tailwind>
45+
<head>
46+
<Font
47+
fontFamily="Geist"
48+
fallbackFontFamily="Helvetica"
49+
fontWeight={400}
50+
fontStyle="normal"
51+
/>
52+
<Font
53+
fontFamily="Geist"
54+
fallbackFontFamily="Helvetica"
55+
fontWeight={500}
56+
fontStyle="normal"
57+
/>
58+
</head>
59+
<Preview>
60+
{`${failedCount} of ${totalCount} automation(s) failed on task "${taskTitle}"`}
61+
</Preview>
62+
63+
<Body className="mx-auto my-auto bg-[#fff] font-sans">
64+
<Container
65+
className="mx-auto my-[40px] max-w-[600px] border-transparent p-[20px] md:border-[#E8E7E1]"
66+
style={{ borderStyle: 'solid', borderWidth: 1 }}
67+
>
68+
<Logo />
69+
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-[#121212]">
70+
Automation Failures
71+
</Heading>
72+
73+
<Text className="text-[14px] leading-[24px] text-[#121212]">
74+
Hello {toName},
75+
</Text>
76+
77+
<Text className="text-[14px] leading-[24px] text-[#121212]">
78+
<strong>{failedCount}</strong> of <strong>{totalCount}</strong> automation(s)
79+
failed on task <strong>"{taskTitle}"</strong> in{' '}
80+
<strong>{organizationName}</strong>.
81+
</Text>
82+
83+
{taskStatusChanged && (
84+
<Text className="text-[14px] leading-[24px] text-[#121212]">
85+
Task status has been changed to <strong>Failed</strong>.
86+
</Text>
87+
)}
88+
89+
<Section className="mt-[32px] mb-[32px] text-center">
90+
<Button
91+
className="rounded-[3px] bg-[#121212] px-[20px] py-[12px] text-center text-[14px] font-semibold text-white no-underline"
92+
href={taskUrl}
93+
>
94+
View Task
95+
</Button>
96+
</Section>
97+
98+
<Text className="text-[14px] leading-[24px] text-[#121212]">
99+
or copy and paste this URL into your browser:{' '}
100+
<a href={taskUrl} className="text-[#121212] underline">
101+
{taskUrl}
102+
</a>
103+
</Text>
104+
105+
<Section className="mt-[30px] mb-[20px]">
106+
<Text className="text-[12px] leading-[20px] text-[#666666]">
107+
Don't want to receive task assignment notifications?{' '}
108+
<Link href={unsubscribeUrl} className="text-[#121212] underline">
109+
Manage your email preferences
110+
</Link>
111+
.
112+
</Text>
113+
</Section>
114+
115+
<br />
116+
117+
<Footer />
118+
</Container>
119+
</Body>
120+
</Tailwind>
121+
</Html>
122+
);
123+
};
124+
125+
export default AutomationFailuresEmail;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
Body,
3+
Controller,
4+
HttpCode,
5+
InternalServerErrorException,
6+
Logger,
7+
Post,
8+
UseGuards,
9+
} from '@nestjs/common';
10+
import { ApiHeader, ApiOperation, ApiProperty, ApiResponse, ApiTags } from '@nestjs/swagger';
11+
import { TaskStatus } from '@db';
12+
import { IsBoolean, IsEnum, IsInt, IsString, Min } from 'class-validator';
13+
import { InternalTokenGuard } from '../auth/internal-token.guard';
14+
import { TaskNotifierService } from './task-notifier.service';
15+
16+
const TaskStatusValues = Object.values(TaskStatus);
17+
18+
class NotifyAutomationFailuresDto {
19+
@ApiProperty({ description: 'Organization ID' })
20+
@IsString()
21+
organizationId: string;
22+
23+
@ApiProperty({ description: 'Task ID' })
24+
@IsString()
25+
taskId: string;
26+
27+
@ApiProperty({ description: 'Task title' })
28+
@IsString()
29+
taskTitle: string;
30+
31+
@ApiProperty({ description: 'Number of failed automations' })
32+
@IsInt()
33+
@Min(1)
34+
failedCount: number;
35+
36+
@ApiProperty({ description: 'Total number of automations' })
37+
@IsInt()
38+
@Min(1)
39+
totalCount: number;
40+
41+
@ApiProperty({ description: 'Whether task status was changed to failed' })
42+
@IsBoolean()
43+
taskStatusChanged: boolean;
44+
}
45+
46+
class NotifyStatusChangeDto {
47+
@ApiProperty({ description: 'Organization ID' })
48+
@IsString()
49+
organizationId: string;
50+
51+
@ApiProperty({ description: 'Task ID' })
52+
@IsString()
53+
taskId: string;
54+
55+
@ApiProperty({ description: 'Task title' })
56+
@IsString()
57+
taskTitle: string;
58+
59+
@ApiProperty({ description: 'Previous task status', enum: TaskStatusValues })
60+
@IsEnum(TaskStatusValues)
61+
oldStatus: TaskStatus;
62+
63+
@ApiProperty({ description: 'New task status', enum: TaskStatusValues })
64+
@IsEnum(TaskStatusValues)
65+
newStatus: TaskStatus;
66+
}
67+
68+
@ApiTags('Internal - Tasks')
69+
@Controller({ path: 'internal/tasks', version: '1' })
70+
@UseGuards(InternalTokenGuard)
71+
@ApiHeader({
72+
name: 'X-Internal-Token',
73+
description: 'Internal service token (required in production)',
74+
required: false,
75+
})
76+
export class InternalTaskNotificationController {
77+
private readonly logger = new Logger(InternalTaskNotificationController.name);
78+
79+
constructor(private readonly taskNotifierService: TaskNotifierService) {}
80+
81+
@Post('notify-status-change')
82+
@HttpCode(200)
83+
@ApiOperation({
84+
summary:
85+
'Send task status change notifications (email + in-app) without a user actor (internal)',
86+
})
87+
@ApiResponse({ status: 200, description: 'Notifications sent' })
88+
@ApiResponse({ status: 500, description: 'Notification delivery failed' })
89+
async notifyStatusChange(@Body() body: NotifyStatusChangeDto) {
90+
this.logger.log(
91+
`[notifyStatusChange] Received request for task ${body.taskId} (${body.oldStatus} -> ${body.newStatus})`,
92+
);
93+
94+
try {
95+
await this.taskNotifierService.notifyStatusChange({
96+
organizationId: body.organizationId,
97+
taskId: body.taskId,
98+
taskTitle: body.taskTitle,
99+
oldStatus: body.oldStatus,
100+
newStatus: body.newStatus,
101+
});
102+
103+
return { success: true };
104+
} catch (error) {
105+
this.logger.error(
106+
`[notifyStatusChange] Failed for task ${body.taskId}:`,
107+
error instanceof Error ? error.message : 'Unknown error',
108+
);
109+
110+
throw new InternalServerErrorException('Failed to send notifications');
111+
}
112+
}
113+
114+
@Post('notify-automation-failures')
115+
@HttpCode(200)
116+
@ApiOperation({
117+
summary:
118+
'Send automation failure notifications (email + in-app) when one or more automations fail (internal)',
119+
})
120+
@ApiResponse({ status: 200, description: 'Notifications sent' })
121+
@ApiResponse({ status: 500, description: 'Notification delivery failed' })
122+
async notifyAutomationFailures(@Body() body: NotifyAutomationFailuresDto) {
123+
this.logger.log(
124+
`[notifyAutomationFailures] Received request for task ${body.taskId} (${body.failedCount}/${body.totalCount} failed, statusChanged=${body.taskStatusChanged})`,
125+
);
126+
127+
try {
128+
await this.taskNotifierService.notifyAutomationFailures({
129+
organizationId: body.organizationId,
130+
taskId: body.taskId,
131+
taskTitle: body.taskTitle,
132+
failedCount: body.failedCount,
133+
totalCount: body.totalCount,
134+
taskStatusChanged: body.taskStatusChanged,
135+
});
136+
137+
return { success: true };
138+
} catch (error) {
139+
this.logger.error(
140+
`[notifyAutomationFailures] Failed for task ${body.taskId}:`,
141+
error instanceof Error ? error.message : 'Unknown error',
142+
);
143+
144+
throw new InternalServerErrorException('Failed to send notifications');
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)