@fufulog/brevomorphic-cms-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ import { Router } from 'express';
2
+ export class EmailTemplateController {
3
+ service;
4
+ constructor(service) {
5
+ this.service = service;
6
+ }
7
+ /**
8
+ * Generates a fully configured Express Router.
9
+ * Can be directly mounted into any Express app: `app.use('/api/cms', controller.getRouter())`
10
+ */
11
+ getRouter() {
12
+ const router = Router();
13
+ // Standard express mapping
14
+ router.get('/templates', this.listTemplates.bind(this));
15
+ router.get('/templates/:id', this.getTemplate.bind(this));
16
+ router.post('/templates', this.createTemplate.bind(this));
17
+ router.put('/templates/:id', this.updateTemplate.bind(this));
18
+ router.post('/templates/:id/toggle', this.toggleTemplate.bind(this));
19
+ router.get('/senders', this.listSenders.bind(this));
20
+ router.post('/send', this.sendEventEmail.bind(this));
21
+ return router;
22
+ }
23
+ /**
24
+ * GET /templates
25
+ * Lists all templates synchronized from Brevo and mapped locally.
26
+ */
27
+ async listTemplates(req, res) {
28
+ try {
29
+ const templates = await this.service.listTemplates();
30
+ res.status(200).json({ success: true, data: templates });
31
+ }
32
+ catch (error) {
33
+ res.status(500).json({ success: false, error: error.message });
34
+ }
35
+ }
36
+ /**
37
+ * GET /templates/:id
38
+ * Fetches a detailed template (including HTML body content) by Brevo ID.
39
+ */
40
+ async getTemplate(req, res) {
41
+ try {
42
+ const id = parseInt(req.params.id, 10);
43
+ if (isNaN(id)) {
44
+ res.status(400).json({ success: false, error: 'Invalid template ID parameter' });
45
+ return;
46
+ }
47
+ const template = await this.service.getTemplate(id);
48
+ res.status(200).json({ success: true, data: template });
49
+ }
50
+ catch (error) {
51
+ if (error.message.includes('not found') || error.message.includes('404')) {
52
+ res.status(404).json({ success: false, error: error.message });
53
+ }
54
+ else {
55
+ res.status(500).json({ success: false, error: error.message });
56
+ }
57
+ }
58
+ }
59
+ /**
60
+ * POST /templates
61
+ * Instantiates a new draft template on Brevo and registers local mapping.
62
+ */
63
+ async createTemplate(req, res) {
64
+ try {
65
+ const { name, subject, senderEmail, senderName } = req.body;
66
+ const newTemplate = await this.service.createTemplate({
67
+ name,
68
+ subject,
69
+ senderEmail,
70
+ senderName,
71
+ });
72
+ res.status(201).json({ success: true, data: newTemplate });
73
+ }
74
+ catch (error) {
75
+ res.status(500).json({ success: false, error: error.message });
76
+ }
77
+ }
78
+ /**
79
+ * PUT /templates/:id
80
+ * Validates and updates a template configuration both on Brevo and the local DB.
81
+ */
82
+ async updateTemplate(req, res) {
83
+ try {
84
+ const id = parseInt(req.params.id, 10);
85
+ if (isNaN(id)) {
86
+ res.status(400).json({ success: false, error: 'Invalid template ID parameter' });
87
+ return;
88
+ }
89
+ const { templateName, subject, sender, htmlContent, eventName, isActive } = req.body;
90
+ const updated = await this.service.updateTemplate(id, {
91
+ templateName,
92
+ subject,
93
+ sender,
94
+ htmlContent,
95
+ eventName,
96
+ isActive: !!isActive,
97
+ });
98
+ res.status(200).json({ success: true, data: updated });
99
+ }
100
+ catch (error) {
101
+ if (error.message.includes('cannot be blank') ||
102
+ error.message.includes('must exceed') ||
103
+ error.message.includes('greater than 10')) {
104
+ res.status(400).json({ success: false, error: error.message });
105
+ }
106
+ else {
107
+ res.status(500).json({ success: false, error: error.message });
108
+ }
109
+ }
110
+ }
111
+ /**
112
+ * POST /templates/:id/toggle
113
+ * Flips active/inactive status across Brevo and DB.
114
+ */
115
+ async toggleTemplate(req, res) {
116
+ try {
117
+ const id = parseInt(req.params.id, 10);
118
+ if (isNaN(id)) {
119
+ res.status(400).json({ success: false, error: 'Invalid template ID parameter' });
120
+ return;
121
+ }
122
+ const { isActive } = req.body;
123
+ if (isActive === undefined) {
124
+ res.status(400).json({ success: false, error: "Field 'isActive' boolean is required in request body" });
125
+ return;
126
+ }
127
+ await this.service.toggleTemplateActive(id, !!isActive);
128
+ res.status(200).json({ success: true, message: `Template status flipped to ${!!isActive}` });
129
+ }
130
+ catch (error) {
131
+ res.status(500).json({ success: false, error: error.message });
132
+ }
133
+ }
134
+ /**
135
+ * GET /senders
136
+ * Lists verified sender profiles from Brevo.
137
+ */
138
+ async listSenders(req, res) {
139
+ try {
140
+ const senders = await this.service.getSenders();
141
+ res.status(200).json({ success: true, data: senders });
142
+ }
143
+ catch (error) {
144
+ res.status(500).json({ success: false, error: error.message });
145
+ }
146
+ }
147
+ /**
148
+ * POST /send
149
+ * Triggers an email send for a given backend event.
150
+ */
151
+ async sendEventEmail(req, res) {
152
+ try {
153
+ const { eventName, recipientEmail, variables } = req.body;
154
+ if (!eventName || !recipientEmail) {
155
+ res.status(400).json({
156
+ success: false,
157
+ error: "Fields 'eventName' and 'recipientEmail' are required in request body",
158
+ });
159
+ return;
160
+ }
161
+ const outcome = await this.service.sendEventEmail(eventName, recipientEmail, variables);
162
+ if (!outcome.sent) {
163
+ res.status(200).json({ success: true, sent: false, message: outcome.message });
164
+ }
165
+ else {
166
+ res.status(200).json({ success: true, sent: true, message: outcome.message });
167
+ }
168
+ }
169
+ catch (error) {
170
+ res.status(500).json({ success: false, error: error.message });
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,5 @@
1
+ export { BrevoClient } from './brevoClient.js';
2
+ export { EmailTemplateRepository } from './repository.js';
3
+ export { EmailTemplateService } from './service.js';
4
+ export { EmailTemplateController } from './controller.js';
5
+ export * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { BrevoClient } from './brevoClient.js';
2
+ export { EmailTemplateRepository } from './repository.js';
3
+ export { EmailTemplateService } from './service.js';
4
+ export { EmailTemplateController } from './controller.js';
5
+ export * from './types.js';
@@ -0,0 +1,34 @@
1
+ import { SDKConfig, LocalMapping } from './types.js';
2
+ export declare class EmailTemplateRepository {
3
+ private dbType;
4
+ private dbClient;
5
+ private tableName;
6
+ constructor(config: SDKConfig);
7
+ /**
8
+ * Helper to parse SQL queries results.
9
+ * Handles PostgreSQL result format (res.rows) and MySQL format ([rows, fields]).
10
+ */
11
+ private parseSQLResult;
12
+ /**
13
+ * Fetch all event-to-template mappings from the local database
14
+ */
15
+ getAllMappings(): Promise<LocalMapping[]>;
16
+ /**
17
+ * Fetch mapping for a specific template ID
18
+ */
19
+ getMappingByTemplateId(templateId: number): Promise<LocalMapping | null>;
20
+ /**
21
+ * Fetch mapping for a specific event name.
22
+ * Assumes we want the active template linked to this event.
23
+ */
24
+ getActiveMappingByEvent(eventName: string): Promise<LocalMapping | null>;
25
+ /**
26
+ * Upsert a mapping (insert or update template config)
27
+ */
28
+ upsertMapping(templateId: number, eventName: string, isActive: boolean): Promise<void>;
29
+ /**
30
+ * Just-In-Time (JIT) Local Mapping Generation.
31
+ * If mapping does not exist, insert blank inactive mapping.
32
+ */
33
+ insertJITMapping(templateId: number): Promise<void>;
34
+ }
@@ -0,0 +1,224 @@
1
+ export class EmailTemplateRepository {
2
+ dbType;
3
+ dbClient;
4
+ tableName;
5
+ constructor(config) {
6
+ this.dbType = config.dbType;
7
+ this.dbClient = config.dbClient;
8
+ this.tableName = config.tableNameOrCollection || 'email_event_templates';
9
+ if (!this.dbClient) {
10
+ throw new Error(`Database client is required for database type: ${this.dbType}`);
11
+ }
12
+ }
13
+ /**
14
+ * Helper to parse SQL queries results.
15
+ * Handles PostgreSQL result format (res.rows) and MySQL format ([rows, fields]).
16
+ */
17
+ parseSQLResult(res) {
18
+ if (!res)
19
+ return [];
20
+ if (res.rows && Array.isArray(res.rows)) {
21
+ return res.rows;
22
+ }
23
+ if (Array.isArray(res)) {
24
+ if (res.length > 0 && Array.isArray(res[0])) {
25
+ // MySQL connection.query returns [rows, fields]
26
+ return res[0];
27
+ }
28
+ return res; // Flat rows array
29
+ }
30
+ return [];
31
+ }
32
+ /**
33
+ * Fetch all event-to-template mappings from the local database
34
+ */
35
+ async getAllMappings() {
36
+ if (this.dbType === 'firestore') {
37
+ const snapshot = await this.dbClient.collection(this.tableName).get();
38
+ const mappings = [];
39
+ snapshot.forEach((doc) => {
40
+ const data = doc.data();
41
+ mappings.push({
42
+ template_id: Number(data.template_id),
43
+ event_name: data.event_name || '',
44
+ is_active: !!data.is_active,
45
+ updated_at: data.updated_at?.toDate ? data.updated_at.toDate() : data.updated_at,
46
+ });
47
+ });
48
+ return mappings;
49
+ }
50
+ else {
51
+ const sql = `SELECT id, template_id, event_name, is_active, updated_at FROM ${this.tableName}`;
52
+ const res = await this.dbClient.query(sql, []);
53
+ const rows = this.parseSQLResult(res);
54
+ return rows.map((r) => ({
55
+ id: r.id,
56
+ template_id: Number(r.template_id),
57
+ event_name: r.event_name || '',
58
+ is_active: Boolean(r.is_active),
59
+ updated_at: r.updated_at,
60
+ }));
61
+ }
62
+ }
63
+ /**
64
+ * Fetch mapping for a specific template ID
65
+ */
66
+ async getMappingByTemplateId(templateId) {
67
+ if (this.dbType === 'firestore') {
68
+ const doc = await this.dbClient.collection(this.tableName).doc(templateId.toString()).get();
69
+ if (!doc.exists)
70
+ return null;
71
+ const data = doc.data();
72
+ return {
73
+ template_id: Number(data.template_id),
74
+ event_name: data.event_name || '',
75
+ is_active: !!data.is_active,
76
+ updated_at: data.updated_at?.toDate ? data.updated_at.toDate() : data.updated_at,
77
+ };
78
+ }
79
+ else {
80
+ const paramChar = this.dbType === 'postgres' ? '$1' : '?';
81
+ const sql = `SELECT id, template_id, event_name, is_active, updated_at FROM ${this.tableName} WHERE template_id = ${paramChar}`;
82
+ const res = await this.dbClient.query(sql, [templateId]);
83
+ const rows = this.parseSQLResult(res);
84
+ if (rows.length === 0)
85
+ return null;
86
+ const r = rows[0];
87
+ return {
88
+ id: r.id,
89
+ template_id: Number(r.template_id),
90
+ event_name: r.event_name || '',
91
+ is_active: Boolean(r.is_active),
92
+ updated_at: r.updated_at,
93
+ };
94
+ }
95
+ }
96
+ /**
97
+ * Fetch mapping for a specific event name.
98
+ * Assumes we want the active template linked to this event.
99
+ */
100
+ async getActiveMappingByEvent(eventName) {
101
+ if (this.dbType === 'firestore') {
102
+ const snapshot = await this.dbClient
103
+ .collection(this.tableName)
104
+ .where('event_name', '==', eventName)
105
+ .where('is_active', '==', true)
106
+ .limit(1)
107
+ .get();
108
+ if (snapshot.empty)
109
+ return null;
110
+ const doc = snapshot.docs[0];
111
+ const data = doc.data();
112
+ return {
113
+ template_id: Number(data.template_id),
114
+ event_name: data.event_name || '',
115
+ is_active: !!data.is_active,
116
+ updated_at: data.updated_at?.toDate ? data.updated_at.toDate() : data.updated_at,
117
+ };
118
+ }
119
+ else {
120
+ const param1 = this.dbType === 'postgres' ? '$1' : '?';
121
+ const param2 = this.dbType === 'postgres' ? '$2' : '?';
122
+ // Wait, is_active could be 1/0 or true/false, standard database handles boolean conversion, but is_active = true or is_active = 1
123
+ const sql = `SELECT id, template_id, event_name, is_active, updated_at FROM ${this.tableName} WHERE event_name = ${param1} AND is_active = ${param2} LIMIT 1`;
124
+ // Pass both parameters for portability
125
+ const res = await this.dbClient.query(sql, [eventName, true]);
126
+ const rows = this.parseSQLResult(res);
127
+ if (rows.length === 0)
128
+ return null;
129
+ const r = rows[0];
130
+ return {
131
+ id: r.id,
132
+ template_id: Number(r.template_id),
133
+ event_name: r.event_name || '',
134
+ is_active: Boolean(r.is_active),
135
+ updated_at: r.updated_at,
136
+ };
137
+ }
138
+ }
139
+ /**
140
+ * Upsert a mapping (insert or update template config)
141
+ */
142
+ async upsertMapping(templateId, eventName, isActive) {
143
+ if (this.dbType === 'firestore') {
144
+ await this.dbClient
145
+ .collection(this.tableName)
146
+ .doc(templateId.toString())
147
+ .set({
148
+ template_id: templateId,
149
+ event_name: eventName,
150
+ is_active: isActive,
151
+ updated_at: new Date(),
152
+ }, { merge: true });
153
+ }
154
+ else {
155
+ let sql = '';
156
+ let params = [];
157
+ if (this.dbType === 'postgres') {
158
+ sql = `
159
+ INSERT INTO ${this.tableName} (template_id, event_name, is_active, updated_at)
160
+ VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
161
+ ON CONFLICT (template_id)
162
+ DO UPDATE SET
163
+ event_name = EXCLUDED.event_name,
164
+ is_active = EXCLUDED.is_active,
165
+ updated_at = CURRENT_TIMESTAMP
166
+ `;
167
+ params = [templateId, eventName, isActive];
168
+ }
169
+ else {
170
+ // MySQL ON DUPLICATE KEY UPDATE
171
+ sql = `
172
+ INSERT INTO ${this.tableName} (template_id, event_name, is_active, updated_at)
173
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
174
+ ON DUPLICATE KEY UPDATE
175
+ event_name = VALUES(event_name),
176
+ is_active = VALUES(is_active),
177
+ updated_at = CURRENT_TIMESTAMP
178
+ `;
179
+ params = [templateId, eventName, isActive];
180
+ }
181
+ await this.dbClient.query(sql, params);
182
+ }
183
+ }
184
+ /**
185
+ * Just-In-Time (JIT) Local Mapping Generation.
186
+ * If mapping does not exist, insert blank inactive mapping.
187
+ */
188
+ async insertJITMapping(templateId) {
189
+ if (this.dbType === 'firestore') {
190
+ const docRef = this.dbClient.collection(this.tableName).doc(templateId.toString());
191
+ const doc = await docRef.get();
192
+ if (!doc.exists) {
193
+ await docRef.set({
194
+ template_id: templateId,
195
+ event_name: '',
196
+ is_active: false,
197
+ updated_at: new Date(),
198
+ });
199
+ }
200
+ }
201
+ else {
202
+ let sql = '';
203
+ let params = [];
204
+ if (this.dbType === 'postgres') {
205
+ sql = `
206
+ INSERT INTO ${this.tableName} (template_id, event_name, is_active, updated_at)
207
+ VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
208
+ ON CONFLICT (template_id)
209
+ DO NOTHING
210
+ `;
211
+ params = [templateId, '', false];
212
+ }
213
+ else {
214
+ // MySQL INSERT IGNORE
215
+ sql = `
216
+ INSERT IGNORE INTO ${this.tableName} (template_id, event_name, is_active, updated_at)
217
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
218
+ `;
219
+ params = [templateId, '', false];
220
+ }
221
+ await this.dbClient.query(sql, params);
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,41 @@
1
+ import { SDKConfig, CombinedTemplate, CreateTemplateInput, UpdateTemplateInput, BrevoSender } from './types.js';
2
+ export declare class EmailTemplateService {
3
+ private brevoClient;
4
+ private repository;
5
+ private defaultSender;
6
+ constructor(config: SDKConfig);
7
+ /**
8
+ * Syncs and returns all templates with their event mappings.
9
+ * Performs Just-In-Time (JIT) mapping creation for any templates missing in local DB.
10
+ */
11
+ listTemplates(): Promise<CombinedTemplate[]>;
12
+ /**
13
+ * Fetch a single combined template by ID.
14
+ * Creates JIT mapping if it is not in local DB.
15
+ */
16
+ getTemplate(id: number): Promise<CombinedTemplate>;
17
+ /**
18
+ * Create a new Brevo Template and its corresponding local DB mapping
19
+ */
20
+ createTemplate(input: CreateTemplateInput): Promise<CombinedTemplate>;
21
+ /**
22
+ * Update an existing template in Brevo and the local mapping DB
23
+ */
24
+ updateTemplate(id: number, input: UpdateTemplateInput): Promise<CombinedTemplate>;
25
+ /**
26
+ * Sync template active status across remote Brevo and local DB
27
+ */
28
+ toggleTemplateActive(id: number, isActive: boolean): Promise<void>;
29
+ /**
30
+ * Retrieve list of verified sender profiles
31
+ */
32
+ getSenders(): Promise<BrevoSender[]>;
33
+ /**
34
+ * Application-facing transactional sender logic:
35
+ * Event triggers query mapping database, checks status, and routes transactional send to Brevo
36
+ */
37
+ sendEventEmail(eventName: string, recipientEmail: string, variables?: Record<string, any>): Promise<{
38
+ sent: boolean;
39
+ message: string;
40
+ }>;
41
+ }
@@ -0,0 +1,185 @@
1
+ import { BrevoClient } from './brevoClient.js';
2
+ import { EmailTemplateRepository } from './repository.js';
3
+ export class EmailTemplateService {
4
+ brevoClient;
5
+ repository;
6
+ defaultSender;
7
+ constructor(config) {
8
+ this.brevoClient = new BrevoClient(config.brevoApiKey);
9
+ this.repository = new EmailTemplateRepository(config);
10
+ this.defaultSender = config.defaultSender;
11
+ }
12
+ /**
13
+ * Syncs and returns all templates with their event mappings.
14
+ * Performs Just-In-Time (JIT) mapping creation for any templates missing in local DB.
15
+ */
16
+ async listTemplates() {
17
+ // 1. Fetch remote templates from Brevo
18
+ const brevoTemplates = await this.brevoClient.getTemplates();
19
+ // 2. Fetch local mappings
20
+ const localMappings = await this.repository.getAllMappings();
21
+ const mappingMap = new Map(localMappings.map((m) => [m.template_id, m]));
22
+ const combined = [];
23
+ for (const bt of brevoTemplates) {
24
+ let mapping = mappingMap.get(bt.id);
25
+ // 3. JIT Mapping Creation: If template exists on Brevo but not locally
26
+ if (!mapping) {
27
+ await this.repository.insertJITMapping(bt.id);
28
+ mapping = {
29
+ template_id: bt.id,
30
+ event_name: '',
31
+ is_active: false,
32
+ };
33
+ }
34
+ combined.push({
35
+ templateId: bt.id,
36
+ templateName: bt.name,
37
+ subject: bt.subject,
38
+ eventName: mapping.event_name,
39
+ isActive: bt.isActive && mapping.is_active, // Sync state
40
+ sender: bt.sender,
41
+ });
42
+ }
43
+ return combined;
44
+ }
45
+ /**
46
+ * Fetch a single combined template by ID.
47
+ * Creates JIT mapping if it is not in local DB.
48
+ */
49
+ async getTemplate(id) {
50
+ const bt = await this.brevoClient.getTemplate(id);
51
+ let mapping = await this.repository.getMappingByTemplateId(id);
52
+ if (!mapping) {
53
+ await this.repository.insertJITMapping(id);
54
+ mapping = {
55
+ template_id: id,
56
+ event_name: '',
57
+ is_active: false,
58
+ };
59
+ }
60
+ return {
61
+ templateId: bt.id,
62
+ templateName: bt.name,
63
+ subject: bt.subject,
64
+ eventName: mapping.event_name,
65
+ isActive: bt.isActive && mapping.is_active,
66
+ sender: bt.sender,
67
+ htmlContent: bt.htmlContent,
68
+ };
69
+ }
70
+ /**
71
+ * Create a new Brevo Template and its corresponding local DB mapping
72
+ */
73
+ async createTemplate(input) {
74
+ const name = input.name || 'New Untitled Template';
75
+ const subject = input.subject || 'Drafting Layout';
76
+ const senderEmail = input.senderEmail || this.defaultSender.email;
77
+ const senderName = input.senderName || this.defaultSender.name;
78
+ const htmlContent = '<html><body><p>Drafting...</p></body></html>';
79
+ // 1. Create on Brevo
80
+ const templateId = await this.brevoClient.createTemplate({
81
+ templateName: name,
82
+ subject: subject,
83
+ sender: { name: senderName, email: senderEmail },
84
+ htmlContent: htmlContent,
85
+ isActive: false,
86
+ });
87
+ // 2. Insert mapping in local DB
88
+ await this.repository.upsertMapping(templateId, '', false);
89
+ return {
90
+ templateId: templateId,
91
+ templateName: name,
92
+ subject: subject,
93
+ eventName: '',
94
+ isActive: false,
95
+ sender: { name: senderName || '', email: senderEmail },
96
+ htmlContent: htmlContent,
97
+ };
98
+ }
99
+ /**
100
+ * Update an existing template in Brevo and the local mapping DB
101
+ */
102
+ async updateTemplate(id, input) {
103
+ // Constraint: Template name cannot be blank
104
+ if (!input.templateName || input.templateName.trim() === '') {
105
+ throw new Error('Template name cannot be blank');
106
+ }
107
+ // Constraint: HTML content must exceed 10 characters
108
+ if (!input.htmlContent || input.htmlContent.length <= 10) {
109
+ throw new Error('HTML content must be greater than 10 characters');
110
+ }
111
+ const sender = {
112
+ email: input.sender?.email || this.defaultSender.email,
113
+ name: input.sender?.name || this.defaultSender.name || '',
114
+ };
115
+ // 1. Update Brevo template configuration
116
+ await this.brevoClient.updateTemplate(id, {
117
+ templateName: input.templateName,
118
+ subject: input.subject,
119
+ sender: sender,
120
+ htmlContent: input.htmlContent,
121
+ isActive: input.isActive,
122
+ });
123
+ // 2. Update local mapping DB
124
+ await this.repository.upsertMapping(id, input.eventName || '', input.isActive);
125
+ return {
126
+ templateId: id,
127
+ templateName: input.templateName,
128
+ subject: input.subject,
129
+ eventName: input.eventName || '',
130
+ isActive: input.isActive,
131
+ sender: sender,
132
+ htmlContent: input.htmlContent,
133
+ };
134
+ }
135
+ /**
136
+ * Sync template active status across remote Brevo and local DB
137
+ */
138
+ async toggleTemplateActive(id, isActive) {
139
+ // 1. Retrieve the existing Brevo template details so we can re-submit them securely
140
+ const bt = await this.brevoClient.getTemplate(id);
141
+ // 2. Flip remote status
142
+ await this.brevoClient.updateTemplate(id, {
143
+ templateName: bt.name,
144
+ subject: bt.subject,
145
+ sender: bt.sender || this.defaultSender,
146
+ htmlContent: bt.htmlContent || '',
147
+ isActive: isActive,
148
+ });
149
+ // 3. Update local DB mapping status
150
+ const mapping = await this.repository.getMappingByTemplateId(id);
151
+ const eventName = mapping ? mapping.event_name : '';
152
+ await this.repository.upsertMapping(id, eventName, isActive);
153
+ }
154
+ /**
155
+ * Retrieve list of verified sender profiles
156
+ */
157
+ async getSenders() {
158
+ return this.brevoClient.getSenders();
159
+ }
160
+ /**
161
+ * Application-facing transactional sender logic:
162
+ * Event triggers query mapping database, checks status, and routes transactional send to Brevo
163
+ */
164
+ async sendEventEmail(eventName, recipientEmail, variables) {
165
+ // 1. Find mapped template configuration
166
+ const mapping = await this.repository.getActiveMappingByEvent(eventName);
167
+ // 2. Guardrail: If template is disabled (isActive = false), event engine completely ignores it
168
+ if (!mapping || !mapping.is_active) {
169
+ return {
170
+ sent: false,
171
+ message: `Event '${eventName}' skipped: No active template mapped to this event.`,
172
+ };
173
+ }
174
+ // 3. Push transactional template send to Brevo API
175
+ await this.brevoClient.sendEmail({
176
+ templateId: mapping.template_id,
177
+ to: recipientEmail,
178
+ variables: variables,
179
+ });
180
+ return {
181
+ sent: true,
182
+ message: `Successfully sent email for event '${eventName}' using template #${mapping.template_id}.`,
183
+ };
184
+ }
185
+ }