@stamhoofd/models 2.3.0 → 2.5.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.
- package/dist/src/factories/GroupFactory.d.ts.map +1 -1
- package/dist/src/factories/GroupFactory.js +5 -5
- package/dist/src/factories/GroupFactory.js.map +1 -1
- package/dist/src/helpers/EmailBuilder.d.ts +3 -2
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +29 -16
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/helpers/GroupBuilder.d.ts.map +1 -1
- package/dist/src/helpers/GroupBuilder.js +0 -23
- package/dist/src/helpers/GroupBuilder.js.map +1 -1
- package/dist/src/migrations/1721050380-email-table.sql +24 -0
- package/dist/src/migrations/1721050381-email-recipients-table.sql +18 -0
- package/dist/src/migrations/1721342679-responsibility-groupId.sql +2 -0
- package/dist/src/migrations/1721342680-responsibility-groupId-foreign-key.sql +1 -0
- package/dist/src/migrations/1721400546-users-memberId.sql +3 -0
- package/dist/src/migrations/1721639159-membership-deleted-at.sql +2 -0
- package/dist/src/migrations/1721639160-membership-generated.sql +2 -0
- package/dist/src/migrations/1721841819-group-type.sql +2 -0
- package/dist/src/migrations/1722090482-events.sql +18 -0
- package/dist/src/migrations/1722269236-group-waitinglist-id.sql +4 -0
- package/dist/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
- package/dist/src/migrations/1722525787-depending-balance-item.sql +2 -0
- package/dist/src/models/BalanceItem.d.ts +7 -0
- package/dist/src/models/BalanceItem.d.ts.map +1 -1
- package/dist/src/models/BalanceItem.js +34 -1
- package/dist/src/models/BalanceItem.js.map +1 -1
- package/dist/src/models/DocumentTemplate.js +14 -14
- package/dist/src/models/DocumentTemplate.js.map +1 -1
- package/dist/src/models/Email.d.ts +41 -0
- package/dist/src/models/Email.d.ts.map +1 -0
- package/dist/src/models/Email.js +490 -0
- package/dist/src/models/Email.js.map +1 -0
- package/dist/src/models/EmailRecipient.d.ts +20 -0
- package/dist/src/models/EmailRecipient.d.ts.map +1 -0
- package/dist/src/models/EmailRecipient.js +95 -0
- package/dist/src/models/EmailRecipient.js.map +1 -0
- package/dist/src/models/EmailTemplate.d.ts +2 -1
- package/dist/src/models/EmailTemplate.d.ts.map +1 -1
- package/dist/src/models/EmailTemplate.js +4 -0
- package/dist/src/models/EmailTemplate.js.map +1 -1
- package/dist/src/models/Event.d.ts +19 -0
- package/dist/src/models/Event.d.ts.map +1 -0
- package/dist/src/models/Event.js +78 -0
- package/dist/src/models/Event.js.map +1 -0
- package/dist/src/models/Group.d.ts +9 -1
- package/dist/src/models/Group.d.ts.map +1 -1
- package/dist/src/models/Group.js +25 -22
- package/dist/src/models/Group.js.map +1 -1
- package/dist/src/models/Member.d.ts +1 -0
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +42 -11
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/MemberPlatformMembership.d.ts +7 -0
- package/dist/src/models/MemberPlatformMembership.d.ts.map +1 -1
- package/dist/src/models/MemberPlatformMembership.js +22 -0
- package/dist/src/models/MemberPlatformMembership.js.map +1 -1
- package/dist/src/models/MemberResponsibilityRecord.d.ts +3 -0
- package/dist/src/models/MemberResponsibilityRecord.d.ts.map +1 -1
- package/dist/src/models/MemberResponsibilityRecord.js +8 -0
- package/dist/src/models/MemberResponsibilityRecord.js.map +1 -1
- package/dist/src/models/Order.js +1 -1
- package/dist/src/models/Order.js.map +1 -1
- package/dist/src/models/Organization.d.ts +3 -4
- package/dist/src/models/Organization.d.ts.map +1 -1
- package/dist/src/models/Organization.js +28 -24
- package/dist/src/models/Organization.js.map +1 -1
- package/dist/src/models/OrganizationRegistrationPeriod.d.ts +1 -0
- package/dist/src/models/OrganizationRegistrationPeriod.d.ts.map +1 -1
- package/dist/src/models/OrganizationRegistrationPeriod.js +7 -0
- package/dist/src/models/OrganizationRegistrationPeriod.js.map +1 -1
- package/dist/src/models/RegistrationPeriod.d.ts +1 -0
- package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
- package/dist/src/models/RegistrationPeriod.js +12 -0
- package/dist/src/models/RegistrationPeriod.js.map +1 -1
- package/dist/src/models/User.d.ts +2 -1
- package/dist/src/models/User.d.ts.map +1 -1
- package/dist/src/models/User.js +13 -3
- package/dist/src/models/User.js.map +1 -1
- package/dist/src/models/index.d.ts +3 -0
- package/dist/src/models/index.d.ts.map +1 -1
- package/dist/src/models/index.js +3 -0
- package/dist/src/models/index.js.map +1 -1
- package/package.json +2 -2
- package/src/factories/GroupFactory.ts +6 -6
- package/src/helpers/EmailBuilder.ts +33 -18
- package/src/helpers/GroupBuilder.ts +0 -23
- package/src/migrations/1721050380-email-table.sql +24 -0
- package/src/migrations/1721050381-email-recipients-table.sql +18 -0
- package/src/migrations/1721342679-responsibility-groupId.sql +2 -0
- package/src/migrations/1721342680-responsibility-groupId-foreign-key.sql +1 -0
- package/src/migrations/1721400546-users-memberId.sql +3 -0
- package/src/migrations/1721639159-membership-deleted-at.sql +2 -0
- package/src/migrations/1721639160-membership-generated.sql +2 -0
- package/src/migrations/1721841819-group-type.sql +2 -0
- package/src/migrations/1722090482-events.sql +18 -0
- package/src/migrations/1722269236-group-waitinglist-id.sql +4 -0
- package/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
- package/src/migrations/1722525787-depending-balance-item.sql +2 -0
- package/src/models/BalanceItem.ts +38 -1
- package/src/models/DocumentTemplate.ts +2 -2
- package/src/models/Email.ts +556 -0
- package/src/models/EmailRecipient.ts +81 -0
- package/src/models/EmailTemplate.ts +5 -1
- package/src/models/Event.ts +71 -0
- package/src/models/Group.ts +27 -37
- package/src/models/Member.ts +60 -12
- package/src/models/MemberPlatformMembership.ts +21 -0
- package/src/models/MemberResponsibilityRecord.ts +7 -0
- package/src/models/Order.ts +2 -2
- package/src/models/Organization.ts +32 -28
- package/src/models/OrganizationRegistrationPeriod.ts +8 -0
- package/src/models/RegistrationPeriod.ts +14 -0
- package/src/models/User.ts +13 -3
- package/src/models/index.ts +3 -0
- package/dist/src/assets/assets/Metropolis-Black.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-BlackItalic.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-Bold.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-BoldItalic.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-ExtraBold.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-ExtraBoldItalic.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-ExtraLight.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-ExtraLightItalic.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-Light.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-LightItalic.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-Medium.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-MediumItalic.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-Regular.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-RegularItalic.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-SemiBold.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-SemiBoldItalic.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-Thin.woff2 +0 -0
- package/dist/src/assets/assets/Metropolis-ThinItalic.woff2 +0 -0
- package/dist/src/assets/assets/logo.png +0 -0
|
@@ -18,15 +18,22 @@ export class BalanceItem extends Model {
|
|
|
18
18
|
})
|
|
19
19
|
id!: string;
|
|
20
20
|
|
|
21
|
+
// Receiving organization
|
|
22
|
+
|
|
21
23
|
@column({ type: "string" })
|
|
22
24
|
organizationId: string
|
|
23
25
|
|
|
26
|
+
// Payer: memberId, userId or payingOrganizationId
|
|
27
|
+
|
|
24
28
|
@column({ type: "string", nullable: true })
|
|
25
29
|
memberId: string | null = null;
|
|
26
30
|
|
|
27
31
|
@column({ type: "string", nullable: true })
|
|
28
32
|
userId: string | null = null;
|
|
29
33
|
|
|
34
|
+
@column({ type: "string", nullable: true })
|
|
35
|
+
payingOrganizationId: string | null = null;
|
|
36
|
+
|
|
30
37
|
/**
|
|
31
38
|
* The registration ID that is linked to this balance item
|
|
32
39
|
*/
|
|
@@ -39,6 +46,14 @@ export class BalanceItem extends Model {
|
|
|
39
46
|
@column({ type: "string", nullable: true })
|
|
40
47
|
orderId: string | null = null;
|
|
41
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The depending balance item ID that is linked to this balance item
|
|
51
|
+
* -> as soon as this balance item is paid, we'll mark this balance item as pending if it is still hidden
|
|
52
|
+
* -> allows for a pay back system where one user needs to pay back a different user
|
|
53
|
+
*/
|
|
54
|
+
@column({ type: "string", nullable: true })
|
|
55
|
+
dependingBalanceItemId: string | null = null;
|
|
56
|
+
|
|
42
57
|
@column({ type: "string" })
|
|
43
58
|
description = "";
|
|
44
59
|
|
|
@@ -137,6 +152,28 @@ export class BalanceItem extends Model {
|
|
|
137
152
|
}
|
|
138
153
|
}
|
|
139
154
|
}
|
|
155
|
+
|
|
156
|
+
// Do we have a different connected balance item?
|
|
157
|
+
// Make it visible if this one is paid
|
|
158
|
+
if (this.dependingBalanceItemId) {
|
|
159
|
+
const depending = await BalanceItem.getByID(this.dependingBalanceItemId)
|
|
160
|
+
if (depending) {
|
|
161
|
+
if (this.status === BalanceItemStatus.Hidden) {
|
|
162
|
+
depending.status = BalanceItemStatus.Pending
|
|
163
|
+
await depending.save()
|
|
164
|
+
|
|
165
|
+
if (depending.memberId) {
|
|
166
|
+
const {Member} = await import("./Member");
|
|
167
|
+
await Member.updateOutstandingBalance([depending.memberId])
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (depending.registrationId) {
|
|
171
|
+
const {Registration} = await import("./Registration");
|
|
172
|
+
await Registration.updateOutstandingBalance([depending.registrationId], depending.organizationId)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
140
177
|
}
|
|
141
178
|
|
|
142
179
|
async undoPaid(payment: Payment, organization: Organization) {
|
|
@@ -178,7 +215,7 @@ export class BalanceItem extends Model {
|
|
|
178
215
|
}
|
|
179
216
|
|
|
180
217
|
updateStatus() {
|
|
181
|
-
this.status = this.pricePaid >= this.price ? BalanceItemStatus.Paid : BalanceItemStatus.Pending;
|
|
218
|
+
this.status = this.pricePaid >= this.price ? BalanceItemStatus.Paid : (this.pricePaid > 0 ? BalanceItemStatus.Pending : (this.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending));
|
|
182
219
|
}
|
|
183
220
|
|
|
184
221
|
static async deleteItems(items: BalanceItem[]) {
|
|
@@ -96,14 +96,14 @@ export class DocumentTemplate extends Model {
|
|
|
96
96
|
id: "registration.startDate",
|
|
97
97
|
type: RecordType.Date,
|
|
98
98
|
}), // settings will be overwritten
|
|
99
|
-
dateValue: group?.settings?.
|
|
99
|
+
dateValue: group?.settings?.startDate
|
|
100
100
|
}),
|
|
101
101
|
"registration.endDate": RecordDateAnswer.create({
|
|
102
102
|
settings: RecordSettings.create({
|
|
103
103
|
id: "registration.endDate",
|
|
104
104
|
type: RecordType.Date,
|
|
105
105
|
}), // settings will be overwritten
|
|
106
|
-
dateValue: group?.settings?.
|
|
106
|
+
dateValue: group?.settings?.endDate
|
|
107
107
|
}),
|
|
108
108
|
"registration.price":
|
|
109
109
|
RecordPriceAnswer.create({
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { column, Model } from '@simonbackx/simple-database';
|
|
2
|
+
import { EditorSmartButton, EditorSmartVariable, EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, LimitedFilteredRequest, PaginatedResponse, Recipient, SortItemDirection } from '@stamhoofd/structures';
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
|
|
5
|
+
import { AnyDecoder, ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
6
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
|
+
import { SQL, SQLWhereSign } from '@stamhoofd/sql';
|
|
9
|
+
import { EmailRecipient } from './EmailRecipient';
|
|
10
|
+
import { getEmailBuilder } from '../helpers/EmailBuilder';
|
|
11
|
+
import { Organization } from './Organization';
|
|
12
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
13
|
+
import { Email as EmailClass } from "@stamhoofd/email";
|
|
14
|
+
|
|
15
|
+
export class Email extends Model {
|
|
16
|
+
static table = "emails";
|
|
17
|
+
|
|
18
|
+
@column({
|
|
19
|
+
primary: true, type: "string", beforeSave(value) {
|
|
20
|
+
return value ?? uuidv4();
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
id!: string;
|
|
24
|
+
|
|
25
|
+
@column({ type: "string", nullable: true })
|
|
26
|
+
organizationId: string|null = null;
|
|
27
|
+
|
|
28
|
+
@column({ type: "string", nullable: true})
|
|
29
|
+
userId: string|null = null;
|
|
30
|
+
|
|
31
|
+
@column({ type: "json", decoder: EmailRecipientFilter })
|
|
32
|
+
recipientFilter: EmailRecipientFilter = EmailRecipientFilter.create({})
|
|
33
|
+
|
|
34
|
+
@column({ type: "string", nullable: true })
|
|
35
|
+
subject: string|null
|
|
36
|
+
|
|
37
|
+
/** Raw json structure to edit the template */
|
|
38
|
+
@column({ type: "json", decoder: AnyDecoder })
|
|
39
|
+
json: any = {};
|
|
40
|
+
|
|
41
|
+
@column({ type: "string", nullable: true })
|
|
42
|
+
html: string|null = null
|
|
43
|
+
|
|
44
|
+
@column({ type: "string", nullable: true})
|
|
45
|
+
text: string|null = null
|
|
46
|
+
|
|
47
|
+
@column({ type: "string", nullable: true})
|
|
48
|
+
fromAddress: string|null = null
|
|
49
|
+
|
|
50
|
+
@column({ type: "string", nullable: true})
|
|
51
|
+
fromName: string|null = null
|
|
52
|
+
|
|
53
|
+
@column({ type: "integer", nullable: true})
|
|
54
|
+
recipientCount: number|null = null
|
|
55
|
+
|
|
56
|
+
@column({ type: "string" })
|
|
57
|
+
status = EmailStatus.Draft;
|
|
58
|
+
|
|
59
|
+
@column({ type: "string" })
|
|
60
|
+
recipientsStatus = EmailRecipientsStatus.NotCreated;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* todo: ignore automatically
|
|
64
|
+
*/
|
|
65
|
+
@column({ type: "json", decoder: new ArrayDecoder(EmailAttachment) })
|
|
66
|
+
attachments: EmailAttachment[] = []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@column({
|
|
70
|
+
type: "datetime",
|
|
71
|
+
nullable: true
|
|
72
|
+
})
|
|
73
|
+
sentAt: Date|null = null
|
|
74
|
+
|
|
75
|
+
@column({
|
|
76
|
+
type: "datetime", beforeSave(old?: any) {
|
|
77
|
+
if (old !== undefined) {
|
|
78
|
+
return old;
|
|
79
|
+
}
|
|
80
|
+
const date = new Date()
|
|
81
|
+
date.setMilliseconds(0)
|
|
82
|
+
return date
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
createdAt: Date
|
|
86
|
+
|
|
87
|
+
@column({
|
|
88
|
+
type: "datetime", beforeSave() {
|
|
89
|
+
const date = new Date()
|
|
90
|
+
date.setMilliseconds(0)
|
|
91
|
+
return date
|
|
92
|
+
},
|
|
93
|
+
skipUpdate: true
|
|
94
|
+
})
|
|
95
|
+
updatedAt: Date
|
|
96
|
+
|
|
97
|
+
static recipientLoaders: Map<EmailRecipientFilterType, {
|
|
98
|
+
fetch(request: LimitedFilteredRequest): Promise<PaginatedResponse<EmailRecipientStruct[], LimitedFilteredRequest>>
|
|
99
|
+
count(request: LimitedFilteredRequest): Promise<number>
|
|
100
|
+
}> = new Map()
|
|
101
|
+
|
|
102
|
+
throwIfNotReadyToSend() {
|
|
103
|
+
if (this.subject == null || this.subject.length == 0) {
|
|
104
|
+
throw new SimpleError({
|
|
105
|
+
code: 'invalid_field',
|
|
106
|
+
message: 'Missing subject',
|
|
107
|
+
human: 'Vul een onderwerp in voor je een e-mail verstuurt'
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.text == null || this.text.length == 0) {
|
|
112
|
+
throw new SimpleError({
|
|
113
|
+
code: 'invalid_field',
|
|
114
|
+
message: 'Missing text',
|
|
115
|
+
human: 'Vul een tekst in voor je een e-mail verstuurt'
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.html == null || this.html.length == 0) {
|
|
120
|
+
throw new SimpleError({
|
|
121
|
+
code: 'invalid_field',
|
|
122
|
+
message: 'Missing html',
|
|
123
|
+
human: 'Vul een tekst in voor je een e-mail verstuurt'
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getFromAddress() {
|
|
129
|
+
if (!this.fromName) {
|
|
130
|
+
return this.fromAddress!
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const cleanedName = Formatter.emailSenderName(this.fromName)
|
|
134
|
+
if (cleanedName.length < 2) {
|
|
135
|
+
return this.fromAddress!
|
|
136
|
+
}
|
|
137
|
+
return '"'+cleanedName+'" <'+this.fromAddress+'>'
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getDefaultFromAddress(organization?: Organization|null): string {
|
|
142
|
+
let address = "noreply@stamhoofd.email";
|
|
143
|
+
|
|
144
|
+
if (organization) {
|
|
145
|
+
address = organization.uri+"@stamhoofd.email";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!this.fromName) {
|
|
149
|
+
return address
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const cleanedName = Formatter.emailSenderName(this.fromName)
|
|
153
|
+
if (cleanedName.length < 2) {
|
|
154
|
+
return address
|
|
155
|
+
}
|
|
156
|
+
return '"'+cleanedName+'" <'+address+'>'
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async send() {
|
|
161
|
+
this.throwIfNotReadyToSend()
|
|
162
|
+
await this.save();
|
|
163
|
+
|
|
164
|
+
const id = this.id;
|
|
165
|
+
return await QueueHandler.schedule('send-email', async function () {
|
|
166
|
+
let upToDate = await Email.getByID(id);
|
|
167
|
+
if (!upToDate) {
|
|
168
|
+
throw new SimpleError({
|
|
169
|
+
code: 'not_found',
|
|
170
|
+
message: 'Email not found',
|
|
171
|
+
human: 'De e-mail die je probeert te versturen bestaat niet meer'
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
if (upToDate.status === EmailStatus.Sent) {
|
|
175
|
+
// Already done
|
|
176
|
+
// In other cases -> queue has stopped and we can retry
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const organization = upToDate.organizationId ? await Organization.getByID(upToDate.organizationId) : null;
|
|
180
|
+
upToDate.throwIfNotReadyToSend()
|
|
181
|
+
|
|
182
|
+
let from = upToDate.getDefaultFromAddress(organization)
|
|
183
|
+
let replyTo: string | null = upToDate.getFromAddress();
|
|
184
|
+
|
|
185
|
+
if (!from) {
|
|
186
|
+
throw new SimpleError({
|
|
187
|
+
code: 'invalid_field',
|
|
188
|
+
message: 'Missing from',
|
|
189
|
+
human: 'Vul een afzender in voor je een e-mail verstuurt'
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Can we send from this e-mail or reply-to?
|
|
194
|
+
if (organization) {
|
|
195
|
+
if (organization.privateMeta.mailDomain && organization.privateMeta.mailDomainActive && upToDate.fromAddress!.endsWith("@"+organization.privateMeta.mailDomain)) {
|
|
196
|
+
from = upToDate.getFromAddress();
|
|
197
|
+
replyTo = null;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
// Platform
|
|
201
|
+
const domains = Object.values(STAMHOOFD.domains.marketing)
|
|
202
|
+
|
|
203
|
+
for (const domain of domains) {
|
|
204
|
+
if (upToDate.fromAddress!.endsWith("@"+domain)) {
|
|
205
|
+
from = upToDate.getFromAddress();
|
|
206
|
+
replyTo = null;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
upToDate.status = EmailStatus.Sending
|
|
213
|
+
upToDate.sentAt = upToDate.sentAt ?? new Date()
|
|
214
|
+
await upToDate.save();
|
|
215
|
+
|
|
216
|
+
// Create recipients if not yet created
|
|
217
|
+
await upToDate.buildRecipients()
|
|
218
|
+
|
|
219
|
+
// Refresh model
|
|
220
|
+
upToDate = await Email.getByID(id);
|
|
221
|
+
if (!upToDate) {
|
|
222
|
+
throw new SimpleError({
|
|
223
|
+
code: 'not_found',
|
|
224
|
+
message: 'Email not found',
|
|
225
|
+
human: 'De e-mail die je probeert te versturen bestaat niet meer'
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (upToDate.recipientsStatus !== EmailRecipientsStatus.Created) {
|
|
230
|
+
throw new SimpleError({
|
|
231
|
+
code: 'recipients_not_created',
|
|
232
|
+
message: 'Failed to create recipients',
|
|
233
|
+
human: 'Er ging iets mis bij het aanmaken van de afzenders.'
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Start actually sending in batches of recipients that are not yet sent
|
|
238
|
+
let idPointer = '';
|
|
239
|
+
const batchSize = 100;
|
|
240
|
+
const recipientsSet = new Set<string>();
|
|
241
|
+
|
|
242
|
+
// eslint-disable-next-line no-constant-condition
|
|
243
|
+
while (true) {
|
|
244
|
+
const q = SQL.select()
|
|
245
|
+
.from(SQL.table('email_recipients'))
|
|
246
|
+
.where(SQL.column('emailId'), upToDate.id)
|
|
247
|
+
.where(SQL.column('sentAt'), null)
|
|
248
|
+
.where(SQL.column('id'), SQLWhereSign.Greater, idPointer);
|
|
249
|
+
|
|
250
|
+
q.orderBy(SQL.column('id'), 'ASC')
|
|
251
|
+
q.limit(batchSize)
|
|
252
|
+
|
|
253
|
+
const data = await q.fetch();
|
|
254
|
+
|
|
255
|
+
const recipients = EmailRecipient.fromRows(data, 'email_recipients');
|
|
256
|
+
|
|
257
|
+
if (recipients.length == 0) {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const sendingPromises: Promise<void>[] = [];
|
|
262
|
+
|
|
263
|
+
for (const recipient of recipients) {
|
|
264
|
+
if (recipientsSet.has(recipient.id)) {
|
|
265
|
+
console.error('Found duplicate recipient while sending email', recipient.id)
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
recipientsSet.add(recipient.email);
|
|
270
|
+
idPointer = recipient.id;
|
|
271
|
+
|
|
272
|
+
let promiseResolve: (value: void | PromiseLike<void>) => void
|
|
273
|
+
const promise = new Promise<void>((resolve) => {
|
|
274
|
+
promiseResolve = resolve;
|
|
275
|
+
});
|
|
276
|
+
sendingPromises.push(promise)
|
|
277
|
+
|
|
278
|
+
const callback = async (error: Error|null) => {
|
|
279
|
+
if (error === null) {
|
|
280
|
+
// Mark saved
|
|
281
|
+
recipient.sentAt = new Date();
|
|
282
|
+
await recipient.save()
|
|
283
|
+
} else {
|
|
284
|
+
recipient.failCount += 1;
|
|
285
|
+
recipient.failErrorMessage = error.message;
|
|
286
|
+
recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
|
|
287
|
+
recipient.lastFailedAt = new Date();
|
|
288
|
+
await recipient.save()
|
|
289
|
+
}
|
|
290
|
+
promiseResolve()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Do send the email
|
|
294
|
+
// Create e-mail builder
|
|
295
|
+
const builder = await getEmailBuilder(organization ?? null, {
|
|
296
|
+
recipients: [
|
|
297
|
+
Recipient.create({
|
|
298
|
+
...recipient
|
|
299
|
+
})
|
|
300
|
+
],
|
|
301
|
+
from,
|
|
302
|
+
replyTo,
|
|
303
|
+
subject: upToDate.subject!,
|
|
304
|
+
html: upToDate.html,
|
|
305
|
+
type: "broadcast",
|
|
306
|
+
callback(error: Error|null ) {
|
|
307
|
+
callback(error).catch(console.error)
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
EmailClass.schedule(builder)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await Promise.all(sendingPromises);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log('Finished sending email', upToDate.id);
|
|
318
|
+
|
|
319
|
+
// Mark email as sent
|
|
320
|
+
upToDate.status = EmailStatus.Sent;
|
|
321
|
+
await upToDate.save();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
updateCount() {
|
|
326
|
+
const id = this.id;
|
|
327
|
+
QueueHandler.schedule('email-count-'+this.id, async function () {
|
|
328
|
+
let upToDate = await Email.getByID(id);
|
|
329
|
+
|
|
330
|
+
if (!upToDate || upToDate.sentAt || !upToDate.id || upToDate.status !== EmailStatus.Draft) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (upToDate.recipientsStatus === EmailRecipientsStatus.Created) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let count = 0;
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
for (const subfilter of upToDate.recipientFilter.filters) {
|
|
342
|
+
|
|
343
|
+
// Create recipients
|
|
344
|
+
const loader = Email.recipientLoaders.get(subfilter.type);
|
|
345
|
+
|
|
346
|
+
if (!loader) {
|
|
347
|
+
throw new Error('Loader for type ' + subfilter.type+' has not been initialised on the Email model')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const request = new LimitedFilteredRequest({
|
|
351
|
+
filter: subfilter.filter,
|
|
352
|
+
sort: [{key: 'id', order: SortItemDirection.ASC}],
|
|
353
|
+
limit: 1,
|
|
354
|
+
search: subfilter.search,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const c = await loader.count(request);
|
|
358
|
+
|
|
359
|
+
count += c
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Check if we have a more reliable recipientCount in the meantime
|
|
363
|
+
upToDate = await Email.getByID(id);
|
|
364
|
+
|
|
365
|
+
if (!upToDate) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (upToDate.recipientsStatus === EmailRecipientsStatus.Created) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
upToDate.recipientCount = count;
|
|
372
|
+
await upToDate.save();
|
|
373
|
+
} catch (e) {
|
|
374
|
+
console.error("Failed to update count for email", id);
|
|
375
|
+
console.error(e);
|
|
376
|
+
}
|
|
377
|
+
}).catch(console.error);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async buildRecipients() {
|
|
381
|
+
const id = this.id;
|
|
382
|
+
await QueueHandler.schedule('email-build-recipients-'+this.id, async function () {
|
|
383
|
+
const upToDate = await Email.getByID(id);
|
|
384
|
+
|
|
385
|
+
if (!upToDate || !upToDate.id) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (upToDate.recipientsStatus === EmailRecipientsStatus.Created) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (upToDate.status === EmailStatus.Sent) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// If it is already creating -> something went wrong (e.g. server restart) and we can safely try again
|
|
398
|
+
|
|
399
|
+
upToDate.recipientsStatus = EmailRecipientsStatus.Creating;
|
|
400
|
+
await upToDate.save();
|
|
401
|
+
|
|
402
|
+
let count = 0;
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// Delete all recipients
|
|
406
|
+
await SQL
|
|
407
|
+
.delete()
|
|
408
|
+
.from(
|
|
409
|
+
SQL.table('email_recipients')
|
|
410
|
+
)
|
|
411
|
+
.where(SQL.column('emailId'), upToDate.id);
|
|
412
|
+
|
|
413
|
+
for (const subfilter of upToDate.recipientFilter.filters) {
|
|
414
|
+
|
|
415
|
+
// Create recipients
|
|
416
|
+
const loader = Email.recipientLoaders.get(subfilter.type);
|
|
417
|
+
|
|
418
|
+
if (!loader) {
|
|
419
|
+
throw new Error('Loader for type ' + subfilter.type+' has not been initialised on the Email model')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let request: LimitedFilteredRequest|null = new LimitedFilteredRequest({
|
|
423
|
+
filter: subfilter.filter,
|
|
424
|
+
sort: [{key: 'id', order: SortItemDirection.ASC}],
|
|
425
|
+
limit: 1000,
|
|
426
|
+
search: subfilter.search,
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
while (request) {
|
|
430
|
+
console.log('Loading page', subfilter.type, request)
|
|
431
|
+
const response = await loader.fetch(request);
|
|
432
|
+
|
|
433
|
+
count += response.results.length;
|
|
434
|
+
|
|
435
|
+
for (const item of response.results) {
|
|
436
|
+
const recipient = new EmailRecipient();
|
|
437
|
+
recipient.emailId = upToDate.id;
|
|
438
|
+
recipient.email = item.email
|
|
439
|
+
recipient.firstName = item.firstName
|
|
440
|
+
recipient.lastName = item.lastName
|
|
441
|
+
recipient.replacements = item.replacements
|
|
442
|
+
|
|
443
|
+
await recipient.save();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
request = response.next ?? null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// todo: loop all members that match the filter in batches of 1000
|
|
451
|
+
// create a new row for every member + calculate the replacement values
|
|
452
|
+
// todo: do intermediate checks on whether the email was deleted, and stop processing if needed
|
|
453
|
+
|
|
454
|
+
upToDate.recipientsStatus = EmailRecipientsStatus.Created;
|
|
455
|
+
upToDate.recipientCount = count;
|
|
456
|
+
await upToDate.save();
|
|
457
|
+
} catch (e) {
|
|
458
|
+
console.error("Failed to build recipients for email", id);
|
|
459
|
+
console.error(e);
|
|
460
|
+
upToDate.recipientsStatus = EmailRecipientsStatus.NotCreated;
|
|
461
|
+
await upToDate.save();
|
|
462
|
+
}
|
|
463
|
+
}).catch(console.error);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async buildExampleRecipient() {
|
|
467
|
+
const id = this.id;
|
|
468
|
+
await QueueHandler.schedule('email-build-recipients-'+this.id, async function () {
|
|
469
|
+
const upToDate = await Email.getByID(id);
|
|
470
|
+
|
|
471
|
+
if (!upToDate || upToDate.sentAt || !upToDate.id) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (upToDate.recipientsStatus !== EmailRecipientsStatus.NotCreated) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
// Delete all recipients
|
|
481
|
+
await SQL
|
|
482
|
+
.delete()
|
|
483
|
+
.from(
|
|
484
|
+
SQL.table('email_recipients')
|
|
485
|
+
)
|
|
486
|
+
.where(SQL.column('emailId'), upToDate.id);
|
|
487
|
+
|
|
488
|
+
for (const subfilter of upToDate.recipientFilter.filters) {
|
|
489
|
+
|
|
490
|
+
// Create recipients
|
|
491
|
+
const loader = Email.recipientLoaders.get(subfilter.type);
|
|
492
|
+
|
|
493
|
+
if (!loader) {
|
|
494
|
+
throw new Error('Loader for type ' + subfilter.type+' has not been initialised on the Email model')
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let request: LimitedFilteredRequest|null = new LimitedFilteredRequest({
|
|
498
|
+
filter: subfilter.filter,
|
|
499
|
+
sort: [{key: 'id', order: SortItemDirection.ASC}],
|
|
500
|
+
limit: 1,
|
|
501
|
+
search: subfilter.search,
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
while (request) {
|
|
505
|
+
console.log('Loading page', subfilter.type, request)
|
|
506
|
+
const response = await loader.fetch(request);
|
|
507
|
+
|
|
508
|
+
// Note: it is possible that a result in the database doesn't return a recipient (in memory filtering)
|
|
509
|
+
// so we do need pagination
|
|
510
|
+
|
|
511
|
+
for (const item of response.results) {
|
|
512
|
+
const recipient = new EmailRecipient();
|
|
513
|
+
recipient.emailId = upToDate.id;
|
|
514
|
+
recipient.email = item.email
|
|
515
|
+
recipient.firstName = item.firstName
|
|
516
|
+
recipient.lastName = item.lastName
|
|
517
|
+
recipient.replacements = item.replacements
|
|
518
|
+
await recipient.save();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
request = response.next ?? null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.warn('No example recipient found for email', id)
|
|
527
|
+
} catch (e) {
|
|
528
|
+
console.error("Failed to build example recipient for email", id);
|
|
529
|
+
console.error(e);
|
|
530
|
+
}
|
|
531
|
+
}).catch(console.error);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
getStructure() {
|
|
535
|
+
return EmailStruct.create(this)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async getPreviewStructure() {
|
|
539
|
+
const recipient = await SQL.select()
|
|
540
|
+
.from(SQL.table(EmailRecipient.table))
|
|
541
|
+
.where(SQL.column('emailId'), this.id)
|
|
542
|
+
.first(false);
|
|
543
|
+
|
|
544
|
+
const recipientRow = recipient ? EmailRecipient.fromRow(recipient[EmailRecipient.table]) : null;
|
|
545
|
+
|
|
546
|
+
const smartVariables = recipientRow ? EditorSmartVariable.forRecipient(recipientRow) : []
|
|
547
|
+
const smartButtons = recipientRow ? EditorSmartButton.forRecipient(recipientRow) : []
|
|
548
|
+
|
|
549
|
+
return EmailPreview.create({
|
|
550
|
+
...this,
|
|
551
|
+
exampleRecipient: recipientRow ? (recipientRow.getStructure()) : null,
|
|
552
|
+
smartVariables,
|
|
553
|
+
smartButtons
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
}
|