@stamhoofd/backend 2.120.5 → 2.121.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/package.json +12 -12
- package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
- package/src/audit-logs/init.ts +2 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/invoices.ts +166 -0
- package/src/crons/mollie-chargebacks.ts +87 -0
- package/src/crons.ts +47 -10
- package/src/email-recipient-loaders/payments.ts +84 -41
- package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
- package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
- package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
- package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
- package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
- package/src/helpers/AdminPermissionChecker.ts +11 -3
- package/src/helpers/AuthenticatedStructures.ts +94 -6
- package/src/helpers/FinancialSupportHelper.ts +21 -0
- package/src/helpers/RecordAnswerHelper.test.ts +746 -0
- package/src/helpers/RecordAnswerHelper.ts +116 -0
- package/src/helpers/StripeHelper.ts +2 -3
- package/src/helpers/ViesHelper.ts +7 -3
- package/src/seeds/1750090030-records-configuration.ts +68 -3
- package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
- package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
- package/src/services/BalanceItemService.ts +12 -16
- package/src/services/InvoiceService.ts +372 -72
- package/src/services/MollieService.ts +537 -0
- package/src/services/PaymentMandateService.ts +214 -0
- package/src/services/PaymentService.ts +578 -222
- package/src/services/PlatformMembershipService.ts +1 -1
- package/src/services/RegistrationService.ts +66 -5
- package/src/services/STPackageService.ts +0 -7
- package/src/services/data/invoice.hbs.html +686 -0
- package/src/sql-filters/groups.ts +11 -1
- package/src/sql-filters/payments.ts +5 -0
- package/src/sql-filters/registration-invitations.ts +90 -0
- package/src/sql-sorters/registration-invitations.ts +36 -0
- package/vitest.config.js +1 -0
- package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { AutoEncoderPatchType, Encodeable, Identifiable, Patchable, PatchableArray } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { isPatchableArray } from '@simonbackx/simple-encoding';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import type { RecordCategory, RecordSettings, RecordType } from '@stamhoofd/structures';
|
|
5
|
+
import { RecordAnswerDecoder } from '@stamhoofd/structures';
|
|
6
|
+
|
|
7
|
+
export class RecordAnswerHelper {
|
|
8
|
+
static throwIfPatchOrPutIsInvalid(original: RecordCategory[], patchOrPut: RecordCategory[] | PatchableArray<string, RecordCategory, AutoEncoderPatchType<RecordCategory>>) {
|
|
9
|
+
if (original.length === 0) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const updatedRecordSettings = this.getAllRecords(patchOrPut)
|
|
14
|
+
// only check if the types changed
|
|
15
|
+
.filter(p => p.type !== undefined || p.fileType !== undefined);
|
|
16
|
+
|
|
17
|
+
if (updatedRecordSettings.length === 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const updatRecordSettingsMap = new Map<string, RecordSettings | AutoEncoderPatchType<RecordSettings>>(updatedRecordSettings.map(p => [p.id, p]));
|
|
22
|
+
|
|
23
|
+
for (const category of original) {
|
|
24
|
+
for (const originalRecord of category.getAllRecords()) {
|
|
25
|
+
const updatedSettings = updatRecordSettingsMap.get(originalRecord.id);
|
|
26
|
+
if (updatedSettings) {
|
|
27
|
+
this.throwIfInvalidRecordSettingsUpdate(originalRecord, updatedSettings);
|
|
28
|
+
|
|
29
|
+
// remove from map
|
|
30
|
+
updatRecordSettingsMap.delete(originalRecord.id);
|
|
31
|
+
|
|
32
|
+
// stop looping if all updated records are checked
|
|
33
|
+
if (updatRecordSettingsMap.size === 0) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static throwIfPatchOrPutsAreInvalid<
|
|
42
|
+
Key extends string & keyof Put,
|
|
43
|
+
Id extends string | number,
|
|
44
|
+
Put extends (Identifiable<Id> & Encodeable & Patchable<Patch> & Record<Key, RecordCategory[]>),
|
|
45
|
+
Patch extends (Identifiable<Id> & Encodeable) | Put
|
|
46
|
+
>(originals: Put[], patchOrPuts: Put[] | Patch[] | PatchableArray<Id, Put, Patch>, key: string) {
|
|
47
|
+
if (isPatchableArray(patchOrPuts)) {
|
|
48
|
+
const puts = patchOrPuts.getPuts().map(p => p.put);
|
|
49
|
+
this.throwIfPatchOrPutsAreInvalid<Key, Id, Put, Patch>(originals, puts, key);
|
|
50
|
+
const patches = patchOrPuts.getPatches();
|
|
51
|
+
this.throwIfPatchOrPutsAreInvalid<Key, Id, Put, Patch>(originals, patches, key);
|
|
52
|
+
} else {
|
|
53
|
+
for (const patchOrPut of patchOrPuts) {
|
|
54
|
+
const original = originals.find(o => o.id === patchOrPut.id);
|
|
55
|
+
if (original) {
|
|
56
|
+
const originalRecordCategories: RecordCategory[] = original[key];
|
|
57
|
+
const newRecordCategories: RecordCategory[] | PatchableArray<string, RecordCategory, AutoEncoderPatchType<RecordCategory>> = patchOrPut[key];
|
|
58
|
+
if (originalRecordCategories && newRecordCategories) {
|
|
59
|
+
this.throwIfPatchOrPutIsInvalid(originalRecordCategories, newRecordCategories);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static throwIfInvalidRecordSettingsUpdate(original: RecordSettings, updated: RecordSettings | AutoEncoderPatchType<RecordSettings>) {
|
|
67
|
+
if (updated.type !== undefined
|
|
68
|
+
&& original.type !== updated.type
|
|
69
|
+
// changing between types with the same class should be allowed
|
|
70
|
+
&& !this.haveTypesSameClass(original.type, updated.type)) {
|
|
71
|
+
throw new SimpleError({
|
|
72
|
+
code: 'invalid_record_type',
|
|
73
|
+
message: `Cannot change record type from ${original.type} to ${updated.type}`,
|
|
74
|
+
human: $t(`%1Qn`, {name: original.name}),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (updated.fileType !== undefined && ((original.fileType ?? null) !== (updated.fileType ?? null))) {
|
|
79
|
+
throw new SimpleError({
|
|
80
|
+
code: 'invalid_record_type',
|
|
81
|
+
message: `Cannot change record file type from ${original.fileType} to ${updated.fileType}`,
|
|
82
|
+
human: $t(`%1SP`, {name: original.name}),
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static haveTypesSameClass(a: RecordType, b: RecordType) {
|
|
88
|
+
return RecordAnswerDecoder.getClassForType(a) === RecordAnswerDecoder.getClassForType(b);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static getAllRecords(patchOrPut: RecordCategory[] | PatchableArray<string, RecordCategory, AutoEncoderPatchType<RecordCategory>>): (RecordSettings | AutoEncoderPatchType<RecordSettings>)[] {
|
|
92
|
+
if (isPatchableArray(patchOrPut)) {
|
|
93
|
+
const patches = patchOrPut.getPatches().flatMap(p => this.getAllRecordsFromRecordCategoryPatch(p));
|
|
94
|
+
const puts = patchOrPut.getPuts().flatMap(p => p.put.getAllRecords());
|
|
95
|
+
return [...patches, ...puts];
|
|
96
|
+
}
|
|
97
|
+
return patchOrPut.flatMap(c => c.getAllRecords());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static getAllRecordsFromRecordCategoryPatch(category: AutoEncoderPatchType<RecordCategory>) {
|
|
101
|
+
const results: (RecordSettings | AutoEncoderPatchType<RecordSettings>)[] = [
|
|
102
|
+
...category.records.getPatches(),
|
|
103
|
+
...category.records.getPuts().map(p => p.put)
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
if (category.childCategories) {
|
|
107
|
+
const childCategoryPatches = category.childCategories.getPatches().flatMap(p => this.getAllRecordsFromRecordCategoryPatch(p));
|
|
108
|
+
results.push(...childCategoryPatches);
|
|
109
|
+
|
|
110
|
+
const childCategoryPuts = category.childCategories.getPuts().flatMap(p => p.put.getAllRecords());
|
|
111
|
+
results.push(...childCategoryPuts);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -207,7 +207,7 @@ export class StripeHelper {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
static async createPayment(
|
|
210
|
-
{ payment, stripeAccount, redirectUrl, cancelUrl, customer, statementDescriptor, i18n, metadata, organization
|
|
210
|
+
{ payment, stripeAccount, redirectUrl, cancelUrl, customer, statementDescriptor, i18n, metadata, organization }: {
|
|
211
211
|
payment: Payment;
|
|
212
212
|
stripeAccount: StripeAccount | null;
|
|
213
213
|
redirectUrl: string;
|
|
@@ -218,9 +218,8 @@ export class StripeHelper {
|
|
|
218
218
|
};
|
|
219
219
|
statementDescriptor: string;
|
|
220
220
|
i18n: I18n;
|
|
221
|
-
metadata: { [key: string]: string };
|
|
221
|
+
metadata: { [key: string]: string | undefined};
|
|
222
222
|
organization: Organization;
|
|
223
|
-
lineItems: (BalanceItemPayment & { balanceItem: BalanceItem })[];
|
|
224
223
|
},
|
|
225
224
|
): Promise<{ paymentUrl: string }> {
|
|
226
225
|
if (!stripeAccount) {
|
|
@@ -128,13 +128,17 @@ export class ViesHelperStatic {
|
|
|
128
128
|
if (!result.isValid) {
|
|
129
129
|
throw new SimpleError({
|
|
130
130
|
code: 'invalid_field',
|
|
131
|
-
message: '
|
|
131
|
+
message: $t('%1To', {'number': vatNumber}),
|
|
132
132
|
field: 'VATNumber',
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const formatted = result.value ?? vatNumber;
|
|
137
137
|
|
|
138
|
+
if (STAMHOOFD.environment === 'development' && formatted === 'NL301828519B01') {
|
|
139
|
+
return formatted
|
|
140
|
+
}
|
|
141
|
+
|
|
138
142
|
try {
|
|
139
143
|
const cleaned = formatted.substring(2).replace(/(?:\.-\s)+/g, '');
|
|
140
144
|
const response = await this.request('POST', 'https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number', {
|
|
@@ -150,7 +154,7 @@ export class ViesHelperStatic {
|
|
|
150
154
|
if (!response.valid) {
|
|
151
155
|
throw new SimpleError({
|
|
152
156
|
code: 'invalid_field',
|
|
153
|
-
message: '
|
|
157
|
+
message: $t('%1TG', {'vat-number': formatted}),
|
|
154
158
|
field: 'VATNumber',
|
|
155
159
|
});
|
|
156
160
|
}
|
|
@@ -164,7 +168,7 @@ export class ViesHelperStatic {
|
|
|
164
168
|
|
|
165
169
|
throw new SimpleError({
|
|
166
170
|
code: 'service_unavailable',
|
|
167
|
-
message: '
|
|
171
|
+
message: $t('%1Sf'),
|
|
168
172
|
field: 'VATNumber',
|
|
169
173
|
});
|
|
170
174
|
}
|
|
@@ -1,11 +1,76 @@
|
|
|
1
|
-
import { Migration } from '@simonbackx/simple-database';
|
|
1
|
+
import { column, Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { AutoEncoder, field } from '@simonbackx/simple-encoding';
|
|
2
3
|
import { Organization, Webshop } from '@stamhoofd/models';
|
|
4
|
+
import { QueryableModel, SQL } from '@stamhoofd/sql';
|
|
5
|
+
import { DataPermissionsSettings, FinancialSupportSettings, Version } from '@stamhoofd/structures';
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
|
|
8
|
+
class OldOrganizationRecordsConfiguration extends AutoEncoder {
|
|
9
|
+
/**
|
|
10
|
+
* If the organizations provides support for families in financial difficulties
|
|
11
|
+
*/
|
|
12
|
+
@field({ decoder: FinancialSupportSettings, nullable: true, version: 117 })
|
|
13
|
+
financialSupport: FinancialSupportSettings | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ask permissions to collect data
|
|
17
|
+
*/
|
|
18
|
+
@field({ decoder: DataPermissionsSettings, nullable: true, version: 117 })
|
|
19
|
+
dataPermission: DataPermissionsSettings | null = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class OldOrganizationMetaData extends AutoEncoder {
|
|
23
|
+
@field({
|
|
24
|
+
decoder: OldOrganizationRecordsConfiguration,
|
|
25
|
+
version: 53,
|
|
26
|
+
defaultValue: () => OldOrganizationRecordsConfiguration.create({})
|
|
27
|
+
})
|
|
28
|
+
recordsConfiguration: OldOrganizationRecordsConfiguration = OldOrganizationRecordsConfiguration.create({});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// get data like it was in V1
|
|
32
|
+
export class OldOrganization extends QueryableModel {
|
|
33
|
+
static table = 'organizations';
|
|
34
|
+
|
|
35
|
+
@column({
|
|
36
|
+
primary: true, type: 'string', beforeSave(value) {
|
|
37
|
+
return value ?? uuidv4();
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
id!: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Public meta data
|
|
44
|
+
*/
|
|
45
|
+
@column({ type: 'json', decoder: OldOrganizationMetaData })
|
|
46
|
+
meta: OldOrganizationMetaData = OldOrganizationMetaData.create({});
|
|
47
|
+
}
|
|
3
48
|
|
|
4
49
|
export async function startRecordsConfigurationMigration() {
|
|
5
|
-
|
|
6
|
-
|
|
50
|
+
|
|
51
|
+
// migrate recordsConfiguration of organizations
|
|
52
|
+
for await (const oldOrganization of OldOrganization.select()
|
|
53
|
+
// prevent migrating same organizations twice if something goes wrong
|
|
54
|
+
.where(SQL.jsonValue(SQL.column('meta'), '$.version'), '<', Version)
|
|
55
|
+
.all()) {
|
|
56
|
+
const oldFinancialSupport = oldOrganization.meta.recordsConfiguration.financialSupport;
|
|
57
|
+
const oldDataPermission = oldOrganization.meta.recordsConfiguration.dataPermission;
|
|
58
|
+
|
|
59
|
+
if (oldFinancialSupport || oldDataPermission) {
|
|
60
|
+
const newOrganization = await Organization.getByID(oldOrganization.id);
|
|
61
|
+
|
|
62
|
+
if (!newOrganization) {
|
|
63
|
+
throw new Error('Organization with id ' + oldOrganization.id + ' not found');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
newOrganization.meta.financialSupport = oldFinancialSupport;
|
|
67
|
+
newOrganization.meta.dataPermission = oldDataPermission;
|
|
68
|
+
|
|
69
|
+
await newOrganization.save();
|
|
70
|
+
}
|
|
7
71
|
}
|
|
8
72
|
|
|
73
|
+
// migrate recordsConfiguration of webshops
|
|
9
74
|
for await (const webshop of Webshop.select().all()) {
|
|
10
75
|
await webshop.save();
|
|
11
76
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
-
import { Group, Organization, OrganizationRegistrationPeriod, Registration, RegistrationPeriod } from '@stamhoofd/models';
|
|
3
|
-
import type { CycleInformation} from '@stamhoofd/structures';
|
|
2
|
+
import { Group, Organization, OrganizationRegistrationPeriod, Registration, RegistrationInvitation, RegistrationPeriod } from '@stamhoofd/models';
|
|
3
|
+
import type { CycleInformation } from '@stamhoofd/structures';
|
|
4
4
|
import { GroupCategory, GroupCategorySettings, GroupPrivateSettings, GroupSettings, GroupStatus, GroupType, RegistrationPeriodSettings, TranslatedString } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
6
|
export default new Migration(async () => {
|
|
@@ -419,6 +419,8 @@ async function migrateRegistrations({ organization, period, originalGroup, newGr
|
|
|
419
419
|
.fetch();
|
|
420
420
|
|
|
421
421
|
for (const registration of registrations) {
|
|
422
|
+
let invitation: RegistrationInvitation | null = null;
|
|
423
|
+
|
|
422
424
|
if (registration.waitingList) {
|
|
423
425
|
const waitingList = await getOrCreateWaitingList();
|
|
424
426
|
if (newGroup.waitingListId !== waitingList.id) {
|
|
@@ -430,6 +432,18 @@ async function migrateRegistrations({ organization, period, originalGroup, newGr
|
|
|
430
432
|
}
|
|
431
433
|
|
|
432
434
|
registration.groupId = waitingList.id;
|
|
435
|
+
|
|
436
|
+
if (registration.canRegister) {
|
|
437
|
+
// we should create an invitation
|
|
438
|
+
invitation = new RegistrationInvitation();
|
|
439
|
+
invitation.groupId = newGroup.id;
|
|
440
|
+
invitation.memberId = registration.memberId;
|
|
441
|
+
invitation.organizationId = organization.id;
|
|
442
|
+
invitation.createdAt = registration.createdAt;
|
|
443
|
+
|
|
444
|
+
// deprecated -> set to false
|
|
445
|
+
registration.canRegister = false;
|
|
446
|
+
}
|
|
433
447
|
}
|
|
434
448
|
else {
|
|
435
449
|
registration.groupId = newGroup.id;
|
|
@@ -440,6 +454,16 @@ async function migrateRegistrations({ organization, period, originalGroup, newGr
|
|
|
440
454
|
|
|
441
455
|
if (!dryRun) {
|
|
442
456
|
await registration.save();
|
|
457
|
+
if (invitation) {
|
|
458
|
+
try {
|
|
459
|
+
await invitation.save();
|
|
460
|
+
} catch (e) {
|
|
461
|
+
// do not throw if duplicate
|
|
462
|
+
if (e.code !== 'ER_DUP_ENTRY') {
|
|
463
|
+
throw e;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
443
467
|
}
|
|
444
468
|
}
|
|
445
469
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
INSERT INTO `email_templates` (`id`, `subject`, `groupId`, `webshopId`, `organizationId`, `type`, `text`, `html`, `json`, `updatedAt`, `createdAt`) VALUES
|
|
2
|
+
('5d97e1d3-3f5c-415b-813a-0669b724f716', 'Fout bij het automatisch aanmaken van facturen', NULL, NULL, NULL, 'InvoiceGenerationErrors', '{{greeting}}\n\nBij het automatisch aanmaken van facturen in naam van {{organizationName}} ging er iets mis. Hieronder kan je een lijst terugvinden van alle foutmeldingen. Kijk ze na en maak indien nodig de juiste correcties in het systeem.\n\n{{errors}}', '<!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>Fout bij het automatisch aanmaken van facturen</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 text-box-trim: none;\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><p style=\"margin: 0; padding: 0; line-height: 1.4;\">Bij het automatisch aanmaken van facturen in naam van {{organizationName}} ging er iets mis. Hieronder kan je een lijst terugvinden van alle foutmeldingen. Kijk ze na en maak indien nodig de juiste correcties in het systeem.</p><p style=\"margin: 0; padding: 0; line-height: 1.4;\"><br></p><p style=\"margin: 0; padding: 0; line-height: 1.4;\">{{errors}}</p>\n</body>\n\n</html>', '{\"value\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"greeting\"}}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"text\": \"Bij het automatisch aanmaken van facturen in naam van \", \"type\": \"text\"}, {\"type\": \"smartVariable\", \"attrs\": {\"id\": \"organizationName\"}}, {\"text\": \" ging er iets mis. Hieronder kan je een lijst terugvinden van alle foutmeldingen. Kijk ze na en maak indien nodig de juiste correcties in het systeem.\", \"type\": \"text\"}]}, {\"type\": \"paragraph\"}, {\"type\": \"paragraph\", \"content\": [{\"type\": \"smartVariable\", \"attrs\": {\"id\": \"errors\"}}]}]}, \"version\": 397}', '2026-05-18 14:52:07', '2026-05-18 14:52:07')
|
|
3
|
+
ON DUPLICATE KEY UPDATE id=id;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Model } from '@simonbackx/simple-database';
|
|
2
|
-
import type { Organization, Payment} from '@stamhoofd/models';
|
|
2
|
+
import type { Organization, Payment } from '@stamhoofd/models';
|
|
3
3
|
import { BalanceItem, CachedBalance, Document, MemberUser, Order, Webshop } from '@stamhoofd/models';
|
|
4
4
|
import { AuditLogSource, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
5
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
5
6
|
import { GroupedThrottledQueue } from '../helpers/GroupedThrottledQueue.js';
|
|
6
7
|
import { ThrottledQueue } from '../helpers/ThrottledQueue.js';
|
|
7
8
|
import { AuditLogService } from './AuditLogService.js';
|
|
@@ -108,10 +109,19 @@ export const BalanceItemService = {
|
|
|
108
109
|
*/
|
|
109
110
|
async updatePaidAndPending(items: BalanceItem[]) {
|
|
110
111
|
console.log('updatePaidAndPending for', items.length, 'items');
|
|
111
|
-
await BalanceItem.updatePricePaid(items.map(i => i.id));
|
|
112
|
+
await BalanceItem.updatePricePaid(Formatter.uniqueArray(items.map(i => i.id)));
|
|
112
113
|
await this.scheduleUpdates(items);
|
|
113
114
|
},
|
|
114
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Call this when a payment or payment balance items have changed.
|
|
118
|
+
* It will also call updateOutstanding automatically, so no need to call that separately again
|
|
119
|
+
*/
|
|
120
|
+
async updateInvoiced(items: (BalanceItem | string)[]) {
|
|
121
|
+
console.log('updateInvoiced for', items.length, 'items');
|
|
122
|
+
await BalanceItem.updateInvoiced(Formatter.uniqueArray(items.map(i => typeof i === 'string' ? i : i.id)));
|
|
123
|
+
},
|
|
124
|
+
|
|
115
125
|
/**
|
|
116
126
|
* In some situations we need immediate updates
|
|
117
127
|
*/
|
|
@@ -275,13 +285,6 @@ export const BalanceItemService = {
|
|
|
275
285
|
await order.undoPaid(payment, organization);
|
|
276
286
|
}
|
|
277
287
|
}
|
|
278
|
-
|
|
279
|
-
// If a rounded payment was canceled, make sure the balance item is hidden again (will become visible again when marking paid)
|
|
280
|
-
if (this.type === BalanceItemType.Rounding && balanceItem.status !== BalanceItemStatus.Hidden) {
|
|
281
|
-
// Mark undue
|
|
282
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
283
|
-
await balanceItem.save();
|
|
284
|
-
}
|
|
285
288
|
},
|
|
286
289
|
|
|
287
290
|
async markFailed(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
|
|
@@ -297,13 +300,6 @@ export const BalanceItemService = {
|
|
|
297
300
|
}
|
|
298
301
|
}
|
|
299
302
|
}
|
|
300
|
-
|
|
301
|
-
// If a rounded payment was canceled, make sure the balance item is hidden again (will become visible again when marking paid)
|
|
302
|
-
if (this.type === BalanceItemType.Rounding && balanceItem.status !== BalanceItemStatus.Hidden) {
|
|
303
|
-
// Mark undue
|
|
304
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
305
|
-
await balanceItem.save();
|
|
306
|
-
}
|
|
307
303
|
},
|
|
308
304
|
|
|
309
305
|
async undoFailed(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
|