@stamhoofd/backend 2.114.0 → 2.115.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.
@@ -4,11 +4,11 @@ import { assertSort, CountFilteredRequest, Document as DocumentStruct, getSortFi
4
4
 
5
5
  import { Decoder } from '@simonbackx/simple-encoding';
6
6
  import { applySQLSorter, compileToSQLFilter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
7
- import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
8
- import { Context } from '../../../../helpers/Context';
9
- import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper';
10
- import { documentFilterCompilers } from '../../../../sql-filters/documents';
11
- import { documentSorters } from '../../../../sql-sorters/documents';
7
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
8
+ import { Context } from '../../../../helpers/Context.js';
9
+ import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper.js';
10
+ import { documentFilterCompilers } from '../../../../sql-filters/documents.js';
11
+ import { documentSorters } from '../../../../sql-sorters/documents.js';
12
12
 
13
13
  type Params = Record<string, never>;
14
14
  type Query = LimitedFilteredRequest;
@@ -45,6 +45,7 @@ export class GetDocumentsEndpoint extends Endpoint<Params, Query, Body, Response
45
45
 
46
46
  const query = SQL
47
47
  .select(SQL.wildcard(documentTable))
48
+ .setMaxExecutionTime(15 * 1000)
48
49
  .from(SQL.table(documentTable))
49
50
  .where('organizationId', organization.id);
50
51
 
@@ -124,8 +124,23 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
124
124
  shouldUpdateSetupSteps = true;
125
125
  }
126
126
  organization.privateMeta.roles = request.body.privateMeta.roles.applyTo(organization.privateMeta.roles);
127
+ if (request.body.privateMeta.roles) {
128
+ for (const role of organization.privateMeta.roles) {
129
+ role.compress();
130
+ }
131
+ }
127
132
  organization.privateMeta.responsibilities = request.body.privateMeta.responsibilities.applyTo(organization.privateMeta.responsibilities);
133
+ if (request.body.privateMeta.responsibilities) {
134
+ for (const responsibility of organization.privateMeta.responsibilities) {
135
+ responsibility.compress();
136
+ }
137
+ }
128
138
  organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles);
139
+ if (request.body.privateMeta.inheritedResponsibilityRoles) {
140
+ for (const role of organization.privateMeta.inheritedResponsibilityRoles) {
141
+ role.compress();
142
+ }
143
+ }
129
144
  organization.privateMeta.privateKey = request.body.privateMeta.privateKey ?? organization.privateMeta.privateKey;
130
145
  organization.privateMeta.featureFlags = patchObject(organization.privateMeta.featureFlags, request.body.privateMeta.featureFlags);
131
146
  organization.privateMeta.balanceNotificationSettings = patchObject(organization.privateMeta.balanceNotificationSettings, request.body.privateMeta.balanceNotificationSettings);
@@ -0,0 +1,77 @@
1
+ import { AutoEncoder, field, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { CpuService } from '../../services/CpuService.js';
5
+
6
+ type Params = Record<string, never>;
7
+ type Body = undefined;
8
+
9
+ class Query extends AutoEncoder {
10
+ @field({ decoder: StringDecoder, optional: true })
11
+ key?: string;
12
+ }
13
+
14
+ export class ResponseBody extends AutoEncoder {
15
+ @field({ decoder: StringDecoder })
16
+ status: 'ok' | 'error';
17
+ }
18
+
19
+ /**
20
+ * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
21
+ */
22
+
23
+ export class HealthEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
24
+ queryDecoder = Query;
25
+
26
+ protected doesMatch(request: Request): [true, Params] | [false] {
27
+ if (request.method !== 'GET') {
28
+ return [false];
29
+ }
30
+
31
+ if (!STAMHOOFD.HEALTH_ACCESS_KEY) {
32
+ return [false];
33
+ }
34
+
35
+ const params = Endpoint.parseParameters(request.url, '/health', {});
36
+
37
+ if (params) {
38
+ return [true, params as Params];
39
+ }
40
+ return [false];
41
+ }
42
+
43
+ async handle(request: DecodedRequest<Params, Query, Body>) {
44
+ if (!STAMHOOFD.HEALTH_ACCESS_KEY) {
45
+ throw new SimpleError({
46
+ code: 'unauthorized',
47
+ message: 'Unauthorized',
48
+ statusCode: 401,
49
+ });
50
+ }
51
+
52
+ if (STAMHOOFD.HEALTH_ACCESS_KEY && request.query.key !== STAMHOOFD.HEALTH_ACCESS_KEY) {
53
+ throw new SimpleError({
54
+ code: 'unauthorized',
55
+ message: 'Unauthorized',
56
+ statusCode: 401,
57
+ });
58
+ }
59
+
60
+ const health = ResponseBody.create({
61
+ status: 'ok',
62
+ });
63
+
64
+ if (CpuService.getAverage(60) > 80) {
65
+ health.status = 'error';
66
+ }
67
+
68
+ const response = new Response(
69
+ health,
70
+ );
71
+
72
+ if (health.status === 'error') {
73
+ response.status = 503;
74
+ }
75
+ return response;
76
+ }
77
+ }
@@ -0,0 +1,144 @@
1
+ import { Order, Webshop } from '@stamhoofd/models';
2
+ import { CheckoutMethodType, PaymentGeneral, PaymentMethod, PaymentMethodHelper, RecordCategory, RecordCheckboxAnswer, WebshopTakeoutMethod } from '@stamhoofd/structures';
3
+ import { Formatter } from '@stamhoofd/utility';
4
+
5
+ export function createOrderDataHTMLTable(order: Order, webshop: Webshop): string {
6
+ let str = `<table width="100%" cellspacing="0" cellpadding="0" class="email-data-table"><tbody>`;
7
+
8
+ const data = [
9
+ {
10
+ title: $t('4d496edf-0203-4df3-a6e9-3e58d226d6c5'),
11
+ value: '' + (order.number ?? '?'),
12
+ },
13
+ {
14
+ title: ((order) => {
15
+ if (order.data.checkoutMethod?.type === CheckoutMethodType.Takeout) {
16
+ return $t(`8113733b-00ea-42ae-8829-6056774a8be0`);
17
+ }
18
+
19
+ if (order.data.checkoutMethod?.type === CheckoutMethodType.OnSite) {
20
+ return $t(`7eec15d0-4d60-423f-b860-4f3824271578`);
21
+ }
22
+
23
+ return $t(`8a910c54-1b2d-4963-9128-2cab93b0151b`);
24
+ })(order),
25
+ value: ((order) => {
26
+ if (order.data.checkoutMethod?.type === CheckoutMethodType.Takeout) {
27
+ return order.data.checkoutMethod.name;
28
+ }
29
+
30
+ if (order.data.checkoutMethod?.type === CheckoutMethodType.OnSite) {
31
+ return order.data.checkoutMethod.name;
32
+ }
33
+
34
+ return order.data.address?.shortString() ?? '';
35
+ })(order),
36
+ },
37
+ ...(
38
+ (order.data.checkoutMethod?.type === CheckoutMethodType.Takeout || order.data.checkoutMethod?.type === CheckoutMethodType.OnSite) && ((order.data.checkoutMethod as any)?.address)
39
+ ? [
40
+ {
41
+ title: $t(`f7e792ed-2265-41e9-845f-e3ce0bc5da7c`),
42
+ value: ((order) => {
43
+ return (order.data.checkoutMethod as WebshopTakeoutMethod)?.address?.shortString() ?? '';
44
+ })(order),
45
+ },
46
+ ]
47
+ : []
48
+ ),
49
+ {
50
+ title: $t(`40aabd99-0331-4267-9b6a-a87c06b3f7fe`),
51
+ value: Formatter.capitalizeFirstLetter(order.data.timeSlot?.dateString() ?? ''),
52
+ },
53
+ {
54
+ title: $t(`7853cca1-c41a-4687-9502-190849405f76`),
55
+ value: order.data.timeSlot?.timeRangeString() ?? '',
56
+ },
57
+ {
58
+ title: $t(`17edcdd6-4fb2-4882-adec-d3a4f43a1926`),
59
+ value: order.data.customer.name,
60
+ },
61
+ ...(order.data.customer.phone
62
+ ? [
63
+ {
64
+ title: $t(`feea3664-9353-4bd4-b17d-aff005d3e265`),
65
+ value: order.data.customer.phone,
66
+ },
67
+ ]
68
+ : []),
69
+ ...order.data.fieldAnswers.filter(a => a.answer).map(a => ({
70
+ title: a.field.name,
71
+ value: a.answer,
72
+ })),
73
+ ...RecordCategory.sortAnswers(order.data.recordAnswers, webshop.meta.recordCategories).filter(a => !a.isEmpty || a instanceof RecordCheckboxAnswer).map(a => ({
74
+ title: a.settings.name.toString(),
75
+ value: a.stringValue,
76
+ })),
77
+ ...(
78
+ (order.data.paymentMethod !== PaymentMethod.Unknown)
79
+ ? [
80
+ {
81
+ title: $t(`07e7025c-0bfb-41be-87bc-1023d297a1a2`),
82
+ value: Formatter.capitalizeFirstLetter(PaymentMethodHelper.getName(order.data.paymentMethod)),
83
+ },
84
+ ]
85
+ : []
86
+ ),
87
+ ...order.data.priceBreakown.map((p) => {
88
+ return {
89
+ title: p.name,
90
+ value: Formatter.price(p.price),
91
+ };
92
+ }),
93
+ ];
94
+
95
+ for (const replacement of data) {
96
+ if (replacement.value.length === 0) {
97
+ continue;
98
+ }
99
+ str += `<tr><td><h4>${Formatter.escapeHtml(replacement.title)}</h4></td><td>${Formatter.escapeHtml(replacement.value)}</td></tr>`;
100
+ }
101
+
102
+ return str + '</tbody></table>';
103
+ }
104
+
105
+ export function createPaymentDataHTMLTable(payment: PaymentGeneral): string {
106
+ let str = `<table width="100%" cellspacing="0" cellpadding="0" class="email-data-table"><tbody>`;
107
+
108
+ const customer = payment.customer;
109
+
110
+ const replacements: { title: string; value: string }[] = [];
111
+
112
+ if (customer) {
113
+ replacements.push({
114
+ title: $t(`17edcdd6-4fb2-4882-adec-d3a4f43a1926`),
115
+ value: customer.dynamicName,
116
+ });
117
+
118
+ if (customer.phone) {
119
+ replacements.push({
120
+ title: $t(`feea3664-9353-4bd4-b17d-aff005d3e265`),
121
+ value: customer.phone,
122
+ });
123
+ }
124
+ }
125
+
126
+ replacements.push({
127
+ title: $t(`07e7025c-0bfb-41be-87bc-1023d297a1a2`),
128
+ value: Formatter.capitalizeFirstLetter(PaymentMethodHelper.getName(payment.method ?? PaymentMethod.Unknown)),
129
+ });
130
+
131
+ replacements.push({
132
+ title: $t(`Totaal`),
133
+ value: Formatter.price(payment.price),
134
+ });
135
+
136
+ for (const replacement of replacements) {
137
+ if (replacement.value.length === 0) {
138
+ continue;
139
+ }
140
+ str += `<tr><td><h4>${Formatter.escapeHtml(replacement.title)}</h4></td><td>${Formatter.escapeHtml(replacement.value)}</td></tr>`;
141
+ }
142
+
143
+ return str + '</tbody></table>';
144
+ }
@@ -53,4 +53,5 @@ INSERT INTO `email_templates` (`id`, `subject`, `groupId`, `webshopId`, `organiz
53
53
  INSERT INTO `email_templates` (`id`, `subject`, `groupId`, `webshopId`, `organizationId`, `type`, `text`, `html`, `json`, `updatedAt`, `createdAt`) VALUES
54
54
  ('f3d1c594-7510-4f12-9899-ef6e0d3a64c0', 'Kampmelding voor {{eventName}} ({{dateRange}}) werd goedgekeurd', NULL, NULL, NULL, 'EventNotificationAccepted', '{{greeting}}Goed nieuws! Jullie kampmelding voor {{eventName}} ({{dateRange}}) van {{organizationName}} werd goedgekeurd. Deze werd ingediend door {{submitterName}}.\n\n \n \n \n \n Kampmelding bekijken ({{reviewUrl}})\n \n \n \n \n\n', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title>Kampmelding voor {{eventName}} ({{dateRange}}) werd goedgekeurd</title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Goed nieuws! Jullie kampmelding voor <strong>{{eventName}}</strong> ({{dateRange}}) van <strong>{{organizationName}}</strong> werd goedgekeurd. Deze werd ingediend door {{submitterName}}.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><div data-type=\"smartButton\" data-id=\"reviewUrl\"><table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"margin: 5px 0;\">\n<tbody><tr>\n <td>\n <table cellspacing=\"0\" cellpadding=\"0\">\n <tbody><tr>\n <td style=\"border-radius: 7px;\" bgcolor=\"{{primaryColor}}\">\n <a class=\"button primary\" href=\"{{reviewUrl}}\" target=\"\" style=\"margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity;\">Kampmelding bekijken</a>\n </td>\n </tr>\n </tbody></table>\n </td>\n</tr>\n</tbody></table></div><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Goed nieuws! Jullie kampmelding voor \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"eventName\"}, \"marks\": [{\"type\": \"bold\"}]}, {\"text\": \" (\", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"dateRange\"}}, {\"text\": \") van \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}, \"marks\": [{\"type\": \"bold\"}]}, {\"text\": \" werd goedgekeurd. Deze werd ingediend door \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"submitterName\"}}, {\"text\": \".\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"smartButton\", \"attrs\": {\"id\": \"reviewUrl\"}, \"content\": [{\"text\": \"Kampmelding bekijken\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}]}, \"version\": 377}', '2025-07-15 09:35:18', '2025-07-15 09:35:18'),
55
55
  ('f5bf2c0a-0ac2-4d1d-a2d2-5c1c0139d399', 'Wachtwoord van {{organizationName}} vergeten', NULL, NULL, NULL, 'ForgotPasswordButNoAccount', '{{greeting}}Je gaf aan dat je jouw wachtwoord bent vergeten van {{organizationName}}, maar er bestaat geen account op het e-mailadres dat je hebt ingegeven ({{email}}). Niet zeker meer welk e-mailadres je kan gebruiken? Wij sturen altijd e-mails naar een e-mailadres waarmee je een account hebt. Lukt dat niet? Dan kan je via ons ledenportaal een nieuw account aanmaken met een e-mailadres naar keuze.{{organizationName}}', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title>Wachtwoord van {{organizationName}} vergeten</title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Je gaf aan dat je jouw wachtwoord bent vergeten van {{organizationName}}, maar er bestaat geen account op het e-mailadres dat je hebt ingegeven ({{email}}). Niet zeker meer welk e-mailadres je kan gebruiken? Wij sturen altijd e-mails naar een e-mailadres waarmee je een account hebt. Lukt dat niet? Dan kan je via ons ledenportaal een nieuw account aanmaken met een e-mailadres naar keuze.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{organizationName}}</p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Je gaf aan dat je jouw wachtwoord bent vergeten van \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}, {\"text\": \", maar er bestaat geen account op het e-mailadres dat je hebt ingegeven (\", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"email\"}}, {\"text\": \"). Niet zeker meer welk e-mailadres je kan gebruiken? Wij sturen altijd e-mails naar een e-mailadres waarmee je een account hebt. Lukt dat niet? Dan kan je via ons ledenportaal een nieuw account aanmaken met een e-mailadres naar keuze.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}]}]}, \"version\": 377}', '2025-07-15 09:18:37', '2025-07-15 09:18:37'),
56
- ('fdf5ff55-5dd3-45e5-82d3-b5b4f511582d', 'DNS-records van {{mailDomain}} zijn onstabiel', NULL, NULL, NULL, 'OrganizationUnstableDNS', '{{greeting}}Dit is een technische e-mail bestemd voor jullie webmaster.Stamhoofd controleert elke dag automatisch of jullie domeinnaam {{mailDomain}}, die gekoppeld is aan {{organizationName}} op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.Stamhoofd', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title>DNS-records van {{mailDomain}} zijn onstabiel</title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Dit is een technische e-mail bestemd voor jullie webmaster.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Stamhoofd controleert elke dag automatisch of jullie domeinnaam {{mailDomain}}, die gekoppeld is aan {{organizationName}} op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Stamhoofd</p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Dit is een technische e-mail bestemd voor jullie webmaster.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Stamhoofd controleert elke dag automatisch of jullie domeinnaam \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"mailDomain\"}}, {\"text\": \", die gekoppeld is aan \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}, {\"text\": \" op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Stamhoofd\", \"type\": \"text\"}]}]}, \"version\": 377}', '2025-07-15 09:24:17', '2025-07-15 09:24:17');
56
+ ('fdf5ff55-5dd3-45e5-82d3-b5b4f511582d', 'DNS-records van {{mailDomain}} zijn onstabiel', NULL, NULL, NULL, 'OrganizationUnstableDNS', '{{greeting}}Dit is een technische e-mail bestemd voor jullie webmaster.Stamhoofd controleert elke dag automatisch of jullie domeinnaam {{mailDomain}}, die gekoppeld is aan {{organizationName}} op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.Stamhoofd', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title>DNS-records van {{mailDomain}} zijn onstabiel</title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Dit is een technische e-mail bestemd voor jullie webmaster.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Stamhoofd controleert elke dag automatisch of jullie domeinnaam {{mailDomain}}, die gekoppeld is aan {{organizationName}} op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Stamhoofd</p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Dit is een technische e-mail bestemd voor jullie webmaster.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Stamhoofd controleert elke dag automatisch of jullie domeinnaam \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"mailDomain\"}}, {\"text\": \", die gekoppeld is aan \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}, {\"text\": \" op Stamhoofd nog correct is ingesteld. Dit is nodig voor het versturen van e-mails via jullie e-mailadressen, maar ook eventueel voor jullie ledenportaal als jullie de ledenadministratie gebruiken.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Nu blijkt al even dat we merken dat op het ene moment jullie domeinnaam correct is ingesteld, en de volgende dag niet meer, en zo gaat dat maar door. Om te verhinderen dat je elke keer een e-mail van ons krijgt daarover, hebben we jullie e-mailadres als onstabiel gemarkeerd.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Controleer of de nameservers van jullie domeinnaam wel correct zijn ingesteld en allemaal dezelfde DNS-records teruggeven.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Je ontvangt terug een e-mail van ons als we merken dat het probleem werd verholpen. Contacteer ons gerust als jullie hierover vragen hebben.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Stamhoofd\", \"type\": \"text\"}]}]}, \"version\": 377}', '2025-07-15 09:24:17', '2025-07-15 09:24:17'),
57
+ ('61199699-492f-4fd3-a966-ca75179c3e71', '', NULL, NULL, NULL, 'DefaultPaymentsEmail', '{{greeting}}\n\nBetaalinstructies\n{{paymentTable}}\n\n{{overviewContext}}\n\n{{balanceItemPaymentsTable}}\nTotaal: {{paymentPrice}}\n\nKomt deze e-mail bij jou terecht, maar weet je niet waarover dit gaat en denk je dat dit aan een andere persoon is gericht (bv. een typefout)? Dan schrijf je best uit voor onze e-mails via deze knop ({{unsubscribeUrl}}) — hierna zal je nooit meer een e-mail van ons ontvangen.\nUitschrijven ({{unsubscribeUrl}})', '<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n<title></title>\n<style type=\"text/css\">body {\n color: #000716;\n color: var(--color-dark, #000716);\n font-family: -apple-system-body, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 12pt;\n line-height: 1.4;\n}\n\np {\n margin: 0;\n padding: 0;\n line-height: 1.4;\n}\n\np.description {\n color: var(--color-gray-4, #5e5e5e);\n}\np.description a, p.description a:link, p.description a:visited, p.description a:active, p.description a:hover {\n text-decoration: underline;\n color: var(--color-gray-4, #5e5e5e);\n}\n\nstrong {\n font-weight: bold;\n}\n\nem {\n font-style: italic;\n}\n\nh1 {\n font-size: 30px;\n font-weight: bold;\n line-height: 1.2;\n margin: 0;\n padding: 0;\n}\n@media (max-width: 350px) {\n h1 {\n font-size: 24px;\n }\n}\n\nh2 {\n font-size: 20px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh3 {\n font-size: 16px;\n line-height: 1.2;\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\nh4 {\n line-height: 1.2;\n font-weight: 500;\n margin: 0;\n padding: 0;\n}\n\nol, ul {\n list-style-position: outside;\n padding-left: 30px;\n}\n\nhr {\n height: 1px;\n background: var(--color-border, var(--color-gray-2, #dcdcdc));\n border-radius: 1px;\n padding: 0;\n margin: 20px 0;\n outline: none;\n border: 0;\n}\n\n.button {\n touch-action: inherit;\n user-select: auto;\n cursor: pointer;\n display: inline-block !important;\n line-height: 42px;\n font-size: 16px;\n font-weight: bold;\n}\n.button:active {\n transform: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\na, a:link, a:visited, a:active, a:hover {\n text-decoration: underline;\n color: blue;\n}\n\n.email-data-table {\n width: 100%;\n border-collapse: collapse;\n}\n.email-data-table th, .email-data-table td {\n text-align: left;\n padding: 10px 10px 10px 0;\n border-bottom: 1px solid var(--color-border, var(--color-gray-2, #dcdcdc));\n vertical-align: middle;\n}\n.email-data-table th:last-child, .email-data-table td:last-child {\n text-align: right;\n padding-right: 0;\n}\n.email-data-table td.price {\n white-space: nowrap;\n}\n.email-data-table thead {\n font-weight: bold;\n}\n.email-data-table thead th {\n font-size: 10pt;\n}\n.email-data-table h4 ~ p {\n padding-top: 3px;\n opacity: 0.8;\n font-size: 11pt;\n}\n\n.email-style-inline-code {\n font-family: monospace;\n white-space: pre-wrap;\n display: inline-block;\n}\n\n.email-style-description-small {\n font-size: 14px;\n line-height: 1.5;\n font-weight: normal;\n color: var(--color-gray-4, #5e5e5e);\n font-variation-settings: \"opsz\" 19;\n}\n\n.email-style-title-list {\n font-size: 16px;\n line-height: 1.3;\n font-weight: 500;\n}\n.email-style-title-list + p {\n padding-top: 3px;\n}\n\n.email-style-title-prefix-list {\n font-size: 11px;\n line-height: 1.5;\n font-weight: bold;\n color: {{primaryColor}};\n text-transform: uppercase;\n margin-bottom: 3px;\n}\n.email-style-title-prefix-list.error {\n color: #f0153d;\n}\n\n.email-style-price-base, .email-style-discount-price, .email-style-discount-old-price, .email-style-price {\n font-size: 15px;\n line-height: 1.4;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n.email-style-price-base.disabled, .disabled.email-style-discount-price, .disabled.email-style-discount-old-price, .disabled.email-style-price {\n opacity: 0.6;\n}\n.email-style-price-base.negative, .negative.email-style-discount-price, .negative.email-style-discount-old-price, .negative.email-style-price {\n color: #f0153d;\n}\n\n.email-style-price {\n font-weight: bold;\n color: {{primaryColor}};\n}\n\n.email-style-discount-old-price {\n text-decoration: line-through;\n color: var(--color-gray-4, #5e5e5e);\n}\n\n.email-style-discount-price {\n font-weight: bold;\n color: #ff4747;\n margin-left: 5px;\n}\n\n.pre-wrap {\n white-space: pre-wrap;\n} hr {height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;} .button.primary { margin: 0; text-decoration: none; font-size: 16px; font-weight: bold; color: {{primaryColorContrast}}; padding: 0 27px; line-height: 42px; background: {{primaryColor}}; text-align: center; border-radius: 7px; touch-action: manipulation; display: inline-block; transition: 0.2s transform, 0.2s opacity; } .button.primary:active { transform: scale(0.95, 0.95); } .inline-link, .inline-link:link, .inline-link:visited, .inline-link:active, .inline-link:hover { margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation; } .inline-link:active { opacity: 0.5; } .description { color: #5e5e5e; } </style>\n</head>\n\n<body>\n<p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{greeting}}</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><hr style=\"height: 2px;background: #e7e7e7; border-radius: 1px; padding: 0; margin: 20px 0; outline: none; border: 0;\"><h2 style=\"margin: 0; padding: 0;\">Betaalinstructies</h2><div data-type=\"smartVariableBlock\" data-id=\"paymentTable\">{{paymentTable}}</div><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><h2 style=\"margin: 0; padding: 0;\">{{overviewContext}}</h2><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><div data-type=\"smartVariableBlock\" data-id=\"balanceItemPaymentsTable\">{{balanceItemPaymentsTable}}</div><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><h3 style=\"margin: 0; padding: 0;\">Totaal: {{paymentPrice}}</h3><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p class=\"description\" style=\"color: #5e5e5e;\"><em>Komt deze e-mail bij jou terecht, maar weet je niet waarover dit gaat en denk je dat dit aan een andere persoon is gericht (bv. een typefout)? Dan schrijf je best uit voor onze e-mails via </em><a class=\"inline-link\" href=\"{{unsubscribeUrl}}\" target=\"\" style=\"margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation;\"><em>deze knop</em></a><em> — hierna zal je nooit meer een e-mail van ons ontvangen.</em></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><a class=\"inline-link\" href=\"{{unsubscribeUrl}}\" target=\"\" style=\"margin: 0; text-decoration: underline; font-size: inherit; font-weight: inherit; color: inherit; touch-action: manipulation;\">Uitschrijven</a></p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"horizontalRule\"}, {\"type\": \"heading\", \"attrs\": {\"level\": 2}, \"content\": [{\"text\": \"Betaalinstructies\", \"type\": \"text\"}]}, {\"type\": \"smartVariableBlock\", \"attrs\": {\"id\": \"paymentTable\"}}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\"}, {\"type\": \"heading\", \"attrs\": {\"level\": 2}, \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"overviewContext\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"smartVariableBlock\", \"attrs\": {\"id\": \"balanceItemPaymentsTable\"}}, {\"type\": \"paragraph\"}, {\"type\": \"heading\", \"attrs\": {\"level\": 3}, \"content\": [{\"text\": \"Totaal: \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"paymentPrice\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\"}, {\"type\": \"descriptiveText\", \"content\": [{\"text\": \"Komt deze e-mail bij jou terecht, maar weet je niet waarover dit gaat en denk je dat dit aan een andere persoon is gericht (bv. een typefout)? Dan schrijf je best uit voor onze e-mails via \", \"type\": \"text\", \"marks\": [{\"type\": \"italic\"}]}, {\"type\": \"smartButtonInline\", \"attrs\": {\"id\": \"unsubscribeUrl\"}, \"content\": [{\"text\": \"deze knop\", \"type\": \"text\", \"marks\": [{\"type\": \"italic\"}]}]}, {\"text\": \" — hierna zal je nooit meer een e-mail van ons ontvangen.\", \"type\": \"text\", \"marks\": [{\"type\": \"italic\"}]}]}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"smartButtonInline\", \"attrs\": {\"id\": \"unsubscribeUrl\"}, \"content\": [{\"text\": \"Uitschrijven\", \"type\": \"text\"}]}]}]}, \"version\": 394}', '2026-02-10 17:44:20', '2026-02-10 17:44:20');
@@ -0,0 +1,123 @@
1
+ import { SQLLogger } from '@stamhoofd/sql';
2
+
3
+ class StaticCpuService {
4
+ samples: number[];
5
+ private maxSamples: number;
6
+ private interval?: NodeJS.Timeout;
7
+ // Current index = the last saved
8
+ private currentIndex = 0;
9
+
10
+ constructor() {
11
+ this.maxSamples = 5 * 60; // 5 minutes of data
12
+ this.samples = new Array(this.maxSamples).fill(0);
13
+ }
14
+
15
+ /**
16
+ * Get a live sample of CPU usage
17
+ * @param samplingInterval
18
+ * @returns
19
+ */
20
+ async takeSample(samplingInterval = 500): Promise<number> {
21
+ const startUsage = process.cpuUsage();
22
+ const startTime = process.hrtime.bigint();
23
+
24
+ return new Promise((resolve) => {
25
+ setTimeout(() => {
26
+ const elapsedUsage = process.cpuUsage(startUsage);
27
+ const elapsedTime = process.hrtime.bigint() - startTime;
28
+
29
+ // Convert to microseconds
30
+ const elapsedTimeMs = Number(elapsedTime) / 1000;
31
+
32
+ // CPU time in microseconds
33
+ const totalCPUTime = elapsedUsage.user + elapsedUsage.system;
34
+
35
+ // Calculate percentage
36
+ const cpuPercent = (totalCPUTime / elapsedTimeMs) * 100;
37
+
38
+ resolve(cpuPercent);
39
+ }, samplingInterval);
40
+ });
41
+ }
42
+
43
+ getCpuUsage(): number | undefined {
44
+ if (this.currentIndex === 0) {
45
+ return this.samples[this.samples.length - 1];
46
+ }
47
+ return this.samples[this.currentIndex - 1];
48
+ }
49
+
50
+ getAverage(size = this.maxSamples): number {
51
+ if (size > this.maxSamples) {
52
+ size = this.maxSamples;
53
+ }
54
+ if (size <= 0) {
55
+ return NaN;
56
+ }
57
+ if (size === this.maxSamples) {
58
+ return this.samples.reduce((a, b) => a + b, 0) / size;
59
+ }
60
+ else {
61
+ // To read before current index
62
+ const toReadBeforeIndex = Math.min(size, this.currentIndex);
63
+ const toReadFromEnd = size - toReadBeforeIndex;
64
+
65
+ // Sum performantly
66
+ let sum = 0;
67
+ for (let i = 0; i < toReadBeforeIndex; i++) {
68
+ sum += this.samples[this.currentIndex - 1 - i];
69
+ }
70
+ for (let i = 0; i < toReadFromEnd; i++) {
71
+ sum += this.samples[this.samples.length - 1 - i];
72
+ }
73
+
74
+ return sum / size;
75
+ }
76
+ }
77
+
78
+ private async saveSample() {
79
+ const sample = await this.takeSample(1000);
80
+ this.samples[this.currentIndex] = sample;
81
+ this.currentIndex = (this.currentIndex + 1) % this.maxSamples;
82
+ const five = this.getAverage(5);
83
+
84
+ if (this.currentIndex % 5 === 0 || five > 80) {
85
+ const min = this.getAverage(60);
86
+ console.log(`[CPU] 5s: ${five.toFixed(2)}%\n[CPU] 1 min: ${min.toFixed(2)}%\n[CPU] 5 min: ${this.getAverage(60 * 5).toFixed(2)}%`);
87
+ }
88
+
89
+ if (five > 80) {
90
+ // Danger zone, in this case we don't want to log all slow queries any longer because the information won't be trustworthy.
91
+ SQLLogger.slowQueryThresholdMs = null;
92
+ }
93
+ else {
94
+ if (five < 20) {
95
+ // No load, safe to log all slow queries
96
+ SQLLogger.slowQueryThresholdMs = 300;
97
+ }
98
+ else {
99
+ SQLLogger.slowQueryThresholdMs = 500;
100
+ }
101
+ }
102
+ }
103
+
104
+ startMonitoring() {
105
+ if (this.interval) {
106
+ return;
107
+ }
108
+ this.interval = setInterval(() => {
109
+ this.saveSample().catch((error) => {
110
+ console.error('Failed to take CPU sample:', error);
111
+ });
112
+ }, 1000); // Sample every second
113
+ }
114
+
115
+ stopMonitoring() {
116
+ if (this.interval) {
117
+ clearInterval(this.interval);
118
+ this.interval = undefined;
119
+ }
120
+ }
121
+ }
122
+
123
+ export const CpuService = new StaticCpuService();
@@ -24,5 +24,10 @@ export const balanceItemPaymentsCompilers: SQLFilterDefinitions = {
24
24
  type: SQLValueType.String,
25
25
  nullable: false,
26
26
  }),
27
+ payingOrganizationId: createColumnFilter({
28
+ expression: SQL.column('balance_items', 'payingOrganizationId'),
29
+ type: SQLValueType.String,
30
+ nullable: true,
31
+ }),
27
32
  },
28
33
  };
@@ -0,0 +1,16 @@
1
+ import { MemberResponsibilityRecord } from '@stamhoofd/models';
2
+ import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
3
+
4
+ const baseTable = SQL.table(MemberResponsibilityRecord.table);
5
+
6
+ /**
7
+ * Defines how to filter member responsibility records in the database from StamhoofdFilter objects
8
+ */
9
+ export const memberResponsibilityRecordFilterCompilers: SQLFilterDefinitions = {
10
+ ...baseSQLFilterCompilers,
11
+ responsibilityId: createColumnFilter({
12
+ expression: SQL.column(baseTable, 'responsibilityId'),
13
+ type: SQLValueType.String,
14
+ nullable: false,
15
+ }),
16
+ };
@@ -31,6 +31,11 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
31
31
  type: SQLValueType.String,
32
32
  nullable: true,
33
33
  }),
34
+ payingOrganizationId: createColumnFilter({
35
+ expression: SQL.column('payingOrganizationId'),
36
+ type: SQLValueType.String,
37
+ nullable: true,
38
+ }),
34
39
  createdAt: createColumnFilter({
35
40
  expression: SQL.column('createdAt'),
36
41
  type: SQLValueType.Datetime,
@@ -27,7 +27,7 @@ export const documentTemplateSorters: SQLSortDefinitions<DocumentTemplate> = {
27
27
  },
28
28
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
29
29
  return new SQLOrderBy({
30
- column: SQL.jsonExtract(SQL.column('settings'), '$.value.name'),
30
+ column: SQL.jsonValue(SQL.column('settings'), '$.value.name', 'CHAR'),
31
31
  direction,
32
32
  });
33
33
  },
@@ -27,7 +27,7 @@ export const documentSorters: SQLSortDefinitions<Document> = {
27
27
  },
28
28
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
29
29
  return new SQLOrderBy({
30
- column: SQL.jsonExtract(SQL.column('data'), '$.value.description'),
30
+ column: SQL.jsonValue(SQL.column('data'), '$.value.description', 'CHAR'),
31
31
  direction,
32
32
  });
33
33
  },