@stamhoofd/backend 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.template.json +63 -0
  2. package/.eslintrc.js +61 -0
  3. package/README.md +40 -0
  4. package/index.ts +172 -0
  5. package/jest.config.js +11 -0
  6. package/migrations.ts +33 -0
  7. package/package.json +48 -0
  8. package/src/crons.ts +845 -0
  9. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +42 -0
  10. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +320 -0
  11. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +171 -0
  12. package/src/endpoints/auth/CreateAdminEndpoint.ts +137 -0
  13. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +68 -0
  14. package/src/endpoints/auth/CreateTokenEndpoint.ts +200 -0
  15. package/src/endpoints/auth/DeleteTokenEndpoint.ts +31 -0
  16. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +70 -0
  17. package/src/endpoints/auth/GetUserEndpoint.test.ts +64 -0
  18. package/src/endpoints/auth/GetUserEndpoint.ts +57 -0
  19. package/src/endpoints/auth/PatchApiUserEndpoint.ts +90 -0
  20. package/src/endpoints/auth/PatchUserEndpoint.ts +122 -0
  21. package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +37 -0
  22. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +41 -0
  23. package/src/endpoints/auth/SignupEndpoint.ts +107 -0
  24. package/src/endpoints/auth/VerifyEmailEndpoint.ts +89 -0
  25. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +95 -0
  26. package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +31 -0
  27. package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +101 -0
  28. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +53 -0
  29. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +57 -0
  30. package/src/endpoints/global/files/UploadFile.ts +147 -0
  31. package/src/endpoints/global/files/UploadImage.ts +119 -0
  32. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +76 -0
  33. package/src/endpoints/global/members/GetMembersCountEndpoint.ts +43 -0
  34. package/src/endpoints/global/members/GetMembersEndpoint.ts +429 -0
  35. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +734 -0
  36. package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +45 -0
  37. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +105 -0
  38. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +146 -0
  39. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +52 -0
  40. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +80 -0
  41. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +49 -0
  42. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +58 -0
  43. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +62 -0
  44. package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +153 -0
  45. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +134 -0
  46. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +44 -0
  47. package/src/endpoints/global/platform/GetPlatformEnpoint.ts +39 -0
  48. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +63 -0
  49. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +68 -0
  50. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +39 -0
  51. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +80 -0
  52. package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +41 -0
  53. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +134 -0
  54. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +521 -0
  55. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +37 -0
  56. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +115 -0
  57. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +187 -0
  58. package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +424 -0
  59. package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +67 -0
  60. package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +39 -0
  61. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +57 -0
  62. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +50 -0
  63. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +50 -0
  64. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +129 -0
  65. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +114 -0
  66. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +50 -0
  67. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +234 -0
  68. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +62 -0
  69. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +85 -0
  70. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +80 -0
  71. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +54 -0
  72. package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +49 -0
  73. package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +63 -0
  74. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +61 -0
  75. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +64 -0
  76. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +84 -0
  77. package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +43 -0
  78. package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +42 -0
  79. package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +43 -0
  80. package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +65 -0
  81. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +281 -0
  82. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +338 -0
  83. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +196 -0
  84. package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +50 -0
  85. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +48 -0
  86. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +207 -0
  87. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +202 -0
  88. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +233 -0
  89. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +66 -0
  90. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -0
  91. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +93 -0
  92. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +59 -0
  93. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +78 -0
  94. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +40 -0
  95. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +69 -0
  96. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +52 -0
  97. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +73 -0
  98. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +60 -0
  99. package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +47 -0
  100. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +41 -0
  101. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +217 -0
  102. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +51 -0
  103. package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +47 -0
  104. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +83 -0
  105. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +68 -0
  106. package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +69 -0
  107. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +125 -0
  108. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +204 -0
  109. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +278 -0
  110. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +80 -0
  111. package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +60 -0
  112. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +379 -0
  113. package/src/endpoints/organization/shared/GetDocumentHtml.ts +54 -0
  114. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +45 -0
  115. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +78 -0
  116. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +34 -0
  117. package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +44 -0
  118. package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +82 -0
  119. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +59 -0
  120. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +51 -0
  121. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +40 -0
  122. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +124 -0
  123. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +130 -0
  124. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +50 -0
  125. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +450 -0
  126. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +335 -0
  127. package/src/helpers/AddressValidator.test.ts +40 -0
  128. package/src/helpers/AddressValidator.ts +256 -0
  129. package/src/helpers/AdminPermissionChecker.ts +1031 -0
  130. package/src/helpers/AuthenticatedStructures.ts +158 -0
  131. package/src/helpers/BuckarooHelper.ts +279 -0
  132. package/src/helpers/CheckSettlements.ts +215 -0
  133. package/src/helpers/Context.ts +202 -0
  134. package/src/helpers/CookieHelper.ts +45 -0
  135. package/src/helpers/ForwardHandler.test.ts +216 -0
  136. package/src/helpers/ForwardHandler.ts +140 -0
  137. package/src/helpers/OpenIDConnectHelper.ts +284 -0
  138. package/src/helpers/StripeHelper.ts +293 -0
  139. package/src/helpers/StripePayoutChecker.ts +188 -0
  140. package/src/middleware/ContextMiddleware.ts +16 -0
  141. package/src/migrations/1646578856-validate-addresses.ts +60 -0
  142. package/src/seeds/0000000000-example.ts +13 -0
  143. package/src/seeds/1715028563-user-permissions.ts +52 -0
  144. package/tests/e2e/stock.test.ts +2120 -0
  145. package/tests/e2e/tickets.test.ts +926 -0
  146. package/tests/helpers/StripeMocker.ts +362 -0
  147. package/tests/helpers/TestServer.ts +21 -0
  148. package/tests/jest.global.setup.ts +29 -0
  149. package/tests/jest.setup.ts +59 -0
  150. package/tsconfig.json +42 -0
package/src/crons.ts ADDED
@@ -0,0 +1,845 @@
1
+ import { Database } from '@simonbackx/simple-database';
2
+ import { logger, StyledText } from "@simonbackx/simple-logging";
3
+ import { I18n } from '@stamhoofd/backend-i18n';
4
+ import { Email } from '@stamhoofd/email';
5
+ import { EmailAddress } from '@stamhoofd/email';
6
+ import { Group, STPackage, Webshop } from '@stamhoofd/models';
7
+ import { Organization } from '@stamhoofd/models';
8
+ import { Payment } from '@stamhoofd/models';
9
+ import { Registration } from '@stamhoofd/models';
10
+ import { STInvoice } from '@stamhoofd/models';
11
+ import { STPendingInvoice } from '@stamhoofd/models';
12
+ import { QueueHandler } from '@stamhoofd/queues';
13
+ import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
14
+ import { Formatter, sleep } from '@stamhoofd/utility';
15
+ import AWS from 'aws-sdk';
16
+ import { DateTime } from 'luxon';
17
+
18
+ import { ExchangeSTPaymentEndpoint } from './endpoints/global/payments/ExchangeSTPaymentEndpoint';
19
+ import { ExchangePaymentEndpoint } from './endpoints/organization/shared/ExchangePaymentEndpoint';
20
+ import { checkSettlements } from './helpers/CheckSettlements';
21
+ import { ForwardHandler } from './helpers/ForwardHandler';
22
+
23
+ // Importing postmark returns undefined (this is a bug, so we need to use require)
24
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
25
+ const postmark = require("postmark")
26
+
27
+ let lastDNSCheck: Date | null = null
28
+ let lastDNSId = ""
29
+ async function checkDNS() {
30
+ if (STAMHOOFD.environment === "development") {
31
+ return;
32
+ }
33
+
34
+ // Wait 6 hours between every complete check
35
+ if (lastDNSCheck && lastDNSCheck > new Date(new Date().getTime() - 6 * 60 * 60 * 1000)) {
36
+ console.log("[DNS] Skip DNS check")
37
+ return
38
+ }
39
+
40
+ const organizations = await Organization.where({ id: { sign: '>', value: lastDNSId } }, {
41
+ limit: 50,
42
+ sort: ["id"]
43
+ })
44
+
45
+ if (organizations.length == 0) {
46
+ // Wait an half hour before starting again
47
+ lastDNSId = ""
48
+ lastDNSCheck = new Date()
49
+ return
50
+ }
51
+
52
+ console.log("[DNS] Checking DNS...")
53
+
54
+ for (const organization of organizations) {
55
+ if (STAMHOOFD.environment === "production") {
56
+ console.log("[DNS] "+organization.name)
57
+ }
58
+ try {
59
+ await organization.updateDNSRecords()
60
+ } catch (e) {
61
+ console.error(e);
62
+ }
63
+ }
64
+
65
+ lastDNSId = organizations[organizations.length - 1].id
66
+
67
+ }
68
+
69
+ let lastExpirationCheck: Date | null = null
70
+ async function checkExpirationEmails() {
71
+ if (STAMHOOFD.environment === "development") {
72
+ return;
73
+ }
74
+
75
+ // Wait 1 hour between every complete check
76
+ if (lastExpirationCheck && lastExpirationCheck > new Date(new Date().getTime() - 1 * 60 * 60 * 1000)) {
77
+ console.log("[EXPIRATION EMAILS] Skip checkExpirationEmails")
78
+ return
79
+ }
80
+
81
+ // Get all packages that expire between now and 31 days
82
+ const packages = await STPackage.where({
83
+ validUntil: [
84
+ { sign: '!=', value: null },
85
+ { sign: '>', value: new Date() },
86
+ { sign: '<', value: new Date(Date.now() + 1000 * 60 * 60 * 24 * 31) }
87
+ ],
88
+ validAt: [
89
+ { sign: '!=', value: null },
90
+ ],
91
+ emailCount: 0
92
+ })
93
+
94
+ console.log("[EXPIRATION EMAILS] Sending expiration emails...")
95
+
96
+ for (const pack of packages) {
97
+ await pack.sendExpiryEmail()
98
+ }
99
+ lastExpirationCheck = new Date()
100
+ }
101
+
102
+ let lastWebshopDNSCheck: Date | null = null
103
+ let lastWebshopDNSId = ""
104
+ async function checkWebshopDNS() {
105
+ if (STAMHOOFD.environment === "development") {
106
+ return;
107
+ }
108
+
109
+ // Wait 6 hours between every complete check
110
+ if (lastWebshopDNSCheck && lastWebshopDNSCheck > new Date(new Date().getTime() - 6 * 60 * 60 * 1000)) {
111
+ console.log("[DNS] Skip webshop DNS check")
112
+ return
113
+ }
114
+
115
+ const webshops = await Webshop.where({
116
+ id: { sign: '>', value: lastWebshopDNSId },
117
+ domain: { sign: '!=', value: null }
118
+ }, {
119
+ limit: 10,
120
+ sort: ["id"]
121
+ })
122
+
123
+ if (webshops.length == 0) {
124
+ // Wait an half hour before starting again
125
+ lastWebshopDNSId = ""
126
+ lastWebshopDNSCheck = new Date()
127
+ return
128
+ }
129
+
130
+ console.log("[DNS] Checking webshop DNS...")
131
+
132
+ for (const webshop of webshops) {
133
+ if (STAMHOOFD.environment === "production" || true) {
134
+ console.log("[DNS] Webshop "+webshop.meta.name+" ("+webshop.id+")"+" ("+webshop.domain+")")
135
+ }
136
+ await webshop.updateDNSRecords()
137
+ }
138
+
139
+ lastWebshopDNSId = webshops[webshops.length - 1].id
140
+ }
141
+
142
+ async function checkReplies() {
143
+ if (STAMHOOFD.environment === "development") {
144
+ return;
145
+ }
146
+
147
+ console.log("Checking replies from AWS SQS")
148
+ const sqs = new AWS.SQS();
149
+ const messages = await sqs.receiveMessage({ QueueUrl: "https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding", MaxNumberOfMessages: 10 }).promise()
150
+ if (messages.Messages) {
151
+ for (const message of messages.Messages) {
152
+ console.log("Received message from forwarding queue");
153
+
154
+ if (message.ReceiptHandle) {
155
+ if (STAMHOOFD.environment === "production") {
156
+ await sqs.deleteMessage({
157
+ QueueUrl: "https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding",
158
+ ReceiptHandle: message.ReceiptHandle
159
+ }).promise()
160
+ console.log("Deleted from queue");
161
+ }
162
+ }
163
+
164
+ try {
165
+ if (message.Body) {
166
+ // decode the JSON value
167
+ const bounce = JSON.parse(message.Body)
168
+
169
+ if (bounce.Message) {
170
+ const message = JSON.parse(bounce.Message)
171
+
172
+ // Read message content
173
+ if (message.mail && message.content && message.receipt) {
174
+ const content = message.content;
175
+ const receipt = message.receipt as {
176
+ recipients: string[];
177
+ spamVerdict: { status: 'PASS' | string };
178
+ virusVerdict: { status: 'PASS' | string };
179
+ spfVerdict: { status: 'PASS' | string };
180
+ dkimVerdict: { status: 'PASS' | string };
181
+ dmarcVerdict: { status: 'PASS' | string };
182
+ }
183
+
184
+ const options = await ForwardHandler.handle(content, receipt)
185
+ if (options) {
186
+ if (STAMHOOFD.environment === "production") {
187
+ Email.send(options)
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+ } catch (e) {
194
+ console.error(e)
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ let lastPostmarkCheck: Date | null = null
201
+ let lastPostmarkId: string | null = null
202
+ async function checkPostmarkBounces() {
203
+ if (STAMHOOFD.environment === "development") {
204
+ return;
205
+ }
206
+
207
+ const token = STAMHOOFD.POSTMARK_SERVER_TOKEN
208
+ if (!token) {
209
+ console.log("[POSTMARK BOUNCES] No postmark token, skipping postmark bounces")
210
+ return
211
+ }
212
+ const fromDate = (lastPostmarkCheck ?? new Date(new Date().getTime() - 24 * 60 * 60 * 1000 * 2))
213
+ const ET = DateTime.fromJSDate(fromDate).setZone('EST').toISO({ includeOffset: false})
214
+ console.log("[POSTMARK BOUNCES] Checking bounces from Postmark since", fromDate, ET)
215
+ const client = new postmark.ServerClient(token);
216
+
217
+ const bounces = await client.getBounces({
218
+ fromdate: ET,
219
+ todate: DateTime.now().setZone('EST').toISO({ includeOffset: false}),
220
+ count: 500,
221
+ offset: 0
222
+ })
223
+
224
+ if (bounces.TotalCount == 0) {
225
+ console.log("[POSTMARK BOUNCES] No Postmark bounces at this time")
226
+ return
227
+ }
228
+
229
+ let lastId: string | null = null
230
+
231
+ for (const bounce of bounces.Bounces) {
232
+ // Try to get the organization, if possible, else default to global blocking: "null", which is not visible for an organization, but it is applied
233
+ const source = bounce.From
234
+ const organization = source ? await Organization.getByEmail(source) : undefined
235
+
236
+ if (bounce.Type === "HardBounce" || bounce.Type === "BadEmailAddress" || bounce.Type === "Blocked") {
237
+ // Block for everyone, but not visible
238
+ console.log("[POSTMARK BOUNCES] Postmark "+bounce.Type+" for: ", bounce.Email, "from", source, "organization", organization?.name)
239
+ const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null)
240
+ emailAddress.hardBounce = true
241
+ await emailAddress.save()
242
+ } else if (bounce.Type === "SpamComplaint" || bounce.Type === "SpamNotification" || bounce.Type === "VirusNotification") {
243
+ console.log("[POSTMARK BOUNCES] Postmark "+bounce.Type+" for: ", bounce.Email, "from", source, "organization", organization?.name)
244
+ const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null)
245
+ emailAddress.markedAsSpam = true
246
+ await emailAddress.save()
247
+ } else {
248
+ console.log("[POSTMARK BOUNCES] Unhandled Postmark "+bounce.Type+": ", bounce.Email, "from", source, "organization", organization?.name)
249
+ console.error("[POSTMARK BOUNCES] Unhandled Postmark "+bounce.Type+": ", bounce.Email, "from", source, "organization", organization?.name)
250
+ }
251
+
252
+ const bouncedAt = new Date(bounce.BouncedAt)
253
+ lastPostmarkCheck = lastPostmarkCheck ? new Date(Math.max(bouncedAt.getTime(), lastPostmarkCheck.getTime())) : bouncedAt
254
+
255
+ lastId = bounce.ID
256
+ }
257
+
258
+ if (lastId && lastPostmarkId) {
259
+ if (lastId === lastPostmarkId) {
260
+ console.log("[POSTMARK BOUNCES] Postmark has no new bounces")
261
+ // Increase timestamp by one second to avoid refetching it every time
262
+ if (lastPostmarkCheck) {
263
+ lastPostmarkCheck = new Date(lastPostmarkCheck.getTime() + 1000)
264
+ }
265
+ }
266
+ }
267
+ lastPostmarkId = lastId
268
+ }
269
+
270
+ async function checkBounces() {
271
+ if (STAMHOOFD.environment !== "production") {
272
+ return
273
+ }
274
+
275
+ console.log("[AWS BOUNCES] Checking bounces from AWS SQS")
276
+ const sqs = new AWS.SQS();
277
+ const messages = await sqs.receiveMessage({ QueueUrl: "https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-bounces-queue", MaxNumberOfMessages: 10 }).promise()
278
+ if (messages.Messages) {
279
+ for (const message of messages.Messages) {
280
+ console.log("[AWS BOUNCES] Received bounce message");
281
+ console.log("[AWS BOUNCES]", message);
282
+
283
+ if (message.ReceiptHandle) {
284
+ if (STAMHOOFD.environment === "production") {
285
+ await sqs.deleteMessage({
286
+ QueueUrl: "https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-bounces-queue",
287
+ ReceiptHandle: message.ReceiptHandle
288
+ }).promise()
289
+ console.log("[AWS BOUNCES] Deleted from queue");
290
+ }
291
+ }
292
+
293
+ try {
294
+ if (message.Body) {
295
+ // decode the JSON value
296
+ const bounce = JSON.parse(message.Body)
297
+
298
+ if (bounce.Message) {
299
+ const message = JSON.parse(bounce.Message)
300
+
301
+ if (message.bounce) {
302
+ const b = message.bounce
303
+ // Block all receivers that generate a permanent bounce
304
+ const type = b.bounceType
305
+
306
+ const source = message.mail.source
307
+
308
+ // try to find organization that is responsible for this e-mail address
309
+
310
+ for (const recipient of b.bouncedRecipients) {
311
+ const email = recipient.emailAddress
312
+
313
+ if (
314
+ type === "Permanent"
315
+ || (
316
+ recipient.diagnosticCode && (
317
+ (recipient.diagnosticCode as string).toLowerCase().includes("invalid domain")
318
+ || (recipient.diagnosticCode as string).toLowerCase().includes('unable to lookup dns')
319
+ )
320
+ )
321
+ ) {
322
+ const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined
323
+ if (organization) {
324
+ const emailAddress = await EmailAddress.getOrCreate(email, organization.id)
325
+ emailAddress.hardBounce = true
326
+ await emailAddress.save()
327
+ } else {
328
+ console.error("[AWS BOUNCES] Unknown organization for email address "+source)
329
+ }
330
+ }
331
+
332
+ }
333
+ console.log("[AWS BOUNCES] For domain "+source)
334
+ } else {
335
+ console.log("[AWS BOUNCES] 'bounce' field missing in bounce message")
336
+ }
337
+ } else {
338
+ console.log("[AWS BOUNCES] 'Message' field missing in bounce message")
339
+ }
340
+ } else {
341
+ console.log("[AWS BOUNCES] Message Body missing in bounce")
342
+ }
343
+ } catch (e) {
344
+ console.log("[AWS BOUNCES] Bounce message processing failed:")
345
+ console.error("[AWS BOUNCES] Bounce message processing failed:")
346
+ console.error("[AWS BOUNCES]", e)
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ async function checkComplaints() {
353
+ if (STAMHOOFD.environment !== "production") {
354
+ return
355
+ }
356
+
357
+ console.log("[AWS COMPLAINTS] Checking complaints from AWS SQS")
358
+ const sqs = new AWS.SQS();
359
+ const messages = await sqs.receiveMessage({ QueueUrl: "https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-complaints-queue", MaxNumberOfMessages: 10 }).promise()
360
+ if (messages.Messages) {
361
+ for (const message of messages.Messages) {
362
+ console.log("[AWS COMPLAINTS] Received complaint message");
363
+ console.log("[AWS COMPLAINTS]", message)
364
+
365
+ if (message.ReceiptHandle) {
366
+ if (STAMHOOFD.environment === "production") {
367
+ await sqs.deleteMessage({
368
+ QueueUrl: "https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-complaints-queue",
369
+ ReceiptHandle: message.ReceiptHandle
370
+ }).promise()
371
+ console.log("[AWS COMPLAINTS] Deleted from queue");
372
+ }
373
+ }
374
+
375
+ try {
376
+ if (message.Body) {
377
+ // decode the JSON value
378
+ const complaint = JSON.parse(message.Body)
379
+ console.log("[AWS COMPLAINTS]", complaint)
380
+
381
+ if (complaint.Message) {
382
+ const message = JSON.parse(complaint.Message)
383
+
384
+ if (message.complaint) {
385
+ const b = message.complaint
386
+ const source = message.mail.source
387
+ const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined
388
+
389
+ const type: "abuse" | "auth-failure" | "fraud" | "not-spam" | "other" | "virus" = b.complaintFeedbackType
390
+
391
+ if (organization) {
392
+ for (const recipient of b.complainedRecipients) {
393
+ const email = recipient.emailAddress
394
+ const emailAddress = await EmailAddress.getOrCreate(email, organization.id)
395
+ emailAddress.markedAsSpam = type !== "not-spam"
396
+ await emailAddress.save()
397
+ }
398
+ } else {
399
+ console.error("[AWS COMPLAINTS] Unknown organization for email address "+source)
400
+ }
401
+
402
+ if (type == "virus" || type == "fraud") {
403
+ console.error("[AWS COMPLAINTS] Received virus / fraud complaint!")
404
+ console.error("[AWS COMPLAINTS]", complaint)
405
+ if (STAMHOOFD.environment === "production") {
406
+ Email.sendInternal({
407
+ to: "simon@stamhoofd.be",
408
+ subject: "Received a "+type+" email notification",
409
+ text: "We received a "+type+" notification for an e-mail from the organization: "+organization?.name+". Please check and adjust if needed.\n"
410
+ }, new I18n("nl", "BE"))
411
+ }
412
+ }
413
+ } else {
414
+ console.log("[AWS COMPLAINTS] Missing complaint field")
415
+ }
416
+ } else {
417
+ console.log("[AWS COMPLAINTS] Missing message field in complaint")
418
+ }
419
+ }
420
+ } catch (e) {
421
+ console.log("[AWS COMPLAINTS] Complain message processing failed:")
422
+ console.error("[AWS COMPLAINTS] Complain message processing failed:")
423
+ console.error("[AWS COMPLAINTS]", e)
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ // Keep checking pending paymetns for 3 days
430
+ async function checkPayments() {
431
+ if (STAMHOOFD.environment === "development") {
432
+ return;
433
+ }
434
+
435
+ const timeout = 60*1000*31;
436
+
437
+ // TODO: only select the ID + organizationId
438
+ const payments = await Payment.where({
439
+ status: {
440
+ sign: "IN",
441
+ value: [PaymentStatus.Created, PaymentStatus.Pending]
442
+ },
443
+ method: {
444
+ sign: "IN",
445
+ value: [PaymentMethod.Bancontact, PaymentMethod.iDEAL, PaymentMethod.Payconiq, PaymentMethod.CreditCard]
446
+ },
447
+ // Check all payments that are 11 minutes old and are still pending
448
+ createdAt: {
449
+ sign: "<",
450
+ value: new Date(new Date().getTime() - timeout)
451
+ },
452
+ }, {
453
+ limit: 100,
454
+
455
+ // Return oldest payments first
456
+ // If at some point, they are still pending after 1 day, their status should change to failed
457
+ sort: [{
458
+ column: 'createdAt',
459
+ direction: 'ASC'
460
+ }]
461
+ })
462
+
463
+ console.log("[DELAYED PAYMENTS] Checking pending payments: "+payments.length)
464
+
465
+ for (const payment of payments) {
466
+ try {
467
+ if (payment.organizationId) {
468
+ const organization = await Organization.getByID(payment.organizationId)
469
+ if (organization) {
470
+ await ExchangePaymentEndpoint.pollStatus(payment.id, organization)
471
+ continue;
472
+ }
473
+ } else {
474
+ // Try stamhoofd payment
475
+ const invoices = await STInvoice.where({ paymentId: payment.id })
476
+ if (invoices.length === 1) {
477
+ await ExchangeSTPaymentEndpoint.pollStatus(payment, invoices[0])
478
+ continue
479
+ }
480
+ }
481
+
482
+ // Check expired
483
+ if (ExchangePaymentEndpoint.isManualExpired(payment.status, payment)) {
484
+ console.error('[DELAYED PAYMENTS] Could not resolve handler for expired payment, marking as failed', payment.id)
485
+ payment.status = PaymentStatus.Failed
486
+ await payment.save()
487
+ }
488
+ } catch (e) {
489
+ console.error(e)
490
+ }
491
+ }
492
+ }
493
+
494
+ let didCheckBuckaroo = false;
495
+ let lastBuckarooId = '';
496
+
497
+ // Time to start checking (needs to be consistent to avoid weird jumps)
498
+ const startBuckarooDate = new Date(new Date().getTime() - 60*1000*60*24*7*4);
499
+
500
+ // Keep checking pending paymetns for 3 days
501
+ async function checkFailedBuckarooPayments() {
502
+ if (STAMHOOFD.environment !== "production") {
503
+ return
504
+ }
505
+
506
+ if (didCheckBuckaroo) {
507
+ return
508
+ }
509
+
510
+ console.log('Checking failed Buckaroo payments')
511
+
512
+ // TODO: only select the ID + organizationId
513
+ const payments = await Payment.where({
514
+ status: {
515
+ sign: "IN",
516
+ value: [PaymentStatus.Failed]
517
+ },
518
+ provider: PaymentProvider.Buckaroo,
519
+
520
+ // Only check payments of last 4 weeks
521
+ createdAt: {
522
+ sign: ">",
523
+ value: startBuckarooDate
524
+ },
525
+ id: {
526
+ sign: ">",
527
+ value: lastBuckarooId
528
+ }
529
+ }, {
530
+ limit: 100,
531
+
532
+ // Sort by ID
533
+ sort: [{
534
+ column: 'id',
535
+ direction: 'ASC'
536
+ }]
537
+ })
538
+
539
+ console.log("[BUCKAROO PAYMENTS] Checking failed payments: "+payments.length)
540
+
541
+ for (const payment of payments) {
542
+ try {
543
+ if (payment.organizationId) {
544
+ const organization = await Organization.getByID(payment.organizationId)
545
+ if (organization) {
546
+ await ExchangePaymentEndpoint.pollStatus(payment.id, organization)
547
+ continue;
548
+ }
549
+ }
550
+ } catch (e) {
551
+ console.error(e)
552
+ }
553
+ }
554
+
555
+ if (payments.length === 0) {
556
+ didCheckBuckaroo = true
557
+ lastBuckarooId = ''
558
+ } else {
559
+ lastBuckarooId = payments[payments.length - 1].id
560
+ }
561
+ }
562
+
563
+ // Unreserve reserved registrations
564
+ async function checkReservedUntil() {
565
+ if (STAMHOOFD.environment !== "development") {
566
+ console.log("Check reserved until...")
567
+ }
568
+
569
+ const registrations = await Registration.where({
570
+ reservedUntil: {
571
+ sign: "<",
572
+ value: new Date()
573
+ },
574
+ }, {
575
+ limit: 200
576
+ })
577
+
578
+ if (registrations.length === 0) {
579
+ return
580
+ }
581
+
582
+ // Clear reservedUntil
583
+ const q = `UPDATE ${Registration.table} SET reservedUntil = NULL where id IN (?) AND reservedUntil < ?`
584
+ await Database.update(q, [registrations.map(r => r.id), new Date()])
585
+
586
+ // Get groups
587
+ const groupIds = registrations.map(r => r.groupId)
588
+ const groups = await Group.where({
589
+ id: {
590
+ sign: "IN",
591
+ value: groupIds
592
+ }
593
+ })
594
+
595
+ // Update occupancy
596
+ for (const group of groups) {
597
+ await group.updateOccupancy()
598
+ await group.save()
599
+ }
600
+ }
601
+
602
+
603
+ // Wait for midnight before checking billing
604
+ let lastBillingCheck: Date | null = new Date()
605
+ let lastBillingId = ""
606
+ async function checkBilling() {
607
+ if (STAMHOOFD.environment === "development") {
608
+ return
609
+ }
610
+
611
+ console.log("[BILLING] Checking billing...")
612
+
613
+ // Wait for the next day before doing a new check
614
+ if (lastBillingCheck && Formatter.dateIso(lastBillingCheck) === Formatter.dateIso(new Date())) {
615
+ console.log("[BILLING] Billing check done for today")
616
+ return
617
+ }
618
+
619
+ const organizations = await Organization.where({ id: { sign: '>', value: lastBillingId } }, {
620
+ limit: 10,
621
+ sort: ["id"]
622
+ })
623
+
624
+ if (organizations.length == 0) {
625
+ // Wait again until next day
626
+ lastBillingId = ""
627
+ lastBillingCheck = new Date()
628
+ return
629
+ }
630
+
631
+ for (const organization of organizations) {
632
+ console.log("[BILLING] Checking billing for "+organization.name)
633
+
634
+ try {
635
+ await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
636
+ await STPendingInvoice.addAutomaticItems(organization)
637
+ });
638
+ } catch (e) {
639
+ console.error(e)
640
+ }
641
+
642
+ }
643
+
644
+ lastBillingId = organizations[organizations.length - 1].id
645
+
646
+ }
647
+
648
+ let lastDripCheck: Date | null = null
649
+ let lastDripId = ""
650
+ async function checkDrips() {
651
+ if (STAMHOOFD.environment === "development") {
652
+ //return;
653
+ }
654
+
655
+ if (lastDripCheck && lastDripCheck > new Date(new Date().getTime() - 6 * 60 * 60 * 1000)) {
656
+ console.log("Skip Drip check")
657
+ return
658
+ }
659
+
660
+ // Only send emails between 8:00 - 18:00 CET
661
+ const CETTime = Formatter.timeIso(new Date())
662
+ if ((CETTime < "08:00" || CETTime > "18:00") && STAMHOOFD.environment === "production") {
663
+ console.log("Skip Drip check: outside hours")
664
+ return;
665
+ }
666
+
667
+ const organizations = await Organization.where({ id: { sign: '>', value: lastDripId } }, {
668
+ limit: STAMHOOFD.environment === "production" ? 30 : 100,
669
+ sort: ["id"]
670
+ })
671
+
672
+ if (organizations.length == 0) {
673
+ // Wait before starting again
674
+ lastDripId = ""
675
+ lastDripCheck = new Date()
676
+ return
677
+ }
678
+
679
+ console.log("Checking drips...")
680
+
681
+ for (const organization of organizations) {
682
+ console.log(organization.name)
683
+ try {
684
+ await organization.checkDrips()
685
+ } catch (e) {
686
+ console.error(e);
687
+ }
688
+ }
689
+
690
+ lastDripId = organizations[organizations.length - 1].id
691
+
692
+ }
693
+
694
+ type CronJobDefinition = {
695
+ name: string,
696
+ method: () => Promise<void>,
697
+ running: boolean
698
+ }
699
+
700
+ const registeredCronJobs: CronJobDefinition[] = []
701
+
702
+ registeredCronJobs.push({
703
+ name: 'checkSettlements',
704
+ method: checkSettlements,
705
+ running: false
706
+ });
707
+
708
+ registeredCronJobs.push({
709
+ name: 'checkFailedBuckarooPayments',
710
+ method: checkFailedBuckarooPayments,
711
+ running: false
712
+ });
713
+
714
+ registeredCronJobs.push({
715
+ name: 'checkExpirationEmails',
716
+ method: checkExpirationEmails,
717
+ running: false
718
+ });
719
+
720
+ registeredCronJobs.push({
721
+ name: 'checkPostmarkBounces',
722
+ method: checkPostmarkBounces,
723
+ running: false
724
+ });
725
+
726
+ registeredCronJobs.push({
727
+ name: 'checkBilling',
728
+ method: checkBilling,
729
+ running: false
730
+ });
731
+
732
+ registeredCronJobs.push({
733
+ name: 'checkReservedUntil',
734
+ method: checkReservedUntil,
735
+ running: false
736
+ });
737
+
738
+ registeredCronJobs.push({
739
+ name: 'checkComplaints',
740
+ method: checkComplaints,
741
+ running: false
742
+ });
743
+
744
+ registeredCronJobs.push({
745
+ name: 'checkReplies',
746
+ method: checkReplies,
747
+ running: false
748
+ });
749
+
750
+ registeredCronJobs.push({
751
+ name: 'checkBounces',
752
+ method: checkBounces,
753
+ running: false
754
+ });
755
+
756
+ registeredCronJobs.push({
757
+ name: 'checkDNS',
758
+ method: checkDNS,
759
+ running: false
760
+ });
761
+
762
+ registeredCronJobs.push({
763
+ name: 'checkWebshopDNS',
764
+ method: checkWebshopDNS,
765
+ running: false
766
+ });
767
+
768
+ registeredCronJobs.push({
769
+ name: 'checkPayments',
770
+ method: checkPayments,
771
+ running: false
772
+ });
773
+
774
+ registeredCronJobs.push({
775
+ name: 'checkDrips',
776
+ method: checkDrips,
777
+ running: false
778
+ });
779
+
780
+ async function run(name: string, handler: () => Promise<void>) {
781
+ try {
782
+ await logger.setContext({
783
+ prefixes: [
784
+ new StyledText(`[${name}] `).addClass('crons', 'tag')
785
+ ],
786
+ tags: ['crons']
787
+ }, async () => {
788
+ try {
789
+ await handler()
790
+ } catch (e) {
791
+ console.error(new StyledText(e).addClass('error'))
792
+ }
793
+ })
794
+ } catch (e) {
795
+ console.error(new StyledText(e).addClass('error'))
796
+ }
797
+ }
798
+
799
+ let stopCrons = false;
800
+ export function stopCronScheduling() {
801
+ stopCrons = true;
802
+ }
803
+
804
+ let schedulingJobs = false;
805
+ export function areCronsRunning(): boolean {
806
+ if (schedulingJobs && !stopCrons) {
807
+ return true
808
+ }
809
+
810
+ for (const job of registeredCronJobs) {
811
+ if (job.running) {
812
+ return true
813
+ }
814
+ }
815
+ return false
816
+ }
817
+
818
+ export const crons = async () => {
819
+ if (STAMHOOFD.CRONS_DISABLED) {
820
+ console.log("Crons are disabled. Make sure to enable them in the environment variables.")
821
+ return;
822
+ }
823
+
824
+ schedulingJobs = true;
825
+ for (const job of registeredCronJobs) {
826
+ if (stopCrons) {
827
+ break;
828
+ }
829
+ if (job.running) {
830
+ continue;
831
+ }
832
+ job.running = true
833
+ run(job.name, job.method).finally(() => {
834
+ job.running = false
835
+ }).catch(e => {
836
+ console.error(e)
837
+ });
838
+
839
+ // Prevent starting too many jobs at once
840
+ if (STAMHOOFD.environment !== "development") {
841
+ await sleep(10 * 1000);
842
+ }
843
+ }
844
+ schedulingJobs = false;
845
+ };