@astralibx/email-rule-engine 1.2.2 → 3.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.
Files changed (109) hide show
  1. package/README.md +93 -46
  2. package/dist/index.d.mts +607 -0
  3. package/dist/index.d.ts +595 -20
  4. package/dist/index.js +1731 -65
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1699 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +27 -8
  9. package/dist/controllers/index.d.ts +0 -5
  10. package/dist/controllers/index.d.ts.map +0 -1
  11. package/dist/controllers/index.js +0 -12
  12. package/dist/controllers/index.js.map +0 -1
  13. package/dist/controllers/rule.controller.d.ts +0 -13
  14. package/dist/controllers/rule.controller.d.ts.map +0 -1
  15. package/dist/controllers/rule.controller.js +0 -142
  16. package/dist/controllers/rule.controller.js.map +0 -1
  17. package/dist/controllers/runner.controller.d.ts +0 -8
  18. package/dist/controllers/runner.controller.d.ts.map +0 -1
  19. package/dist/controllers/runner.controller.js +0 -22
  20. package/dist/controllers/runner.controller.js.map +0 -1
  21. package/dist/controllers/settings.controller.d.ts +0 -7
  22. package/dist/controllers/settings.controller.d.ts.map +0 -1
  23. package/dist/controllers/settings.controller.js +0 -56
  24. package/dist/controllers/settings.controller.js.map +0 -1
  25. package/dist/controllers/template.controller.d.ts +0 -15
  26. package/dist/controllers/template.controller.d.ts.map +0 -1
  27. package/dist/controllers/template.controller.js +0 -169
  28. package/dist/controllers/template.controller.js.map +0 -1
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/routes/index.d.ts +0 -16
  31. package/dist/routes/index.d.ts.map +0 -1
  32. package/dist/routes/index.js +0 -47
  33. package/dist/routes/index.js.map +0 -1
  34. package/dist/schemas/index.d.ts +0 -6
  35. package/dist/schemas/index.d.ts.map +0 -1
  36. package/dist/schemas/index.js +0 -14
  37. package/dist/schemas/index.js.map +0 -1
  38. package/dist/schemas/rule-send.schema.d.ts +0 -25
  39. package/dist/schemas/rule-send.schema.d.ts.map +0 -1
  40. package/dist/schemas/rule-send.schema.js +0 -41
  41. package/dist/schemas/rule-send.schema.js.map +0 -1
  42. package/dist/schemas/rule.schema.d.ts +0 -22
  43. package/dist/schemas/rule.schema.d.ts.map +0 -1
  44. package/dist/schemas/rule.schema.js +0 -81
  45. package/dist/schemas/rule.schema.js.map +0 -1
  46. package/dist/schemas/run-log.schema.d.ts +0 -39
  47. package/dist/schemas/run-log.schema.d.ts.map +0 -1
  48. package/dist/schemas/run-log.schema.js +0 -47
  49. package/dist/schemas/run-log.schema.js.map +0 -1
  50. package/dist/schemas/template.schema.d.ts +0 -24
  51. package/dist/schemas/template.schema.d.ts.map +0 -1
  52. package/dist/schemas/template.schema.js +0 -65
  53. package/dist/schemas/template.schema.js.map +0 -1
  54. package/dist/schemas/throttle-config.schema.d.ts +0 -19
  55. package/dist/schemas/throttle-config.schema.d.ts.map +0 -1
  56. package/dist/schemas/throttle-config.schema.js +0 -32
  57. package/dist/schemas/throttle-config.schema.js.map +0 -1
  58. package/dist/services/index.d.ts +0 -5
  59. package/dist/services/index.d.ts.map +0 -1
  60. package/dist/services/index.js +0 -12
  61. package/dist/services/index.js.map +0 -1
  62. package/dist/services/rule-runner.service.d.ts +0 -31
  63. package/dist/services/rule-runner.service.d.ts.map +0 -1
  64. package/dist/services/rule-runner.service.js +0 -279
  65. package/dist/services/rule-runner.service.js.map +0 -1
  66. package/dist/services/rule.service.d.ts +0 -27
  67. package/dist/services/rule.service.d.ts.map +0 -1
  68. package/dist/services/rule.service.js +0 -127
  69. package/dist/services/rule.service.js.map +0 -1
  70. package/dist/services/template-render.service.d.ts +0 -23
  71. package/dist/services/template-render.service.d.ts.map +0 -1
  72. package/dist/services/template-render.service.js +0 -178
  73. package/dist/services/template-render.service.js.map +0 -1
  74. package/dist/services/template.service.d.ts +0 -42
  75. package/dist/services/template.service.d.ts.map +0 -1
  76. package/dist/services/template.service.js +0 -128
  77. package/dist/services/template.service.js.map +0 -1
  78. package/dist/types/config.types.d.ts +0 -85
  79. package/dist/types/config.types.d.ts.map +0 -1
  80. package/dist/types/config.types.js +0 -3
  81. package/dist/types/config.types.js.map +0 -1
  82. package/dist/types/enums.d.ts +0 -43
  83. package/dist/types/enums.d.ts.map +0 -1
  84. package/dist/types/enums.js +0 -40
  85. package/dist/types/enums.js.map +0 -1
  86. package/dist/types/index.d.ts +0 -6
  87. package/dist/types/index.d.ts.map +0 -1
  88. package/dist/types/index.js +0 -11
  89. package/dist/types/index.js.map +0 -1
  90. package/dist/types/rule.types.d.ts +0 -91
  91. package/dist/types/rule.types.d.ts.map +0 -1
  92. package/dist/types/rule.types.js +0 -3
  93. package/dist/types/rule.types.js.map +0 -1
  94. package/dist/types/template.types.d.ts +0 -43
  95. package/dist/types/template.types.d.ts.map +0 -1
  96. package/dist/types/template.types.js +0 -3
  97. package/dist/types/template.types.js.map +0 -1
  98. package/dist/types/throttle.types.d.ts +0 -15
  99. package/dist/types/throttle.types.d.ts.map +0 -1
  100. package/dist/types/throttle.types.js +0 -3
  101. package/dist/types/throttle.types.js.map +0 -1
  102. package/dist/utils/express-helpers.d.ts +0 -4
  103. package/dist/utils/express-helpers.d.ts.map +0 -1
  104. package/dist/utils/express-helpers.js +0 -15
  105. package/dist/utils/express-helpers.js.map +0 -1
  106. package/dist/utils/redis-lock.d.ts +0 -13
  107. package/dist/utils/redis-lock.d.ts.map +0 -1
  108. package/dist/utils/redis-lock.js +0 -36
  109. package/dist/utils/redis-lock.js.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,1699 @@
1
+ import { Schema } from 'mongoose';
2
+ import Handlebars from 'handlebars';
3
+ import mjml2html from 'mjml';
4
+ import { convert } from 'html-to-text';
5
+ import { baseRedisSchema, baseDbSchema, loggerSchema, AlxError } from '@astralibx/core';
6
+ import crypto from 'crypto';
7
+ import { Router } from 'express';
8
+ import { z } from 'zod';
9
+
10
+ // src/schemas/template.schema.ts
11
+
12
+ // src/constants/index.ts
13
+ var TEMPLATE_CATEGORY = {
14
+ Onboarding: "onboarding",
15
+ Engagement: "engagement",
16
+ Transactional: "transactional",
17
+ ReEngagement: "re-engagement",
18
+ Announcement: "announcement"
19
+ };
20
+ var TEMPLATE_AUDIENCE = {
21
+ Customer: "customer",
22
+ Provider: "provider",
23
+ All: "all"
24
+ };
25
+ var RULE_OPERATOR = {
26
+ Eq: "eq",
27
+ Neq: "neq",
28
+ Gt: "gt",
29
+ Gte: "gte",
30
+ Lt: "lt",
31
+ Lte: "lte",
32
+ Exists: "exists",
33
+ NotExists: "not_exists",
34
+ In: "in",
35
+ NotIn: "not_in",
36
+ Contains: "contains"
37
+ };
38
+ var EMAIL_TYPE = {
39
+ Automated: "automated",
40
+ Transactional: "transactional"
41
+ };
42
+ var RUN_TRIGGER = {
43
+ Cron: "cron",
44
+ Manual: "manual"
45
+ };
46
+ var THROTTLE_WINDOW = {
47
+ Rolling: "rolling"
48
+ };
49
+ var EMAIL_SEND_STATUS = {
50
+ Sent: "sent",
51
+ Error: "error",
52
+ Skipped: "skipped",
53
+ Invalid: "invalid",
54
+ Throttled: "throttled"
55
+ };
56
+
57
+ // src/schemas/template.schema.ts
58
+ function createEmailTemplateSchema(platformValues, audienceValues, categoryValues, collectionPrefix) {
59
+ const schema = new Schema(
60
+ {
61
+ name: { type: String, required: true },
62
+ slug: { type: String, required: true, unique: true },
63
+ description: String,
64
+ category: { type: String, enum: categoryValues || Object.values(TEMPLATE_CATEGORY), required: true },
65
+ audience: { type: String, enum: audienceValues || Object.values(TEMPLATE_AUDIENCE), required: true },
66
+ platform: {
67
+ type: String,
68
+ required: true,
69
+ ...platformValues ? { enum: platformValues } : {}
70
+ },
71
+ subject: { type: String, required: true },
72
+ body: { type: String, required: true },
73
+ textBody: String,
74
+ variables: [{ type: String }],
75
+ version: { type: Number, default: 1 },
76
+ isActive: { type: Boolean, default: true, index: true }
77
+ },
78
+ {
79
+ timestamps: true,
80
+ collection: `${collectionPrefix || ""}email_templates`,
81
+ statics: {
82
+ findBySlug(slug) {
83
+ return this.findOne({ slug });
84
+ },
85
+ findActive() {
86
+ return this.find({ isActive: true }).sort({ category: 1, name: 1 });
87
+ },
88
+ findByCategory(category) {
89
+ return this.find({ category, isActive: true }).sort({ name: 1 });
90
+ },
91
+ findByAudience(audience) {
92
+ return this.find({
93
+ $or: [{ audience }, { audience: TEMPLATE_AUDIENCE.All }],
94
+ isActive: true
95
+ }).sort({ name: 1 });
96
+ },
97
+ async createTemplate(input) {
98
+ return this.create({
99
+ name: input.name,
100
+ slug: input.slug,
101
+ description: input.description,
102
+ category: input.category,
103
+ audience: input.audience,
104
+ platform: input.platform,
105
+ subject: input.subject,
106
+ body: input.body,
107
+ textBody: input.textBody,
108
+ variables: input.variables || [],
109
+ version: 1,
110
+ isActive: true
111
+ });
112
+ }
113
+ }
114
+ }
115
+ );
116
+ schema.index({ category: 1, isActive: 1 });
117
+ schema.index({ audience: 1, platform: 1, isActive: 1 });
118
+ return schema;
119
+ }
120
+ function createEmailRuleSchema(platformValues, audienceValues, collectionPrefix) {
121
+ const RuleConditionSchema = new Schema({
122
+ field: { type: String, required: true },
123
+ operator: { type: String, enum: Object.values(RULE_OPERATOR), required: true },
124
+ value: { type: Schema.Types.Mixed }
125
+ }, { _id: false });
126
+ const RuleTargetSchema = new Schema({
127
+ role: { type: String, enum: audienceValues || Object.values(TEMPLATE_AUDIENCE), required: true },
128
+ platform: {
129
+ type: String,
130
+ required: true,
131
+ ...platformValues ? { enum: platformValues } : {}
132
+ },
133
+ conditions: [RuleConditionSchema]
134
+ }, { _id: false });
135
+ const RuleRunStatsSchema = new Schema({
136
+ matched: { type: Number, default: 0 },
137
+ sent: { type: Number, default: 0 },
138
+ skipped: { type: Number, default: 0 },
139
+ skippedByThrottle: { type: Number, default: 0 },
140
+ errors: { type: Number, default: 0 }
141
+ }, { _id: false });
142
+ const schema = new Schema(
143
+ {
144
+ name: { type: String, required: true },
145
+ description: String,
146
+ isActive: { type: Boolean, default: false, index: true },
147
+ sortOrder: { type: Number, default: 10 },
148
+ target: { type: RuleTargetSchema, required: true },
149
+ templateId: { type: Schema.Types.ObjectId, ref: "EmailTemplate", required: true, index: true },
150
+ sendOnce: { type: Boolean, default: true },
151
+ resendAfterDays: Number,
152
+ cooldownDays: Number,
153
+ autoApprove: { type: Boolean, default: true },
154
+ maxPerRun: Number,
155
+ bypassThrottle: { type: Boolean, default: false },
156
+ emailType: { type: String, enum: Object.values(EMAIL_TYPE), default: EMAIL_TYPE.Automated },
157
+ totalSent: { type: Number, default: 0 },
158
+ totalSkipped: { type: Number, default: 0 },
159
+ lastRunAt: Date,
160
+ lastRunStats: RuleRunStatsSchema
161
+ },
162
+ {
163
+ timestamps: true,
164
+ collection: `${collectionPrefix || ""}email_rules`,
165
+ statics: {
166
+ findActive() {
167
+ return this.find({ isActive: true }).sort({ sortOrder: 1 });
168
+ },
169
+ findByTemplateId(templateId) {
170
+ return this.find({ templateId });
171
+ },
172
+ async createRule(input) {
173
+ return this.create({
174
+ name: input.name,
175
+ description: input.description,
176
+ isActive: false,
177
+ sortOrder: input.sortOrder ?? 10,
178
+ target: input.target,
179
+ templateId: input.templateId,
180
+ sendOnce: input.sendOnce ?? true,
181
+ resendAfterDays: input.resendAfterDays,
182
+ cooldownDays: input.cooldownDays,
183
+ autoApprove: input.autoApprove ?? true,
184
+ maxPerRun: input.maxPerRun,
185
+ bypassThrottle: input.bypassThrottle ?? false,
186
+ emailType: input.emailType ?? EMAIL_TYPE.Automated,
187
+ totalSent: 0,
188
+ totalSkipped: 0
189
+ });
190
+ }
191
+ }
192
+ }
193
+ );
194
+ schema.index({ isActive: 1, sortOrder: 1 });
195
+ schema.index({ templateId: 1 });
196
+ return schema;
197
+ }
198
+ function createEmailRuleSendSchema(collectionPrefix) {
199
+ const schema = new Schema(
200
+ {
201
+ ruleId: { type: Schema.Types.ObjectId, ref: "EmailRule", required: true },
202
+ userId: { type: Schema.Types.ObjectId, required: true },
203
+ emailIdentifierId: { type: Schema.Types.ObjectId },
204
+ messageId: { type: Schema.Types.ObjectId },
205
+ sentAt: { type: Date, required: true, default: () => /* @__PURE__ */ new Date() },
206
+ status: { type: String },
207
+ accountId: { type: String },
208
+ senderName: { type: String },
209
+ subject: { type: String },
210
+ failureReason: { type: String }
211
+ },
212
+ {
213
+ collection: `${collectionPrefix || ""}email_rule_sends`,
214
+ statics: {
215
+ findLatestForUser(ruleId, userId) {
216
+ return this.findOne({ ruleId, userId }).sort({ sentAt: -1 });
217
+ },
218
+ findRecentByUserIds(userIds, sinceDays) {
219
+ const since = new Date(Date.now() - sinceDays * 864e5);
220
+ return this.find({
221
+ userId: { $in: userIds },
222
+ sentAt: { $gte: since }
223
+ }).sort({ sentAt: -1 });
224
+ },
225
+ async logSend(ruleId, userId, emailIdentifierId, messageId, extra) {
226
+ return this.create({
227
+ ruleId,
228
+ userId,
229
+ emailIdentifierId,
230
+ messageId,
231
+ sentAt: /* @__PURE__ */ new Date(),
232
+ ...extra
233
+ });
234
+ }
235
+ }
236
+ }
237
+ );
238
+ schema.index({ ruleId: 1, userId: 1, sentAt: -1 });
239
+ schema.index({ userId: 1, sentAt: -1 });
240
+ schema.index({ ruleId: 1, sentAt: -1 });
241
+ return schema;
242
+ }
243
+ function createEmailRuleRunLogSchema(collectionPrefix) {
244
+ const PerRuleStatsSchema = new Schema({
245
+ ruleId: { type: Schema.Types.ObjectId, ref: "EmailRule", required: true },
246
+ ruleName: { type: String, required: true },
247
+ matched: { type: Number, default: 0 },
248
+ sent: { type: Number, default: 0 },
249
+ skipped: { type: Number, default: 0 },
250
+ skippedByThrottle: { type: Number, default: 0 },
251
+ errors: { type: Number, default: 0 }
252
+ }, { _id: false });
253
+ const TotalStatsSchema = new Schema({
254
+ matched: { type: Number, default: 0 },
255
+ sent: { type: Number, default: 0 },
256
+ skipped: { type: Number, default: 0 },
257
+ skippedByThrottle: { type: Number, default: 0 },
258
+ errors: { type: Number, default: 0 }
259
+ }, { _id: false });
260
+ const schema = new Schema(
261
+ {
262
+ runAt: { type: Date, required: true, default: () => /* @__PURE__ */ new Date() },
263
+ triggeredBy: { type: String, enum: Object.values(RUN_TRIGGER), required: true },
264
+ duration: { type: Number, required: true },
265
+ rulesProcessed: { type: Number, required: true },
266
+ totalStats: { type: TotalStatsSchema, required: true },
267
+ perRuleStats: [PerRuleStatsSchema]
268
+ },
269
+ {
270
+ collection: `${collectionPrefix || ""}email_rule_run_logs`,
271
+ statics: {
272
+ getRecent(limit = 20) {
273
+ return this.find().sort({ runAt: -1 }).limit(limit);
274
+ },
275
+ getByRuleId(ruleId, limit = 20) {
276
+ return this.find({ "perRuleStats.ruleId": ruleId }).sort({ runAt: -1 }).limit(limit);
277
+ }
278
+ }
279
+ }
280
+ );
281
+ schema.index({ runAt: -1 });
282
+ schema.index({ runAt: 1 }, { expireAfterSeconds: 90 * 86400 });
283
+ return schema;
284
+ }
285
+ function createEmailThrottleConfigSchema(collectionPrefix) {
286
+ const schema = new Schema(
287
+ {
288
+ maxPerUserPerDay: { type: Number, default: 1 },
289
+ maxPerUserPerWeek: { type: Number, default: 2 },
290
+ minGapDays: { type: Number, default: 3 },
291
+ throttleWindow: { type: String, enum: Object.values(THROTTLE_WINDOW), default: THROTTLE_WINDOW.Rolling }
292
+ },
293
+ {
294
+ timestamps: true,
295
+ collection: `${collectionPrefix || ""}email_throttle_config`,
296
+ statics: {
297
+ async getConfig() {
298
+ let config = await this.findOne();
299
+ if (!config) {
300
+ config = await this.create({
301
+ maxPerUserPerDay: 1,
302
+ maxPerUserPerWeek: 2,
303
+ minGapDays: 3,
304
+ throttleWindow: THROTTLE_WINDOW.Rolling
305
+ });
306
+ }
307
+ return config;
308
+ }
309
+ }
310
+ }
311
+ );
312
+ return schema;
313
+ }
314
+ var MJML_BASE_OPEN = `<mjml>
315
+ <mj-head>
316
+ <mj-attributes>
317
+ <mj-all font-family="Arial, sans-serif" />
318
+ <mj-text font-size="15px" color="#333333" line-height="1.6" />
319
+ </mj-attributes>
320
+ </mj-head>
321
+ <mj-body background-color="#ffffff">
322
+ <mj-section padding="20px">
323
+ <mj-column>
324
+ <mj-text>`;
325
+ var MJML_BASE_CLOSE = ` </mj-text>
326
+ </mj-column>
327
+ </mj-section>
328
+ </mj-body>
329
+ </mjml>`;
330
+ var DATE_FORMAT_OPTIONS = {
331
+ day: "numeric",
332
+ month: "short",
333
+ year: "numeric"
334
+ };
335
+ function registerHelpers() {
336
+ Handlebars.registerHelper("currency", (val) => {
337
+ return `\u20B9${Number(val).toLocaleString("en-IN")}`;
338
+ });
339
+ Handlebars.registerHelper("formatDate", (date) => {
340
+ const d = new Date(date);
341
+ return d.toLocaleDateString("en-IN", DATE_FORMAT_OPTIONS);
342
+ });
343
+ Handlebars.registerHelper("capitalize", (str) => {
344
+ if (!str) return "";
345
+ return str.charAt(0).toUpperCase() + str.slice(1);
346
+ });
347
+ Handlebars.registerHelper("eq", (a, b) => a === b);
348
+ Handlebars.registerHelper("neq", (a, b) => a !== b);
349
+ Handlebars.registerHelper("not", (val) => !val);
350
+ Handlebars.registerHelper("gt", (a, b) => a > b);
351
+ Handlebars.registerHelper("lt", (a, b) => a < b);
352
+ Handlebars.registerHelper("gte", (a, b) => a >= b);
353
+ Handlebars.registerHelper("lte", (a, b) => a <= b);
354
+ Handlebars.registerHelper("lowercase", (str) => {
355
+ return str ? str.toLowerCase() : "";
356
+ });
357
+ Handlebars.registerHelper("uppercase", (str) => {
358
+ return str ? str.toUpperCase() : "";
359
+ });
360
+ Handlebars.registerHelper("join", (arr, separator) => {
361
+ if (!Array.isArray(arr)) return "";
362
+ const sep = typeof separator === "string" ? separator : ", ";
363
+ return arr.join(sep);
364
+ });
365
+ Handlebars.registerHelper("pluralize", (count, singular, plural) => {
366
+ return count === 1 ? singular : typeof plural === "string" ? plural : singular + "s";
367
+ });
368
+ }
369
+ var helpersRegistered = false;
370
+ function ensureHelpers() {
371
+ if (!helpersRegistered) {
372
+ registerHelpers();
373
+ helpersRegistered = true;
374
+ }
375
+ }
376
+ function wrapInMjml(body) {
377
+ if (body.trim().startsWith("<mjml")) {
378
+ return body;
379
+ }
380
+ return `${MJML_BASE_OPEN}${body}${MJML_BASE_CLOSE}`;
381
+ }
382
+ function compileMjml(mjmlSource) {
383
+ const result = mjml2html(mjmlSource, {
384
+ validationLevel: "soft",
385
+ minify: false
386
+ });
387
+ if (result.errors && result.errors.length > 0) {
388
+ const criticalErrors = result.errors.filter((e) => e.tagName !== void 0);
389
+ if (criticalErrors.length > 0) {
390
+ throw new Error(`MJML compilation errors: ${criticalErrors.map((e) => e.message).join("; ")}`);
391
+ }
392
+ }
393
+ return result.html;
394
+ }
395
+ function htmlToPlainText(html) {
396
+ return convert(html, {
397
+ wordwrap: 80,
398
+ selectors: [
399
+ { selector: "a", options: { hideLinkHrefIfSameAsText: true } },
400
+ { selector: "img", format: "skip" }
401
+ ]
402
+ });
403
+ }
404
+ var TemplateRenderService = class {
405
+ constructor() {
406
+ ensureHelpers();
407
+ }
408
+ renderSingle(subject, body, data, textBody) {
409
+ const subjectFn = Handlebars.compile(subject, { strict: true });
410
+ const resolvedSubject = subjectFn(data);
411
+ const bodyFn = Handlebars.compile(body, { strict: true });
412
+ const resolvedBody = bodyFn(data);
413
+ const mjmlSource = wrapInMjml(resolvedBody);
414
+ const html = compileMjml(mjmlSource);
415
+ let text;
416
+ if (textBody) {
417
+ const textFn = Handlebars.compile(textBody, { strict: true });
418
+ text = textFn(data);
419
+ } else {
420
+ text = htmlToPlainText(html);
421
+ }
422
+ return { html, text, subject: resolvedSubject };
423
+ }
424
+ compileBatch(subject, body, textBody) {
425
+ const mjmlSource = wrapInMjml(body);
426
+ const htmlWithHandlebars = compileMjml(mjmlSource);
427
+ const subjectFn = Handlebars.compile(subject, { strict: true });
428
+ const bodyFn = Handlebars.compile(htmlWithHandlebars, { strict: true });
429
+ const textBodyFn = textBody ? Handlebars.compile(textBody, { strict: true }) : void 0;
430
+ return { subjectFn, bodyFn, textBodyFn };
431
+ }
432
+ renderFromCompiled(compiled, data) {
433
+ const subject = compiled.subjectFn(data);
434
+ const html = compiled.bodyFn(data);
435
+ const text = compiled.textBodyFn ? compiled.textBodyFn(data) : htmlToPlainText(html);
436
+ return { html, text, subject };
437
+ }
438
+ renderPreview(subject, body, data, textBody) {
439
+ return this.renderSingle(subject, body, data, textBody);
440
+ }
441
+ extractVariables(template) {
442
+ const regex = /\{\{(?!#|\/|!|>)([^}]+)\}\}/g;
443
+ const variables = /* @__PURE__ */ new Set();
444
+ let match;
445
+ while ((match = regex.exec(template)) !== null) {
446
+ const variable = match[1].trim();
447
+ if (!variable.startsWith("else")) {
448
+ variables.add(variable);
449
+ }
450
+ }
451
+ return Array.from(variables).sort();
452
+ }
453
+ validateTemplate(body) {
454
+ const errors = [];
455
+ try {
456
+ Handlebars.precompile(body);
457
+ } catch (e) {
458
+ errors.push(`Handlebars syntax error: ${e.message}`);
459
+ }
460
+ const mjmlSource = wrapInMjml(body);
461
+ try {
462
+ const result = mjml2html(mjmlSource, { validationLevel: "strict" });
463
+ if (result.errors && result.errors.length > 0) {
464
+ for (const err of result.errors) {
465
+ errors.push(`MJML error: ${err.message}`);
466
+ }
467
+ }
468
+ } catch (e) {
469
+ errors.push(`MJML compilation error: ${e.message}`);
470
+ }
471
+ return { valid: errors.length === 0, errors };
472
+ }
473
+ };
474
+ var AlxEmailError = class extends AlxError {
475
+ constructor(message, code) {
476
+ super(message, code);
477
+ this.name = "AlxEmailError";
478
+ }
479
+ };
480
+ var ConfigValidationError = class extends AlxEmailError {
481
+ constructor(message, field) {
482
+ super(message, "CONFIG_VALIDATION");
483
+ this.field = field;
484
+ this.name = "ConfigValidationError";
485
+ }
486
+ };
487
+ var TemplateNotFoundError = class extends AlxEmailError {
488
+ constructor(templateId) {
489
+ super(`Template not found: ${templateId}`, "TEMPLATE_NOT_FOUND");
490
+ this.templateId = templateId;
491
+ this.name = "TemplateNotFoundError";
492
+ }
493
+ };
494
+ var TemplateSyntaxError = class extends AlxEmailError {
495
+ constructor(message, errors) {
496
+ super(message, "TEMPLATE_SYNTAX");
497
+ this.errors = errors;
498
+ this.name = "TemplateSyntaxError";
499
+ }
500
+ };
501
+ var RuleNotFoundError = class extends AlxEmailError {
502
+ constructor(ruleId) {
503
+ super(`Rule not found: ${ruleId}`, "RULE_NOT_FOUND");
504
+ this.ruleId = ruleId;
505
+ this.name = "RuleNotFoundError";
506
+ }
507
+ };
508
+ var RuleTemplateIncompatibleError = class extends AlxEmailError {
509
+ constructor(reason) {
510
+ super(`Rule-template incompatibility: ${reason}`, "RULE_TEMPLATE_INCOMPATIBLE");
511
+ this.reason = reason;
512
+ this.name = "RuleTemplateIncompatibleError";
513
+ }
514
+ };
515
+ var LockAcquisitionError = class extends AlxEmailError {
516
+ constructor() {
517
+ super("Could not acquire distributed lock \u2014 another run is in progress", "LOCK_ACQUISITION");
518
+ this.name = "LockAcquisitionError";
519
+ }
520
+ };
521
+ var DuplicateSlugError = class extends AlxEmailError {
522
+ constructor(slug) {
523
+ super(`Template with slug "${slug}" already exists`, "DUPLICATE_SLUG");
524
+ this.slug = slug;
525
+ this.name = "DuplicateSlugError";
526
+ }
527
+ };
528
+
529
+ // src/services/template.service.ts
530
+ var UPDATEABLE_FIELDS = /* @__PURE__ */ new Set([
531
+ "name",
532
+ "description",
533
+ "category",
534
+ "audience",
535
+ "platform",
536
+ "subject",
537
+ "body",
538
+ "textBody",
539
+ "variables",
540
+ "isActive"
541
+ ]);
542
+ function slugify(name) {
543
+ return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
544
+ }
545
+ var TemplateService = class {
546
+ constructor(EmailTemplate, config) {
547
+ this.EmailTemplate = EmailTemplate;
548
+ this.config = config;
549
+ }
550
+ renderService = new TemplateRenderService();
551
+ async list(filters) {
552
+ const query = {};
553
+ if (filters?.category) query["category"] = filters.category;
554
+ if (filters?.audience) query["audience"] = filters.audience;
555
+ if (filters?.platform) query["platform"] = filters.platform;
556
+ if (filters?.isActive !== void 0) query["isActive"] = filters.isActive;
557
+ return this.EmailTemplate.find(query).sort({ category: 1, name: 1 });
558
+ }
559
+ async getById(id) {
560
+ return this.EmailTemplate.findById(id);
561
+ }
562
+ async getBySlug(slug) {
563
+ return this.EmailTemplate.findBySlug(slug);
564
+ }
565
+ async create(input) {
566
+ const slug = input.slug || slugify(input.name);
567
+ const existing = await this.EmailTemplate.findBySlug(slug);
568
+ if (existing) {
569
+ throw new DuplicateSlugError(slug);
570
+ }
571
+ const validation = this.renderService.validateTemplate(input.body);
572
+ if (!validation.valid) {
573
+ throw new TemplateSyntaxError(`Template validation failed: ${validation.errors.join("; ")}`, validation.errors);
574
+ }
575
+ const variables = input.variables || this.renderService.extractVariables(
576
+ `${input.subject} ${input.body} ${input.textBody || ""}`
577
+ );
578
+ return this.EmailTemplate.createTemplate({
579
+ ...input,
580
+ slug,
581
+ variables
582
+ });
583
+ }
584
+ async update(id, input) {
585
+ const template = await this.EmailTemplate.findById(id);
586
+ if (!template) return null;
587
+ if (input.body) {
588
+ const validation = this.renderService.validateTemplate(input.body);
589
+ if (!validation.valid) {
590
+ throw new TemplateSyntaxError(`Template validation failed: ${validation.errors.join("; ")}`, validation.errors);
591
+ }
592
+ }
593
+ if (input.body || input.subject || input.textBody) {
594
+ const subject = input.subject ?? template.subject;
595
+ const body = input.body ?? template.body;
596
+ const textBody = input.textBody ?? template.textBody;
597
+ input.variables = this.renderService.extractVariables(
598
+ `${subject} ${body} ${textBody || ""}`
599
+ );
600
+ }
601
+ const setFields = {};
602
+ for (const [key, value] of Object.entries(input)) {
603
+ if (value !== void 0 && UPDATEABLE_FIELDS.has(key)) {
604
+ setFields[key] = value;
605
+ }
606
+ }
607
+ const update = { $set: setFields };
608
+ if (input.body || input.subject || input.textBody) {
609
+ update["$inc"] = { version: 1 };
610
+ }
611
+ return this.EmailTemplate.findByIdAndUpdate(
612
+ id,
613
+ update,
614
+ { new: true }
615
+ );
616
+ }
617
+ async delete(id) {
618
+ const result = await this.EmailTemplate.findByIdAndDelete(id);
619
+ return result !== null;
620
+ }
621
+ async toggleActive(id) {
622
+ const template = await this.EmailTemplate.findById(id);
623
+ if (!template) return null;
624
+ template.isActive = !template.isActive;
625
+ await template.save();
626
+ return template;
627
+ }
628
+ async preview(id, sampleData) {
629
+ const template = await this.EmailTemplate.findById(id);
630
+ if (!template) return null;
631
+ return this.renderService.renderPreview(
632
+ template.subject,
633
+ template.body,
634
+ sampleData,
635
+ template.textBody
636
+ );
637
+ }
638
+ async previewRaw(subject, body, sampleData, textBody) {
639
+ return this.renderService.renderPreview(subject, body, sampleData, textBody);
640
+ }
641
+ async validate(body) {
642
+ const validation = this.renderService.validateTemplate(body);
643
+ const variables = this.renderService.extractVariables(body);
644
+ return { ...validation, variables };
645
+ }
646
+ async sendTestEmail(id, testEmail, sampleData) {
647
+ if (!this.config.adapters.sendTestEmail) {
648
+ return { success: false, error: "Test email sending not configured" };
649
+ }
650
+ const template = await this.EmailTemplate.findById(id);
651
+ if (!template) {
652
+ return { success: false, error: "Template not found" };
653
+ }
654
+ const rendered = this.renderService.renderSingle(
655
+ template.subject,
656
+ template.body,
657
+ sampleData,
658
+ template.textBody
659
+ );
660
+ try {
661
+ await this.config.adapters.sendTestEmail(
662
+ testEmail,
663
+ `[TEST] ${rendered.subject}`,
664
+ rendered.html,
665
+ rendered.text
666
+ );
667
+ return { success: true };
668
+ } catch (error) {
669
+ return { success: false, error: error.message };
670
+ }
671
+ }
672
+ };
673
+
674
+ // src/services/rule.service.ts
675
+ var UPDATEABLE_FIELDS2 = /* @__PURE__ */ new Set([
676
+ "name",
677
+ "description",
678
+ "sortOrder",
679
+ "target",
680
+ "templateId",
681
+ "sendOnce",
682
+ "resendAfterDays",
683
+ "cooldownDays",
684
+ "autoApprove",
685
+ "maxPerRun",
686
+ "bypassThrottle",
687
+ "emailType"
688
+ ]);
689
+ function validateRuleTemplateCompat(targetRole, targetPlatform, template) {
690
+ const templateAudience = template.audience;
691
+ const templatePlatform = template.platform;
692
+ if (templateAudience !== "all") {
693
+ if (targetRole === "all") {
694
+ return `Template "${template.name}" targets ${templateAudience} only, but rule targets all users`;
695
+ }
696
+ if (targetRole !== templateAudience) {
697
+ return `Template targets ${templateAudience}, but rule targets ${targetRole}`;
698
+ }
699
+ }
700
+ if (templatePlatform !== "both") {
701
+ if (targetPlatform === "both") {
702
+ return `Template is for ${templatePlatform} only, but rule targets all platforms`;
703
+ }
704
+ if (templatePlatform !== targetPlatform) {
705
+ return `Template is for ${templatePlatform}, but rule targets ${targetPlatform}`;
706
+ }
707
+ }
708
+ return null;
709
+ }
710
+ var RuleService = class {
711
+ constructor(EmailRule, EmailTemplate, EmailRuleRunLog, config) {
712
+ this.EmailRule = EmailRule;
713
+ this.EmailTemplate = EmailTemplate;
714
+ this.EmailRuleRunLog = EmailRuleRunLog;
715
+ this.config = config;
716
+ }
717
+ async list() {
718
+ return this.EmailRule.find().populate("templateId", "name slug").sort({ sortOrder: 1, createdAt: -1 });
719
+ }
720
+ async getById(id) {
721
+ return this.EmailRule.findById(id);
722
+ }
723
+ async create(input) {
724
+ const template = await this.EmailTemplate.findById(input.templateId);
725
+ if (!template) {
726
+ throw new TemplateNotFoundError(input.templateId);
727
+ }
728
+ const compatError = validateRuleTemplateCompat(
729
+ input.target.role,
730
+ input.target.platform,
731
+ template
732
+ );
733
+ if (compatError) {
734
+ throw new RuleTemplateIncompatibleError(compatError);
735
+ }
736
+ return this.EmailRule.createRule(input);
737
+ }
738
+ async update(id, input) {
739
+ const rule = await this.EmailRule.findById(id);
740
+ if (!rule) return null;
741
+ const templateId = input.templateId ?? rule.templateId.toString();
742
+ const targetRole = input.target?.role ?? rule.target.role;
743
+ const targetPlatform = input.target?.platform ?? rule.target.platform;
744
+ if (input.templateId || input.target) {
745
+ const template = await this.EmailTemplate.findById(templateId);
746
+ if (!template) {
747
+ throw new TemplateNotFoundError(templateId);
748
+ }
749
+ const compatError = validateRuleTemplateCompat(targetRole, targetPlatform, template);
750
+ if (compatError) {
751
+ throw new RuleTemplateIncompatibleError(compatError);
752
+ }
753
+ }
754
+ const setFields = {};
755
+ for (const [key, value] of Object.entries(input)) {
756
+ if (value !== void 0 && UPDATEABLE_FIELDS2.has(key)) {
757
+ setFields[key] = value;
758
+ }
759
+ }
760
+ return this.EmailRule.findByIdAndUpdate(
761
+ id,
762
+ { $set: setFields },
763
+ { new: true }
764
+ );
765
+ }
766
+ async delete(id) {
767
+ const rule = await this.EmailRule.findById(id);
768
+ if (!rule) return { deleted: false };
769
+ if (rule.totalSent > 0) {
770
+ rule.isActive = false;
771
+ await rule.save();
772
+ return { deleted: false, disabled: true };
773
+ }
774
+ await this.EmailRule.findByIdAndDelete(id);
775
+ return { deleted: true };
776
+ }
777
+ async toggleActive(id) {
778
+ const rule = await this.EmailRule.findById(id);
779
+ if (!rule) return null;
780
+ if (!rule.isActive) {
781
+ const template = await this.EmailTemplate.findById(rule.templateId);
782
+ if (!template) {
783
+ throw new TemplateNotFoundError(rule.templateId.toString());
784
+ }
785
+ if (!template.isActive) {
786
+ throw new RuleTemplateIncompatibleError("Cannot activate rule: linked template is inactive");
787
+ }
788
+ }
789
+ rule.isActive = !rule.isActive;
790
+ await rule.save();
791
+ return rule;
792
+ }
793
+ async dryRun(id) {
794
+ const rule = await this.EmailRule.findById(id);
795
+ if (!rule) {
796
+ throw new RuleNotFoundError(id);
797
+ }
798
+ const users = await this.config.adapters.queryUsers(rule.target, 5e4);
799
+ return { matchedCount: users.length, ruleId: id };
800
+ }
801
+ async getRunHistory(limit = 20) {
802
+ return this.EmailRuleRunLog.getRecent(limit);
803
+ }
804
+ };
805
+ var RedisLock = class {
806
+ constructor(redis, lockKey, ttlMs, logger) {
807
+ this.redis = redis;
808
+ this.lockKey = lockKey;
809
+ this.ttlMs = ttlMs;
810
+ this.logger = logger;
811
+ }
812
+ lockValue = "";
813
+ async acquire() {
814
+ this.lockValue = crypto.randomUUID();
815
+ const result = await this.redis.set(this.lockKey, this.lockValue, "PX", this.ttlMs, "NX");
816
+ return result === "OK";
817
+ }
818
+ async release() {
819
+ try {
820
+ const script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
821
+ await this.redis.eval(script, 1, this.lockKey, this.lockValue);
822
+ } catch (err) {
823
+ this.logger?.error("Failed to release lock", { error: err });
824
+ }
825
+ }
826
+ };
827
+
828
+ // src/services/rule-runner.service.ts
829
+ var MS_PER_DAY = 864e5;
830
+ var DEFAULT_LOCK_TTL_MS = 30 * 60 * 1e3;
831
+ var IDENTIFIER_CHUNK_SIZE = 50;
832
+ async function processInChunks(items, fn, chunkSize) {
833
+ const results = [];
834
+ for (let i = 0; i < items.length; i += chunkSize) {
835
+ const chunk = items.slice(i, i + chunkSize);
836
+ const chunkResults = await Promise.all(chunk.map(fn));
837
+ results.push(...chunkResults);
838
+ }
839
+ return results;
840
+ }
841
+ var defaultLogger = {
842
+ info: () => {
843
+ },
844
+ warn: () => {
845
+ },
846
+ error: () => {
847
+ }
848
+ };
849
+ var RuleRunnerService = class {
850
+ constructor(EmailRule, EmailTemplate, EmailRuleSend, EmailRuleRunLog, EmailThrottleConfig, config) {
851
+ this.EmailRule = EmailRule;
852
+ this.EmailTemplate = EmailTemplate;
853
+ this.EmailRuleSend = EmailRuleSend;
854
+ this.EmailRuleRunLog = EmailRuleRunLog;
855
+ this.EmailThrottleConfig = EmailThrottleConfig;
856
+ this.config = config;
857
+ const keyPrefix = config.redis.keyPrefix || "";
858
+ const lockTTL = config.options?.lockTTLMs || DEFAULT_LOCK_TTL_MS;
859
+ this.lock = new RedisLock(
860
+ config.redis.connection,
861
+ `${keyPrefix}email-rule-runner:lock`,
862
+ lockTTL,
863
+ config.logger
864
+ );
865
+ this.logger = config.logger || defaultLogger;
866
+ }
867
+ templateRenderer = new TemplateRenderService();
868
+ lock;
869
+ logger;
870
+ async runAllRules(triggeredBy = RUN_TRIGGER.Cron) {
871
+ if (this.config.options?.sendWindow) {
872
+ const { startHour, endHour, timezone } = this.config.options.sendWindow;
873
+ const now = /* @__PURE__ */ new Date();
874
+ const formatter = new Intl.DateTimeFormat("en-US", { hour: "numeric", hour12: false, timeZone: timezone });
875
+ const currentHour = parseInt(formatter.format(now), 10);
876
+ if (currentHour < startHour || currentHour >= endHour) {
877
+ this.logger.info("Outside send window, skipping run", { currentHour, startHour, endHour, timezone });
878
+ return;
879
+ }
880
+ }
881
+ const lockAcquired = await this.lock.acquire();
882
+ if (!lockAcquired) {
883
+ this.logger.warn("Rule runner already executing, skipping");
884
+ return;
885
+ }
886
+ const runStartTime = Date.now();
887
+ try {
888
+ const throttleConfig = await this.EmailThrottleConfig.getConfig();
889
+ const activeRules = await this.EmailRule.findActive();
890
+ this.config.hooks?.onRunStart?.({ rulesCount: activeRules.length, triggeredBy });
891
+ if (activeRules.length === 0) {
892
+ this.logger.info("No active rules to process");
893
+ await this.EmailRuleRunLog.create({
894
+ runAt: /* @__PURE__ */ new Date(),
895
+ triggeredBy,
896
+ duration: Date.now() - runStartTime,
897
+ rulesProcessed: 0,
898
+ totalStats: { matched: 0, sent: 0, skipped: 0, skippedByThrottle: 0, errors: 0 },
899
+ perRuleStats: []
900
+ });
901
+ return;
902
+ }
903
+ const templateIds = [...new Set(activeRules.map((r) => r.templateId.toString()))];
904
+ const templates = await this.EmailTemplate.find({ _id: { $in: templateIds } }).lean();
905
+ const templateMap = /* @__PURE__ */ new Map();
906
+ for (const t of templates) {
907
+ templateMap.set(t._id.toString(), t);
908
+ }
909
+ const recentSends = await this.EmailRuleSend.find({
910
+ sentAt: { $gte: new Date(Date.now() - 7 * MS_PER_DAY) }
911
+ }).lean();
912
+ const throttleMap = this.buildThrottleMap(recentSends);
913
+ const perRuleStats = [];
914
+ for (const rule of activeRules) {
915
+ const stats = await this.executeRule(rule, throttleMap, throttleConfig, templateMap);
916
+ perRuleStats.push({
917
+ ruleId: rule._id.toString(),
918
+ ruleName: rule.name,
919
+ ...stats
920
+ });
921
+ }
922
+ const totalStats = perRuleStats.reduce(
923
+ (acc, s) => ({
924
+ matched: acc.matched + s.matched,
925
+ sent: acc.sent + s.sent,
926
+ skipped: acc.skipped + s.skipped,
927
+ skippedByThrottle: acc.skippedByThrottle + s.skippedByThrottle,
928
+ errors: acc.errors + s.errors
929
+ }),
930
+ { matched: 0, sent: 0, skipped: 0, skippedByThrottle: 0, errors: 0 }
931
+ );
932
+ await this.EmailRuleRunLog.create({
933
+ runAt: /* @__PURE__ */ new Date(),
934
+ triggeredBy,
935
+ duration: Date.now() - runStartTime,
936
+ rulesProcessed: activeRules.length,
937
+ totalStats,
938
+ perRuleStats
939
+ });
940
+ this.config.hooks?.onRunComplete?.({ duration: Date.now() - runStartTime, totalStats, perRuleStats });
941
+ this.logger.info("Rule run completed", {
942
+ triggeredBy,
943
+ rulesProcessed: activeRules.length,
944
+ totalSent: totalStats.sent,
945
+ totalSkipped: totalStats.skipped,
946
+ duration: Date.now() - runStartTime
947
+ });
948
+ } finally {
949
+ await this.lock.release();
950
+ }
951
+ }
952
+ async executeRule(rule, throttleMap, throttleConfig, templateMap) {
953
+ const stats = { matched: 0, sent: 0, skipped: 0, skippedByThrottle: 0, errors: 0 };
954
+ const template = templateMap?.get(rule.templateId.toString()) ?? await this.EmailTemplate.findById(rule.templateId);
955
+ if (!template) {
956
+ this.logger.error(`Rule "${rule.name}": template ${rule.templateId} not found`);
957
+ stats.errors = 1;
958
+ return stats;
959
+ }
960
+ let users;
961
+ try {
962
+ users = await this.config.adapters.queryUsers(rule.target, rule.maxPerRun || this.config.options?.defaultMaxPerRun || 500);
963
+ } catch (err) {
964
+ this.logger.error(`Rule "${rule.name}": query failed`, { error: err });
965
+ stats.errors = 1;
966
+ return stats;
967
+ }
968
+ stats.matched = users.length;
969
+ this.config.hooks?.onRuleStart?.({ ruleId: rule._id.toString(), ruleName: rule.name, matchedCount: users.length });
970
+ if (users.length === 0) return stats;
971
+ const userIds = users.map((u) => u._id?.toString()).filter(Boolean);
972
+ const emails = users.map((u) => u.email).filter(Boolean);
973
+ const allRuleSends = await this.EmailRuleSend.find({ ruleId: rule._id, userId: { $in: userIds } }).sort({ sentAt: -1 }).lean();
974
+ const sendMap = /* @__PURE__ */ new Map();
975
+ for (const send of allRuleSends) {
976
+ const uid = send.userId.toString();
977
+ if (!sendMap.has(uid)) {
978
+ sendMap.set(uid, send);
979
+ }
980
+ }
981
+ const uniqueEmails = [...new Set(emails.map((e) => e.toLowerCase().trim()))];
982
+ const identifierResults = await processInChunks(
983
+ uniqueEmails,
984
+ async (email) => {
985
+ const result = await this.config.adapters.findIdentifier(email);
986
+ return result ? { email, ...result } : null;
987
+ },
988
+ IDENTIFIER_CHUNK_SIZE
989
+ );
990
+ const identifierMap = /* @__PURE__ */ new Map();
991
+ for (const result of identifierResults) {
992
+ if (result) {
993
+ identifierMap.set(result.email, { id: result.id, contactId: result.contactId });
994
+ }
995
+ }
996
+ const compiled = this.templateRenderer.compileBatch(
997
+ template.subject,
998
+ template.body,
999
+ template.textBody
1000
+ );
1001
+ const ruleId = rule._id.toString();
1002
+ const templateId = rule.templateId.toString();
1003
+ for (let i = 0; i < users.length; i++) {
1004
+ const user = users[i];
1005
+ try {
1006
+ const userId = user._id?.toString();
1007
+ const email = user.email;
1008
+ if (!userId || !email) {
1009
+ stats.skipped++;
1010
+ this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email: email || "unknown", status: "invalid" });
1011
+ continue;
1012
+ }
1013
+ const lastSend = sendMap.get(userId);
1014
+ if (lastSend) {
1015
+ if (rule.sendOnce && !rule.resendAfterDays) {
1016
+ stats.skipped++;
1017
+ this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
1018
+ continue;
1019
+ }
1020
+ if (rule.resendAfterDays) {
1021
+ const daysSince = (Date.now() - new Date(lastSend.sentAt).getTime()) / MS_PER_DAY;
1022
+ if (daysSince < rule.resendAfterDays) {
1023
+ stats.skipped++;
1024
+ this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
1025
+ continue;
1026
+ }
1027
+ } else {
1028
+ stats.skipped++;
1029
+ this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
1030
+ continue;
1031
+ }
1032
+ }
1033
+ const identifier = identifierMap.get(email.toLowerCase().trim());
1034
+ if (!identifier) {
1035
+ stats.skipped++;
1036
+ this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "invalid" });
1037
+ continue;
1038
+ }
1039
+ if (!this.checkThrottle(rule, userId, email, throttleMap, throttleConfig, stats)) continue;
1040
+ const agentSelection = await this.config.adapters.selectAgent(identifier.id, { ruleId, templateId });
1041
+ if (!agentSelection) {
1042
+ stats.skipped++;
1043
+ this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
1044
+ continue;
1045
+ }
1046
+ const templateData = this.config.adapters.resolveData(user);
1047
+ const rendered = this.templateRenderer.renderFromCompiled(compiled, templateData);
1048
+ await this.config.adapters.sendEmail({
1049
+ identifierId: identifier.id,
1050
+ contactId: identifier.contactId,
1051
+ accountId: agentSelection.accountId,
1052
+ subject: rendered.subject,
1053
+ htmlBody: rendered.html,
1054
+ textBody: rendered.text,
1055
+ ruleId,
1056
+ autoApprove: rule.autoApprove ?? true
1057
+ });
1058
+ await this.EmailRuleSend.logSend(
1059
+ ruleId,
1060
+ userId,
1061
+ identifier.id,
1062
+ void 0,
1063
+ { status: "sent", accountId: agentSelection.accountId, subject: rendered.subject }
1064
+ );
1065
+ const current = throttleMap.get(userId) || { today: 0, thisWeek: 0, lastSentDate: null };
1066
+ throttleMap.set(userId, {
1067
+ today: current.today + 1,
1068
+ thisWeek: current.thisWeek + 1,
1069
+ lastSentDate: /* @__PURE__ */ new Date()
1070
+ });
1071
+ stats.sent++;
1072
+ this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "sent" });
1073
+ if (i < users.length - 1) {
1074
+ const delayMs = this.config.options?.delayBetweenSendsMs || 0;
1075
+ const jitterMs = this.config.options?.jitterMs || 0;
1076
+ if (delayMs > 0 || jitterMs > 0) {
1077
+ const totalDelay = delayMs + Math.floor(Math.random() * (jitterMs + 1));
1078
+ if (totalDelay > 0) await new Promise((resolve) => setTimeout(resolve, totalDelay));
1079
+ }
1080
+ }
1081
+ } catch (err) {
1082
+ stats.errors++;
1083
+ this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email: user.email || "unknown", status: "error" });
1084
+ this.logger.error(`Rule "${rule.name}" failed for user ${user._id?.toString()}`, { error: err });
1085
+ }
1086
+ }
1087
+ await this.EmailRule.findByIdAndUpdate(rule._id, {
1088
+ $set: { lastRunAt: /* @__PURE__ */ new Date(), lastRunStats: stats },
1089
+ $inc: { totalSent: stats.sent, totalSkipped: stats.skipped }
1090
+ });
1091
+ this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats });
1092
+ return stats;
1093
+ }
1094
+ checkThrottle(rule, userId, email, throttleMap, config, stats) {
1095
+ if (rule.emailType === EMAIL_TYPE.Transactional || rule.bypassThrottle) return true;
1096
+ const userThrottle = throttleMap.get(userId) || { today: 0, thisWeek: 0, lastSentDate: null };
1097
+ if (userThrottle.today >= config.maxPerUserPerDay) {
1098
+ stats.skippedByThrottle++;
1099
+ this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled" });
1100
+ return false;
1101
+ }
1102
+ if (userThrottle.thisWeek >= config.maxPerUserPerWeek) {
1103
+ stats.skippedByThrottle++;
1104
+ this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled" });
1105
+ return false;
1106
+ }
1107
+ if (userThrottle.lastSentDate) {
1108
+ const daysSinceLastSend = (Date.now() - userThrottle.lastSentDate.getTime()) / MS_PER_DAY;
1109
+ if (daysSinceLastSend < config.minGapDays) {
1110
+ stats.skippedByThrottle++;
1111
+ this.config.hooks?.onSend?.({ ruleId: rule._id.toString(), ruleName: rule.name, email, status: "throttled" });
1112
+ return false;
1113
+ }
1114
+ }
1115
+ return true;
1116
+ }
1117
+ getTodayStart() {
1118
+ const timezone = this.config.options?.sendWindow?.timezone;
1119
+ if (timezone) {
1120
+ const now = /* @__PURE__ */ new Date();
1121
+ const parts = new Intl.DateTimeFormat("en-US", {
1122
+ timeZone: timezone,
1123
+ year: "numeric",
1124
+ month: "2-digit",
1125
+ day: "2-digit",
1126
+ hour: "2-digit",
1127
+ minute: "2-digit",
1128
+ second: "2-digit",
1129
+ hour12: false
1130
+ }).formatToParts(now);
1131
+ const get = (type) => parts.find((p) => p.type === type)?.value || "0";
1132
+ const tzNowMs = Date.UTC(
1133
+ parseInt(get("year")),
1134
+ parseInt(get("month")) - 1,
1135
+ parseInt(get("day")),
1136
+ parseInt(get("hour")),
1137
+ parseInt(get("minute")),
1138
+ parseInt(get("second"))
1139
+ );
1140
+ const tzMidnightMs = Date.UTC(
1141
+ parseInt(get("year")),
1142
+ parseInt(get("month")) - 1,
1143
+ parseInt(get("day"))
1144
+ );
1145
+ const offsetMs = now.getTime() - tzNowMs;
1146
+ return new Date(tzMidnightMs + offsetMs);
1147
+ }
1148
+ const todayStart = /* @__PURE__ */ new Date();
1149
+ todayStart.setHours(0, 0, 0, 0);
1150
+ return todayStart;
1151
+ }
1152
+ buildThrottleMap(recentSends) {
1153
+ const map = /* @__PURE__ */ new Map();
1154
+ const todayStart = this.getTodayStart();
1155
+ for (const send of recentSends) {
1156
+ const key = send.userId.toString();
1157
+ const current = map.get(key) || { today: 0, thisWeek: 0, lastSentDate: null };
1158
+ const sentAt = new Date(send.sentAt);
1159
+ current.thisWeek++;
1160
+ if (sentAt >= todayStart) {
1161
+ current.today++;
1162
+ }
1163
+ if (!current.lastSentDate || sentAt > current.lastSentDate) {
1164
+ current.lastSentDate = sentAt;
1165
+ }
1166
+ map.set(key, current);
1167
+ }
1168
+ return map;
1169
+ }
1170
+ };
1171
+
1172
+ // src/utils/express-helpers.ts
1173
+ function getParam(req, name) {
1174
+ const val = req.params[name];
1175
+ return Array.isArray(val) ? val[0] : val;
1176
+ }
1177
+
1178
+ // src/controllers/template.controller.ts
1179
+ function isValidValue(allowed, value) {
1180
+ return typeof value === "string" && allowed.includes(value);
1181
+ }
1182
+ function getErrorStatus(message) {
1183
+ if (message.includes("already exists") || message.includes("validation failed")) return 400;
1184
+ if (message.includes("not found")) return 404;
1185
+ return 500;
1186
+ }
1187
+ function createTemplateController(templateService, options) {
1188
+ const platformValues = options?.platforms;
1189
+ const validCategories = options?.categories || Object.values(TEMPLATE_CATEGORY);
1190
+ const validAudiences = options?.audiences || Object.values(TEMPLATE_AUDIENCE);
1191
+ async function list(req, res) {
1192
+ try {
1193
+ const { category, audience, platform, isActive } = req.query;
1194
+ const templates = await templateService.list({
1195
+ category,
1196
+ audience,
1197
+ platform,
1198
+ isActive: isActive !== void 0 ? isActive === "true" : void 0
1199
+ });
1200
+ res.json({ success: true, data: { templates } });
1201
+ } catch (error) {
1202
+ const message = error instanceof Error ? error.message : "Unknown error";
1203
+ res.status(500).json({ success: false, error: message });
1204
+ }
1205
+ }
1206
+ async function getById(req, res) {
1207
+ try {
1208
+ const template = await templateService.getById(getParam(req, "id"));
1209
+ if (!template) {
1210
+ return res.status(404).json({ success: false, error: "Template not found" });
1211
+ }
1212
+ res.json({ success: true, data: { template } });
1213
+ } catch (error) {
1214
+ const message = error instanceof Error ? error.message : "Unknown error";
1215
+ res.status(500).json({ success: false, error: message });
1216
+ }
1217
+ }
1218
+ async function create(req, res) {
1219
+ try {
1220
+ const { name, subject, body, category, audience, platform } = req.body;
1221
+ if (!name || !subject || !body || !category || !audience || !platform) {
1222
+ return res.status(400).json({ success: false, error: "name, subject, body, category, audience, and platform are required" });
1223
+ }
1224
+ if (!isValidValue(validCategories, category)) {
1225
+ return res.status(400).json({ success: false, error: `Invalid category. Must be one of: ${validCategories.join(", ")}` });
1226
+ }
1227
+ if (!isValidValue(validAudiences, audience)) {
1228
+ return res.status(400).json({ success: false, error: `Invalid audience. Must be one of: ${validAudiences.join(", ")}` });
1229
+ }
1230
+ if (platformValues && !platformValues.includes(platform)) {
1231
+ return res.status(400).json({ success: false, error: `Invalid platform. Must be one of: ${platformValues.join(", ")}` });
1232
+ }
1233
+ const template = await templateService.create(req.body);
1234
+ res.status(201).json({ success: true, data: { template } });
1235
+ } catch (error) {
1236
+ const message = error instanceof Error ? error.message : "Unknown error";
1237
+ res.status(getErrorStatus(message)).json({ success: false, error: message });
1238
+ }
1239
+ }
1240
+ async function update(req, res) {
1241
+ try {
1242
+ const template = await templateService.update(getParam(req, "id"), req.body);
1243
+ if (!template) {
1244
+ return res.status(404).json({ success: false, error: "Template not found" });
1245
+ }
1246
+ res.json({ success: true, data: { template } });
1247
+ } catch (error) {
1248
+ const message = error instanceof Error ? error.message : "Unknown error";
1249
+ res.status(getErrorStatus(message)).json({ success: false, error: message });
1250
+ }
1251
+ }
1252
+ async function remove(req, res) {
1253
+ try {
1254
+ const deleted = await templateService.delete(getParam(req, "id"));
1255
+ if (!deleted) {
1256
+ return res.status(404).json({ success: false, error: "Template not found" });
1257
+ }
1258
+ res.json({ success: true });
1259
+ } catch (error) {
1260
+ const message = error instanceof Error ? error.message : "Unknown error";
1261
+ res.status(500).json({ success: false, error: message });
1262
+ }
1263
+ }
1264
+ async function toggleActive(req, res) {
1265
+ try {
1266
+ const template = await templateService.toggleActive(getParam(req, "id"));
1267
+ if (!template) {
1268
+ return res.status(404).json({ success: false, error: "Template not found" });
1269
+ }
1270
+ res.json({ success: true, data: { template } });
1271
+ } catch (error) {
1272
+ const message = error instanceof Error ? error.message : "Unknown error";
1273
+ res.status(500).json({ success: false, error: message });
1274
+ }
1275
+ }
1276
+ async function preview(req, res) {
1277
+ try {
1278
+ const { sampleData } = req.body;
1279
+ const result = await templateService.preview(getParam(req, "id"), sampleData || {});
1280
+ if (!result) {
1281
+ return res.status(404).json({ success: false, error: "Template not found" });
1282
+ }
1283
+ res.json({ success: true, data: result });
1284
+ } catch (error) {
1285
+ const message = error instanceof Error ? error.message : "Unknown error";
1286
+ res.status(500).json({ success: false, error: message });
1287
+ }
1288
+ }
1289
+ async function previewRaw(req, res) {
1290
+ try {
1291
+ const { subject, body, textBody, sampleData } = req.body;
1292
+ if (!subject || !body) {
1293
+ return res.status(400).json({ success: false, error: "subject and body are required" });
1294
+ }
1295
+ const result = await templateService.previewRaw(subject, body, sampleData || {}, textBody);
1296
+ res.json({ success: true, data: result });
1297
+ } catch (error) {
1298
+ const message = error instanceof Error ? error.message : "Unknown error";
1299
+ res.status(500).json({ success: false, error: message });
1300
+ }
1301
+ }
1302
+ async function validate(req, res) {
1303
+ try {
1304
+ const { body: templateBody } = req.body;
1305
+ if (!templateBody) {
1306
+ return res.status(400).json({ success: false, error: "body is required" });
1307
+ }
1308
+ const result = await templateService.validate(templateBody);
1309
+ res.json({ success: true, data: result });
1310
+ } catch (error) {
1311
+ const message = error instanceof Error ? error.message : "Unknown error";
1312
+ res.status(500).json({ success: false, error: message });
1313
+ }
1314
+ }
1315
+ async function sendTestEmail(req, res) {
1316
+ try {
1317
+ const { testEmail, sampleData } = req.body;
1318
+ if (!testEmail) {
1319
+ return res.status(400).json({ success: false, error: "testEmail is required" });
1320
+ }
1321
+ const result = await templateService.sendTestEmail(getParam(req, "id"), testEmail, sampleData || {});
1322
+ if (!result.success) {
1323
+ return res.status(400).json({ success: false, error: result.error });
1324
+ }
1325
+ res.json({ success: true });
1326
+ } catch (error) {
1327
+ const message = error instanceof Error ? error.message : "Unknown error";
1328
+ res.status(500).json({ success: false, error: message });
1329
+ }
1330
+ }
1331
+ return { list, getById, create, update, remove, toggleActive, preview, previewRaw, validate, sendTestEmail };
1332
+ }
1333
+
1334
+ // src/controllers/rule.controller.ts
1335
+ function isValidValue2(allowed, value) {
1336
+ return typeof value === "string" && allowed.includes(value);
1337
+ }
1338
+ function getErrorStatus2(message) {
1339
+ if (message.includes("not found")) return 404;
1340
+ if (message.includes("mismatch") || message.includes("validation failed") || message.includes("Cannot activate")) return 400;
1341
+ return 500;
1342
+ }
1343
+ function createRuleController(ruleService, options) {
1344
+ const platformValues = options?.platforms;
1345
+ const validAudiences = options?.audiences || Object.values(TEMPLATE_AUDIENCE);
1346
+ const validEmailTypes = Object.values(EMAIL_TYPE);
1347
+ async function list(_req, res) {
1348
+ try {
1349
+ const rules = await ruleService.list();
1350
+ res.json({ success: true, data: { rules } });
1351
+ } catch (error) {
1352
+ const message = error instanceof Error ? error.message : "Unknown error";
1353
+ res.status(500).json({ success: false, error: message });
1354
+ }
1355
+ }
1356
+ async function getById(req, res) {
1357
+ try {
1358
+ const rule = await ruleService.getById(getParam(req, "id"));
1359
+ if (!rule) {
1360
+ return res.status(404).json({ success: false, error: "Rule not found" });
1361
+ }
1362
+ res.json({ success: true, data: { rule } });
1363
+ } catch (error) {
1364
+ const message = error instanceof Error ? error.message : "Unknown error";
1365
+ res.status(500).json({ success: false, error: message });
1366
+ }
1367
+ }
1368
+ async function create(req, res) {
1369
+ try {
1370
+ const { name, target, templateId } = req.body;
1371
+ if (!name || !target || !templateId) {
1372
+ return res.status(400).json({ success: false, error: "name, target, and templateId are required" });
1373
+ }
1374
+ if (!target.role || !isValidValue2(validAudiences, target.role)) {
1375
+ return res.status(400).json({ success: false, error: `Invalid target.role. Must be one of: ${validAudiences.join(", ")}` });
1376
+ }
1377
+ if (platformValues && !platformValues.includes(target.platform)) {
1378
+ return res.status(400).json({ success: false, error: `Invalid target.platform. Must be one of: ${platformValues.join(", ")}` });
1379
+ }
1380
+ if (!Array.isArray(target.conditions)) {
1381
+ return res.status(400).json({ success: false, error: "target.conditions must be an array" });
1382
+ }
1383
+ if (req.body.emailType && !isValidValue2(validEmailTypes, req.body.emailType)) {
1384
+ return res.status(400).json({ success: false, error: `Invalid emailType. Must be one of: ${validEmailTypes.join(", ")}` });
1385
+ }
1386
+ const rule = await ruleService.create(req.body);
1387
+ res.status(201).json({ success: true, data: { rule } });
1388
+ } catch (error) {
1389
+ const message = error instanceof Error ? error.message : "Unknown error";
1390
+ res.status(getErrorStatus2(message)).json({ success: false, error: message });
1391
+ }
1392
+ }
1393
+ async function update(req, res) {
1394
+ try {
1395
+ const { target, emailType } = req.body;
1396
+ if (target?.role && !isValidValue2(validAudiences, target.role)) {
1397
+ return res.status(400).json({ success: false, error: `Invalid target.role. Must be one of: ${validAudiences.join(", ")}` });
1398
+ }
1399
+ if (target?.platform && platformValues && !platformValues.includes(target.platform)) {
1400
+ return res.status(400).json({ success: false, error: `Invalid target.platform. Must be one of: ${platformValues.join(", ")}` });
1401
+ }
1402
+ if (target?.conditions && !Array.isArray(target.conditions)) {
1403
+ return res.status(400).json({ success: false, error: "target.conditions must be an array" });
1404
+ }
1405
+ if (emailType && !isValidValue2(validEmailTypes, emailType)) {
1406
+ return res.status(400).json({ success: false, error: `Invalid emailType. Must be one of: ${validEmailTypes.join(", ")}` });
1407
+ }
1408
+ const rule = await ruleService.update(getParam(req, "id"), req.body);
1409
+ if (!rule) {
1410
+ return res.status(404).json({ success: false, error: "Rule not found" });
1411
+ }
1412
+ res.json({ success: true, data: { rule } });
1413
+ } catch (error) {
1414
+ const message = error instanceof Error ? error.message : "Unknown error";
1415
+ res.status(getErrorStatus2(message)).json({ success: false, error: message });
1416
+ }
1417
+ }
1418
+ async function remove(req, res) {
1419
+ try {
1420
+ const result = await ruleService.delete(getParam(req, "id"));
1421
+ if (!result.deleted && !result.disabled) {
1422
+ return res.status(404).json({ success: false, error: "Rule not found" });
1423
+ }
1424
+ res.json({ success: true, data: result });
1425
+ } catch (error) {
1426
+ const message = error instanceof Error ? error.message : "Unknown error";
1427
+ res.status(500).json({ success: false, error: message });
1428
+ }
1429
+ }
1430
+ async function toggleActive(req, res) {
1431
+ try {
1432
+ const rule = await ruleService.toggleActive(getParam(req, "id"));
1433
+ if (!rule) {
1434
+ return res.status(404).json({ success: false, error: "Rule not found" });
1435
+ }
1436
+ res.json({ success: true, data: { rule } });
1437
+ } catch (error) {
1438
+ const message = error instanceof Error ? error.message : "Unknown error";
1439
+ res.status(getErrorStatus2(message)).json({ success: false, error: message });
1440
+ }
1441
+ }
1442
+ async function dryRun(req, res) {
1443
+ try {
1444
+ const result = await ruleService.dryRun(getParam(req, "id"));
1445
+ res.json({ success: true, data: result });
1446
+ } catch (error) {
1447
+ const message = error instanceof Error ? error.message : "Unknown error";
1448
+ res.status(getErrorStatus2(message)).json({ success: false, error: message });
1449
+ }
1450
+ }
1451
+ async function runHistory(req, res) {
1452
+ try {
1453
+ const limitParam = req.query.limit;
1454
+ const limit = parseInt(String(Array.isArray(limitParam) ? limitParam[0] : limitParam), 10) || 20;
1455
+ const logs = await ruleService.getRunHistory(limit);
1456
+ res.json({ success: true, data: { logs } });
1457
+ } catch (error) {
1458
+ const message = error instanceof Error ? error.message : "Unknown error";
1459
+ res.status(500).json({ success: false, error: message });
1460
+ }
1461
+ }
1462
+ return { list, getById, create, update, remove, toggleActive, dryRun, runHistory };
1463
+ }
1464
+
1465
+ // src/controllers/runner.controller.ts
1466
+ var defaultLogger2 = {
1467
+ info: () => {
1468
+ },
1469
+ warn: () => {
1470
+ },
1471
+ error: () => {
1472
+ }
1473
+ };
1474
+ function createRunnerController(runnerService, EmailRuleRunLog, logger) {
1475
+ const log = logger || defaultLogger2;
1476
+ async function triggerManualRun(_req, res) {
1477
+ runnerService.runAllRules(RUN_TRIGGER.Manual).catch((err) => {
1478
+ log.error("Manual rule run failed", { error: err });
1479
+ });
1480
+ res.json({ success: true, data: { message: "Rule run triggered" } });
1481
+ }
1482
+ async function getLatestRun(_req, res) {
1483
+ try {
1484
+ const latestRun = await EmailRuleRunLog.findOne().sort({ runAt: -1 });
1485
+ res.json({ success: true, data: { latestRun } });
1486
+ } catch (error) {
1487
+ const message = error instanceof Error ? error.message : "Unknown error";
1488
+ res.status(500).json({ success: false, error: message });
1489
+ }
1490
+ }
1491
+ return { triggerManualRun, getLatestRun };
1492
+ }
1493
+
1494
+ // src/controllers/settings.controller.ts
1495
+ function createSettingsController(EmailThrottleConfig) {
1496
+ async function getThrottleConfig(_req, res) {
1497
+ try {
1498
+ const config = await EmailThrottleConfig.getConfig();
1499
+ res.json({ success: true, data: { config } });
1500
+ } catch (error) {
1501
+ const message = error instanceof Error ? error.message : "Unknown error";
1502
+ res.status(500).json({ success: false, error: message });
1503
+ }
1504
+ }
1505
+ async function updateThrottleConfig(req, res) {
1506
+ try {
1507
+ const { maxPerUserPerDay, maxPerUserPerWeek, minGapDays } = req.body;
1508
+ const updates = {};
1509
+ if (maxPerUserPerDay !== void 0) {
1510
+ if (!Number.isInteger(maxPerUserPerDay) || maxPerUserPerDay < 1) {
1511
+ return res.status(400).json({ success: false, error: "maxPerUserPerDay must be a positive integer" });
1512
+ }
1513
+ updates.maxPerUserPerDay = maxPerUserPerDay;
1514
+ }
1515
+ if (maxPerUserPerWeek !== void 0) {
1516
+ if (!Number.isInteger(maxPerUserPerWeek) || maxPerUserPerWeek < 1) {
1517
+ return res.status(400).json({ success: false, error: "maxPerUserPerWeek must be a positive integer" });
1518
+ }
1519
+ updates.maxPerUserPerWeek = maxPerUserPerWeek;
1520
+ }
1521
+ if (minGapDays !== void 0) {
1522
+ if (!Number.isInteger(minGapDays) || minGapDays < 0) {
1523
+ return res.status(400).json({ success: false, error: "minGapDays must be a non-negative integer" });
1524
+ }
1525
+ updates.minGapDays = minGapDays;
1526
+ }
1527
+ if (Object.keys(updates).length === 0) {
1528
+ return res.status(400).json({ success: false, error: "No valid fields to update" });
1529
+ }
1530
+ const config = await EmailThrottleConfig.getConfig();
1531
+ const finalDaily = updates.maxPerUserPerDay ?? config.maxPerUserPerDay;
1532
+ const finalWeekly = updates.maxPerUserPerWeek ?? config.maxPerUserPerWeek;
1533
+ if (finalWeekly < finalDaily) {
1534
+ return res.status(400).json({ success: false, error: "maxPerUserPerWeek must be >= maxPerUserPerDay" });
1535
+ }
1536
+ const updated = await EmailThrottleConfig.findByIdAndUpdate(
1537
+ config._id,
1538
+ { $set: updates },
1539
+ { new: true }
1540
+ );
1541
+ res.json({ success: true, data: { config: updated } });
1542
+ } catch (error) {
1543
+ const message = error instanceof Error ? error.message : "Unknown error";
1544
+ res.status(500).json({ success: false, error: message });
1545
+ }
1546
+ }
1547
+ return { getThrottleConfig, updateThrottleConfig };
1548
+ }
1549
+
1550
+ // src/routes/index.ts
1551
+ function createRoutes(deps) {
1552
+ const router = Router();
1553
+ const templateCtrl = createTemplateController(deps.templateService, {
1554
+ platforms: deps.platformValues,
1555
+ categories: deps.categoryValues,
1556
+ audiences: deps.audienceValues
1557
+ });
1558
+ const ruleCtrl = createRuleController(deps.ruleService, {
1559
+ platforms: deps.platformValues,
1560
+ audiences: deps.audienceValues
1561
+ });
1562
+ const runnerCtrl = createRunnerController(deps.runnerService, deps.EmailRuleRunLog, deps.logger);
1563
+ const settingsCtrl = createSettingsController(deps.EmailThrottleConfig);
1564
+ const templateRouter = Router();
1565
+ templateRouter.get("/", templateCtrl.list);
1566
+ templateRouter.post("/", templateCtrl.create);
1567
+ templateRouter.post("/validate", templateCtrl.validate);
1568
+ templateRouter.post("/preview", templateCtrl.previewRaw);
1569
+ templateRouter.get("/:id", templateCtrl.getById);
1570
+ templateRouter.put("/:id", templateCtrl.update);
1571
+ templateRouter.delete("/:id", templateCtrl.remove);
1572
+ templateRouter.patch("/:id/toggle", templateCtrl.toggleActive);
1573
+ templateRouter.post("/:id/preview", templateCtrl.preview);
1574
+ templateRouter.post("/:id/test-email", templateCtrl.sendTestEmail);
1575
+ const ruleRouter = Router();
1576
+ ruleRouter.get("/", ruleCtrl.list);
1577
+ ruleRouter.post("/", ruleCtrl.create);
1578
+ ruleRouter.get("/run-history", ruleCtrl.runHistory);
1579
+ ruleRouter.get("/:id", ruleCtrl.getById);
1580
+ ruleRouter.patch("/:id", ruleCtrl.update);
1581
+ ruleRouter.delete("/:id", ruleCtrl.remove);
1582
+ ruleRouter.patch("/:id/toggle", ruleCtrl.toggleActive);
1583
+ ruleRouter.post("/:id/dry-run", ruleCtrl.dryRun);
1584
+ const runnerRouter = Router();
1585
+ runnerRouter.post("/", runnerCtrl.triggerManualRun);
1586
+ runnerRouter.get("/status", runnerCtrl.getLatestRun);
1587
+ const settingsRouter = Router();
1588
+ settingsRouter.get("/throttle", settingsCtrl.getThrottleConfig);
1589
+ settingsRouter.patch("/throttle", settingsCtrl.updateThrottleConfig);
1590
+ router.use("/templates", templateRouter);
1591
+ router.use("/rules", ruleRouter);
1592
+ router.use("/runner", runnerRouter);
1593
+ router.use("/settings", settingsRouter);
1594
+ return router;
1595
+ }
1596
+ var configSchema = z.object({
1597
+ db: baseDbSchema,
1598
+ redis: baseRedisSchema,
1599
+ adapters: z.object({
1600
+ queryUsers: z.function(),
1601
+ resolveData: z.function(),
1602
+ sendEmail: z.function(),
1603
+ selectAgent: z.function(),
1604
+ findIdentifier: z.function(),
1605
+ sendTestEmail: z.function().optional()
1606
+ }),
1607
+ platforms: z.array(z.string()).optional(),
1608
+ audiences: z.array(z.string()).optional(),
1609
+ categories: z.array(z.string()).optional(),
1610
+ logger: loggerSchema.optional(),
1611
+ options: z.object({
1612
+ lockTTLMs: z.number().positive().optional(),
1613
+ defaultMaxPerRun: z.number().positive().optional(),
1614
+ sendWindow: z.object({
1615
+ startHour: z.number().min(0).max(23),
1616
+ endHour: z.number().min(0).max(23),
1617
+ timezone: z.string()
1618
+ }).optional(),
1619
+ delayBetweenSendsMs: z.number().min(0).optional(),
1620
+ jitterMs: z.number().min(0).optional()
1621
+ }).optional(),
1622
+ hooks: z.object({
1623
+ onRunStart: z.function().optional(),
1624
+ onRuleStart: z.function().optional(),
1625
+ onSend: z.function().optional(),
1626
+ onRuleComplete: z.function().optional(),
1627
+ onRunComplete: z.function().optional()
1628
+ }).optional()
1629
+ });
1630
+ function validateConfig(raw) {
1631
+ const result = configSchema.safeParse(raw);
1632
+ if (!result.success) {
1633
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
1634
+ throw new ConfigValidationError(
1635
+ `Invalid EmailRuleEngineConfig:
1636
+ ${issues}`,
1637
+ result.error.issues[0]?.path.join(".") ?? ""
1638
+ );
1639
+ }
1640
+ }
1641
+
1642
+ // src/index.ts
1643
+ function createEmailRuleEngine(config) {
1644
+ validateConfig(config);
1645
+ const conn = config.db.connection;
1646
+ const prefix = config.db.collectionPrefix || "";
1647
+ const EmailTemplate = conn.model(
1648
+ `${prefix}EmailTemplate`,
1649
+ createEmailTemplateSchema(config.platforms, config.audiences, config.categories, prefix)
1650
+ );
1651
+ const EmailRule = conn.model(
1652
+ `${prefix}EmailRule`,
1653
+ createEmailRuleSchema(config.platforms, config.audiences, prefix)
1654
+ );
1655
+ const EmailRuleSend = conn.model(
1656
+ `${prefix}EmailRuleSend`,
1657
+ createEmailRuleSendSchema(prefix)
1658
+ );
1659
+ const EmailRuleRunLog = conn.model(
1660
+ `${prefix}EmailRuleRunLog`,
1661
+ createEmailRuleRunLogSchema(prefix)
1662
+ );
1663
+ const EmailThrottleConfig = conn.model(
1664
+ `${prefix}EmailThrottleConfig`,
1665
+ createEmailThrottleConfigSchema(prefix)
1666
+ );
1667
+ const templateService = new TemplateService(EmailTemplate, config);
1668
+ const ruleService = new RuleService(EmailRule, EmailTemplate, EmailRuleRunLog, config);
1669
+ const runnerService = new RuleRunnerService(
1670
+ EmailRule,
1671
+ EmailTemplate,
1672
+ EmailRuleSend,
1673
+ EmailRuleRunLog,
1674
+ EmailThrottleConfig,
1675
+ config
1676
+ );
1677
+ const routes = createRoutes({
1678
+ templateService,
1679
+ ruleService,
1680
+ runnerService,
1681
+ EmailRuleRunLog,
1682
+ EmailThrottleConfig,
1683
+ platformValues: config.platforms,
1684
+ categoryValues: config.categories,
1685
+ audienceValues: config.audiences,
1686
+ logger: config.logger
1687
+ });
1688
+ return {
1689
+ routes,
1690
+ runner: runnerService,
1691
+ templateService,
1692
+ ruleService,
1693
+ models: { EmailTemplate, EmailRule, EmailRuleSend, EmailRuleRunLog, EmailThrottleConfig }
1694
+ };
1695
+ }
1696
+
1697
+ export { AlxEmailError, ConfigValidationError, DuplicateSlugError, EMAIL_SEND_STATUS, EMAIL_TYPE, LockAcquisitionError, RULE_OPERATOR, RUN_TRIGGER, RedisLock, RuleNotFoundError, RuleRunnerService, RuleService, RuleTemplateIncompatibleError, TEMPLATE_AUDIENCE, TEMPLATE_CATEGORY, THROTTLE_WINDOW, TemplateNotFoundError, TemplateRenderService, TemplateService, TemplateSyntaxError, createEmailRuleEngine, createEmailRuleRunLogSchema, createEmailRuleSchema, createEmailRuleSendSchema, createEmailTemplateSchema, createEmailThrottleConfigSchema, validateConfig };
1698
+ //# sourceMappingURL=index.mjs.map
1699
+ //# sourceMappingURL=index.mjs.map