@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.
Files changed (61) hide show
  1. package/package.json +12 -12
  2. package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
  3. package/src/audit-logs/init.ts +2 -0
  4. package/src/crons/index.ts +2 -0
  5. package/src/crons/invoices.ts +166 -0
  6. package/src/crons/mollie-chargebacks.ts +87 -0
  7. package/src/crons.ts +47 -10
  8. package/src/email-recipient-loaders/payments.ts +84 -41
  9. package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
  11. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
  12. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
  13. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
  14. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
  15. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
  16. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
  17. package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
  18. package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
  19. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
  20. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
  21. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
  22. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
  23. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
  24. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
  25. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
  26. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
  27. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
  28. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
  29. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
  30. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
  31. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
  32. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
  33. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
  34. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
  35. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
  36. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
  37. package/src/helpers/AdminPermissionChecker.ts +11 -3
  38. package/src/helpers/AuthenticatedStructures.ts +94 -6
  39. package/src/helpers/FinancialSupportHelper.ts +21 -0
  40. package/src/helpers/RecordAnswerHelper.test.ts +746 -0
  41. package/src/helpers/RecordAnswerHelper.ts +116 -0
  42. package/src/helpers/StripeHelper.ts +2 -3
  43. package/src/helpers/ViesHelper.ts +7 -3
  44. package/src/seeds/1750090030-records-configuration.ts +68 -3
  45. package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
  46. package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
  47. package/src/services/BalanceItemService.ts +12 -16
  48. package/src/services/InvoiceService.ts +372 -72
  49. package/src/services/MollieService.ts +537 -0
  50. package/src/services/PaymentMandateService.ts +214 -0
  51. package/src/services/PaymentService.ts +578 -222
  52. package/src/services/PlatformMembershipService.ts +1 -1
  53. package/src/services/RegistrationService.ts +66 -5
  54. package/src/services/STPackageService.ts +0 -7
  55. package/src/services/data/invoice.hbs.html +686 -0
  56. package/src/sql-filters/groups.ts +11 -1
  57. package/src/sql-filters/payments.ts +5 -0
  58. package/src/sql-filters/registration-invitations.ts +90 -0
  59. package/src/sql-sorters/registration-invitations.ts +36 -0
  60. package/vitest.config.js +1 -0
  61. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
@@ -0,0 +1,214 @@
1
+ import type { User } from '@stamhoofd/models';
2
+ import { Organization } from '@stamhoofd/models';
3
+ import { Platform } from '@stamhoofd/models';
4
+ import { PaymentMandateStatus } from '@stamhoofd/structures/PaymentMandate.js';
5
+ import type {PaymentMandate} from '@stamhoofd/structures/PaymentMandate.js';
6
+ import { MollieService } from './MollieService.js';
7
+ import { SimpleError } from '@simonbackx/simple-errors';
8
+ import { Context } from '../helpers/Context.js';
9
+ import { PaymentProvider } from '@stamhoofd/structures';
10
+
11
+ export class PaymentMandateService {
12
+ static async getMandates({ sellingOrganization, user, payingOrganization }: {
13
+ sellingOrganization: Organization,
14
+
15
+ /**
16
+ * Mandates for B2B payments
17
+ */
18
+ payingOrganization: Organization | null,
19
+
20
+ /**
21
+ * Not yet supported, but in the future you'll be able to save mandates for a certain user.
22
+ * Only for B2C payments
23
+ */
24
+ user: User | null,
25
+ }): Promise<PaymentMandate[]> {
26
+ if (!payingOrganization) {
27
+ // Not yet supportedd
28
+ return [];
29
+ }
30
+
31
+ const mollieService = await MollieService.create({sellingOrganization});
32
+ if (!mollieService) {
33
+ return [];
34
+ }
35
+
36
+ if (sellingOrganization.id !== (await Platform.getShared()).membershipOrganizationId) {
37
+ // Not yet supported
38
+ return [];
39
+ }
40
+
41
+ return await mollieService.getMandates({payingOrganization, user});
42
+ }
43
+
44
+ static async deleteMandate({ mandateId, sellingOrganization, user, payingOrganization }: {
45
+ mandateId: string,
46
+
47
+ sellingOrganization: Organization,
48
+
49
+ /**
50
+ * Mandates for B2B payments
51
+ */
52
+ payingOrganization: Organization | null,
53
+
54
+ /**
55
+ * Not yet supported, but in the future you'll be able to save mandates for a certain user.
56
+ * Only for B2C payments
57
+ */
58
+ user: User | null,
59
+ }) {
60
+ const mandates = await PaymentMandateService.getMandates({
61
+ sellingOrganization,
62
+ user,
63
+ payingOrganization
64
+ });
65
+ const {grouped} = this.groupByMandate(mandates);
66
+ const match = mandates.find(m => m.id === mandateId);
67
+
68
+ if (!match) {
69
+ // Not allowed or does not exist
70
+ throw new SimpleError({
71
+ code: 'not_allowed',
72
+ message: 'Mandate not found',
73
+ human: $t('%1Q1')
74
+ })
75
+ }
76
+
77
+ if (match.isDefault) {
78
+ if (!Context.optionalAuth?.hasPlatformFullAccess()) {
79
+ throw new SimpleError({
80
+ code: 'not_allowed',
81
+ message: 'You cannot delete the default mandate',
82
+ human: $t('%1Qu')
83
+ })
84
+ }
85
+ }
86
+
87
+ if (grouped.size <= 1) {
88
+ if (!Context.optionalAuth?.hasPlatformFullAccess()) {
89
+ throw new SimpleError({
90
+ code: 'not_allowed',
91
+ message: 'You cannot delete the last mandate',
92
+ human: $t('%1Q7')
93
+ })
94
+ }
95
+ }
96
+
97
+ // Delete all that have the same card linked to it
98
+ const deleteId = match.identifier;
99
+ for (const mandate of mandates) {
100
+ if (mandate.identifier === deleteId) {
101
+ // delete (todo)
102
+ if (mandate.provider === PaymentProvider.Mollie) {
103
+ const mollieService = await MollieService.create({sellingOrganization});
104
+ if (!mollieService) {
105
+ return [];
106
+ }
107
+
108
+ try {
109
+ await mollieService.deleteMandate({
110
+ mandateId: mandate.id,
111
+ payingOrganization,
112
+ user
113
+ })
114
+ } catch (e) {
115
+ console.error('Failed to delete Mollie mandate', mandateId, e)
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ static async setDefaultMandate({ mandateId, sellingOrganization, payingUserId, payingOrganizationId }: {
123
+ mandateId: string,
124
+
125
+ sellingOrganization: Organization,
126
+
127
+ /**
128
+ * Mandates for B2B payments
129
+ */
130
+ payingOrganizationId: Organization | string | null,
131
+
132
+ /**
133
+ * Not yet supported, but in the future you'll be able to save mandates for a certain user.
134
+ * Only for B2C payments
135
+ */
136
+ payingUserId: string | null,
137
+ }) {
138
+ try {
139
+ if (!payingOrganizationId) {
140
+ // Not supported yet
141
+ return;
142
+ }
143
+
144
+ if (sellingOrganization.id !== (await Platform.getShared()).membershipOrganizationId) {
145
+ // Not yet supported
146
+ return [];
147
+ }
148
+
149
+ // Set as default
150
+ if (payingOrganizationId) {
151
+ const payingOrganization = typeof payingOrganizationId === 'string' ? await Organization.getByID(payingOrganizationId) : payingOrganizationId;
152
+ if (payingOrganization) {
153
+ console.log('Saving ' + mandateId + ' as default mandate for organization ' + payingOrganization.id + ' ' + payingOrganization.name)
154
+ payingOrganization.serverMeta.mollieMandateId = mandateId
155
+ await payingOrganization.save()
156
+ }
157
+ }
158
+ } catch (e) {
159
+ console.error('Failed to save default mandate for mandate ' + mandateId + ' in organization ' + (typeof payingOrganizationId === 'string' ? payingOrganizationId : payingOrganizationId?.id), {cause: e})
160
+
161
+ throw new SimpleError({
162
+ code: 'failed',
163
+ message: 'Failed to update default mandate',
164
+ human: $t('%1S4')
165
+ })
166
+ }
167
+ }
168
+
169
+ /**
170
+ * The same mandates should be bundled and only the most recent active one should be returned
171
+ */
172
+ static groupByMandate(base: PaymentMandate[]) {
173
+ base.sort((a, b) => {
174
+ if (a.status === PaymentMandateStatus.Valid && b.status !== PaymentMandateStatus.Valid ) return -1;
175
+ if (a.status !== PaymentMandateStatus.Valid && b.status === PaymentMandateStatus.Valid ) return 1;
176
+ if (a.isDefault && !b.isDefault) return -1;
177
+ if (!a.isDefault && b.isDefault) return 1;
178
+ return b.createdAt.getTime() - a.createdAt.getTime();
179
+ });
180
+ const found = new Map<string, PaymentMandate[]>();
181
+
182
+ const cleaned: PaymentMandate[] = [];
183
+
184
+ for (const mandate of base) {
185
+ const existing = found.get(mandate.identifier || '');
186
+
187
+ if (existing) {
188
+ if (mandate.isDefault) {
189
+ // Make sure first is also marked as default
190
+ existing[0].isDefault = true;
191
+ }
192
+ existing.push(mandate)
193
+ continue; // Skip duplicates
194
+ }
195
+
196
+ found.set(mandate.identifier || '', [mandate]);
197
+ cleaned.push(mandate);
198
+ }
199
+
200
+ // Restort as isDefault might have changed
201
+ cleaned.sort((a, b) => {
202
+ if (a.status === PaymentMandateStatus.Valid && b.status !== PaymentMandateStatus.Valid ) return -1;
203
+ if (a.status !== PaymentMandateStatus.Valid && b.status === PaymentMandateStatus.Valid ) return 1;
204
+ if (a.isDefault && !b.isDefault) return -1;
205
+ if (!a.isDefault && b.isDefault) return 1;
206
+ return b.createdAt.getTime() - a.createdAt.getTime();
207
+ });
208
+
209
+ return {
210
+ mandates: cleaned.filter(b => b.status === PaymentMandateStatus.Valid || b.isDefault),
211
+ grouped: found
212
+ }
213
+ }
214
+ }