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