@stamhoofd/backend 2.18.0 → 2.20.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.
@@ -1,7 +1,7 @@
1
1
  import { AutoEncoderPatchType, Decoder, patchObject } from "@simonbackx/simple-encoding";
2
2
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
3
  import { Organization, Platform, RegistrationPeriod } from "@stamhoofd/models";
4
- import { PlatformPremiseType, Platform as PlatformStruct } from "@stamhoofd/structures";
4
+ import { MemberResponsibility, PlatformConfig, PlatformPremiseType, Platform as PlatformStruct } from "@stamhoofd/structures";
5
5
 
6
6
  import { SimpleError } from "@simonbackx/simple-errors";
7
7
  import { Context } from "../../../helpers/Context";
@@ -12,8 +12,15 @@ type Query = undefined;
12
12
  type Body = AutoEncoderPatchType<PlatformStruct>;
13
13
  type ResponseBody = PlatformStruct;
14
14
 
15
- export class PatchPlatformEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
- bodyDecoder = PlatformStruct.patchType() as Decoder<AutoEncoderPatchType<PlatformStruct>>
15
+ export class PatchPlatformEndpoint extends Endpoint<
16
+ Params,
17
+ Query,
18
+ Body,
19
+ ResponseBody
20
+ > {
21
+ bodyDecoder = PlatformStruct.patchType() as Decoder<
22
+ AutoEncoderPatchType<PlatformStruct>
23
+ >;
17
24
 
18
25
  protected doesMatch(request: Request): [true, Params] | [false] {
19
26
  if (request.method != "PATCH") {
@@ -29,65 +36,102 @@ export class PatchPlatformEndpoint extends Endpoint<Params, Query, Body, Respons
29
36
  }
30
37
 
31
38
  async handle(request: DecodedRequest<Params, Query, Body>) {
32
- await Context.authenticate()
39
+ await Context.authenticate();
33
40
 
34
41
  // Fast throw first (more in depth checking for patches later)
35
42
  if (!Context.auth.hasPlatformFullAccess()) {
36
- throw Context.auth.error()
43
+ throw Context.auth.error();
37
44
  }
38
45
 
39
- const platform = await Platform.getShared()
46
+ const platform = await Platform.getShared();
40
47
 
41
48
  if (request.body.privateConfig) {
42
49
  // Did we patch roles?
43
50
  if (request.body.privateConfig.roles) {
44
51
  if (!Context.auth.canManagePlatformAdmins()) {
45
- throw Context.auth.error()
52
+ throw Context.auth.error();
46
53
  }
47
54
 
48
55
  // Update roles
49
- platform.privateConfig.roles = patchObject(platform.privateConfig.roles, request.body.privateConfig.roles)
56
+ platform.privateConfig.roles = patchObject(
57
+ platform.privateConfig.roles,
58
+ request.body.privateConfig.roles
59
+ );
50
60
  }
51
61
 
52
62
  if (request.body.privateConfig.emails) {
53
63
  if (!Context.auth.hasPlatformFullAccess()) {
54
- throw Context.auth.error()
64
+ throw Context.auth.error();
55
65
  }
56
66
 
57
67
  // Update roles
58
- platform.privateConfig.emails = patchObject(platform.privateConfig.emails, request.body.privateConfig.emails)
68
+ platform.privateConfig.emails = patchObject(
69
+ platform.privateConfig.emails,
70
+ request.body.privateConfig.emails
71
+ );
59
72
  }
60
73
  }
61
74
 
75
+ let shouldUpdateSetupSteps = false;
76
+
62
77
  if (request.body.config) {
63
78
  if (!Context.auth.hasPlatformFullAccess()) {
64
- throw Context.auth.error()
79
+ throw Context.auth.error();
65
80
  }
66
81
 
82
+ const newConfig = request.body.config;
83
+
67
84
  // Update config
68
- if(request.body.config.premiseTypes) {
69
- const oldConfig = platform.config.clone();
70
- platform.config = patchObject(platform.config, request.body.config);
71
- const newPremiseTypes = platform.config.premiseTypes;
72
-
73
- // update setup step premise types
74
- if(this.shouldUpdateSetupStepPremise(newPremiseTypes, oldConfig.premiseTypes)) {
75
- await SetupStepUpdater.updateSetupStepsForAllOrganizationsInCurrentPeriod({premiseTypes: newPremiseTypes});
85
+ if (newConfig) {
86
+ if (newConfig.premiseTypes || newConfig.responsibilities) {
87
+ const oldConfig = platform.config.clone();
88
+ platform.config = patchObject(platform.config, newConfig);
89
+ const currentConfig = platform.config;
90
+
91
+ shouldUpdateSetupSteps = this.shouldUpdateSetupSteps(
92
+ currentConfig,
93
+ newConfig,
94
+ oldConfig
95
+ );
96
+ } else {
97
+ platform.config = patchObject(platform.config, newConfig);
76
98
  }
77
- } else {
78
- platform.config = patchObject(platform.config, request.body.config)
79
99
  }
80
100
  }
81
101
 
82
- if (request.body.period && request.body.period.id !== platform.periodId) {
83
- const period = await RegistrationPeriod.getByID(request.body.period.id)
102
+ if (
103
+ request.body.period &&
104
+ request.body.period.id !== platform.periodId
105
+ ) {
106
+ const period = await RegistrationPeriod.getByID(
107
+ request.body.period.id
108
+ );
84
109
  if (!period || period.organizationId) {
85
110
  throw new SimpleError({
86
111
  code: "invalid_period",
87
- message: "Invalid period"
88
- })
112
+ message: "Invalid period",
113
+ });
114
+ }
115
+ platform.periodId = period.id;
116
+ }
117
+
118
+ if (request.body.membershipOrganizationId !== undefined) {
119
+ if (!Context.auth.hasPlatformFullAccess()) {
120
+ throw Context.auth.error()
121
+ }
122
+
123
+ if (request.body.membershipOrganizationId) {
124
+ const organization = await Organization.getByID(request.body.membershipOrganizationId)
125
+ if (!organization) {
126
+ throw new SimpleError({
127
+ code: "invalid_organization",
128
+ message: "Invalid organization"
129
+ })
130
+ }
131
+ platform.membershipOrganizationId = organization.id
132
+ } else {
133
+ platform.membershipOrganizationId = null
89
134
  }
90
- platform.periodId = period.id
91
135
  }
92
136
 
93
137
  if (request.body.membershipOrganizationId !== undefined) {
@@ -109,35 +153,126 @@ export class PatchPlatformEndpoint extends Endpoint<Params, Query, Body, Respons
109
153
  }
110
154
  }
111
155
 
112
- await platform.save()
156
+ await platform.save();
157
+
158
+ if(shouldUpdateSetupSteps) {
159
+ SetupStepUpdater.updateSetupStepsForAllOrganizationsInCurrentPeriod().catch(console.error);
160
+ }
161
+
113
162
  return new Response(await Platform.getSharedPrivateStruct());
114
163
  }
115
164
 
116
- private shouldUpdateSetupStepPremise(newPremiseTypes: PlatformPremiseType[], oldPremiseTypes: PlatformPremiseType[]) {
117
- for(const premiseType of newPremiseTypes) {
165
+ private shouldUpdateSetupSteps(
166
+ currentConfig: PlatformConfig,
167
+ newConfig: PlatformConfig | AutoEncoderPatchType<PlatformConfig>,
168
+ oldConfig: PlatformConfig
169
+ ): boolean {
170
+ let shouldUpdate = false;
171
+ const premiseTypes: PlatformPremiseType[] = currentConfig.premiseTypes;
172
+ const responsibilities: MemberResponsibility[] =
173
+ currentConfig.responsibilities;
174
+
175
+ if (
176
+ newConfig.premiseTypes &&
177
+ this.shouldUpdateSetupStepPremise(
178
+ premiseTypes,
179
+ oldConfig.premiseTypes
180
+ )
181
+ ) {
182
+ shouldUpdate = true;
183
+ }
184
+
185
+ if (
186
+ !shouldUpdate &&
187
+ newConfig.responsibilities &&
188
+ this.shouldUpdateSetupStepFunctions(
189
+ responsibilities,
190
+ oldConfig.responsibilities
191
+ )
192
+ ) {
193
+ shouldUpdate = true;
194
+ }
195
+
196
+ return shouldUpdate;
197
+ }
198
+
199
+ private shouldUpdateSetupStepPremise(
200
+ newPremiseTypes: PlatformPremiseType[],
201
+ oldPremiseTypes: PlatformPremiseType[]
202
+ ) {
203
+ for (const premiseType of newPremiseTypes) {
118
204
  const id = premiseType.id;
119
- const oldVersion = oldPremiseTypes.find(x => x.id === id);
205
+ const oldVersion = oldPremiseTypes.find((x) => x.id === id);
120
206
 
121
207
  // if premise type is not new
122
- if(oldVersion) {
123
- if(oldVersion.min !== premiseType.min || oldVersion.max !== premiseType.max) {
208
+ if (oldVersion) {
209
+ if (
210
+ oldVersion.min !== premiseType.min ||
211
+ oldVersion.max !== premiseType.max
212
+ ) {
124
213
  return true;
125
214
  }
126
215
  continue;
127
216
  }
128
217
 
129
218
  // if premise type is new
130
- if(premiseType.min || premiseType.max) {
219
+ if (premiseType.min || premiseType.max) {
131
220
  return true;
132
221
  }
133
222
  }
134
223
 
135
- for(const oldPremiseType of oldPremiseTypes) {
224
+ for (const oldPremiseType of oldPremiseTypes) {
136
225
  const id = oldPremiseType.id;
137
226
 
138
227
  // if premise type is removed
139
- if(!newPremiseTypes.some(x => x.id === id)) {
140
- if(oldPremiseType.min || oldPremiseType.max) {
228
+ if (!newPremiseTypes.some((x) => x.id === id)) {
229
+ if (oldPremiseType.min || oldPremiseType.max) {
230
+ return true;
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ private shouldUpdateSetupStepFunctions(
237
+ newResponsibilities: MemberResponsibility[],
238
+ oldResponsibilities: MemberResponsibility[]
239
+ ) {
240
+ for (const responsibility of newResponsibilities) {
241
+ const id = responsibility.id;
242
+ const oldVersion = oldResponsibilities.find((x) => x.id === id);
243
+
244
+ // if responsibility is not new
245
+ if (oldVersion) {
246
+ // if restrictions changed
247
+ if (
248
+ oldVersion.minimumMembers !==
249
+ responsibility.minimumMembers ||
250
+ oldVersion.maximumMembers !== responsibility.maximumMembers
251
+ ) {
252
+ return true;
253
+ }
254
+ continue;
255
+ }
256
+
257
+ // if responsibility is new
258
+ if (
259
+ responsibility.minimumMembers ||
260
+ responsibility.maximumMembers
261
+ ) {
262
+ return true;
263
+ }
264
+ }
265
+
266
+ for (const oldResponsibility of oldResponsibilities) {
267
+ const id = oldResponsibility.id;
268
+
269
+ // if responsibility is removed
270
+ if (!newResponsibilities.some((x) => x.id === id)) {
271
+ // if responsibility had restrictions
272
+ if (
273
+ oldResponsibility.minimumMembers ||
274
+ oldResponsibility.maximumMembers
275
+ ) {
141
276
  return true;
142
277
  }
143
278
  }
@@ -1,5 +1,5 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
- import { OrganizationDetailedBillingStatus, OrganizationDetailedBillingStatusItem, PaymentMethod } from "@stamhoofd/structures";
2
+ import { OrganizationDetailedBillingStatus, OrganizationDetailedBillingStatusItem, PaymentMethod, PaymentStatus } from "@stamhoofd/structures";
3
3
 
4
4
  import { BalanceItem, Organization, Payment } from "@stamhoofd/models";
5
5
  import { SQL } from "@stamhoofd/sql";
@@ -41,8 +41,7 @@ export class GetDetailedBillingStatusEndpoint extends Endpoint<Params, Query, Bo
41
41
  const paymentModels = await Payment.select()
42
42
  .where('payingOrganizationId', organization.id)
43
43
  .andWhere(
44
- SQL.whereNot('paidAt', null)
45
- .or('method', [PaymentMethod.Transfer, PaymentMethod.DirectDebit, PaymentMethod.PointOfSale, PaymentMethod.Unknown])
44
+ SQL.whereNot('status', PaymentStatus.Failed)
46
45
  )
47
46
  .fetch()
48
47
 
@@ -3,6 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import jwt from 'jsonwebtoken';
4
4
 
5
5
  import { Context } from '../../../../helpers/Context';
6
+ import { SimpleError } from '@simonbackx/simple-errors';
6
7
 
7
8
  type Params = Record<string, never>;
8
9
  type Query = undefined;
@@ -39,7 +40,7 @@ export class CreateNoltTokenEndpoint extends Endpoint<Params, Query, Body, Respo
39
40
  if (!await Context.auth.hasSomeAccess(organization.id)) {
40
41
  throw Context.auth.error()
41
42
  }
42
-
43
+
43
44
  // Create token
44
45
  const payload = {
45
46
  // The ID that you use in your app for this user
@@ -2,19 +2,15 @@ import { createMollieClient } from '@mollie/api-client';
2
2
  import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
3
3
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
4
  import { SimpleError } from "@simonbackx/simple-errors";
5
- import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, STPendingInvoice } from '@stamhoofd/models';
5
+ import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment } from '@stamhoofd/models';
6
6
  import { QueueHandler } from '@stamhoofd/queues';
7
- import { PaymentGeneral, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, STInvoiceItem } from "@stamhoofd/structures";
7
+ import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
8
8
 
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
10
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
11
11
  import { Context } from '../../../helpers/Context';
12
12
  import { StripeHelper } from '../../../helpers/StripeHelper';
13
13
 
14
- function calculateFee(totalPrice: number, fixed: number, percentageTimes100: number) {
15
- return Math.round(fixed + Math.max(1, totalPrice * percentageTimes100 / 100 / 100)); // € 0,21 + 0,2%
16
- }
17
-
18
14
  type Params = {id: string};
19
15
  class Query extends AutoEncoder {
20
16
  @field({ decoder: BooleanDecoder, optional: true })
@@ -77,7 +73,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
77
73
  if (payment.status === status) {
78
74
  return;
79
75
  }
80
- const wasPaid = payment.paidAt !== null
76
+ // const wasPaid = payment.paidAt !== null
81
77
  if (status === PaymentStatus.Succeeded) {
82
78
  payment.status = PaymentStatus.Succeeded
83
79
  payment.paidAt = new Date()
@@ -97,31 +93,31 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
97
93
  await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
98
94
  })
99
95
 
100
- if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
101
- // Charge transaction fees
102
- let fee = 0
103
-
104
- if (payment.method === PaymentMethod.iDEAL) {
105
- fee = calculateFee(payment.price, 21, 20); // € 0,21 + 0,2%
106
- } else if (payment.method === PaymentMethod.Bancontact || payment.method === PaymentMethod.Payconiq) {
107
- fee = calculateFee(payment.price, 24, 20); // € 0,24 + 0,2%
108
- } else {
109
- fee = calculateFee(payment.price, 25, 150); // € 0,25 + 1,5%
110
- }
111
-
112
- const name = "Transactiekosten voor "+PaymentMethodHelper.getName(payment.method)
113
- const item = STInvoiceItem.create({
114
- name,
115
- description: "Via Buckaroo",
116
- amount: 1,
117
- unitPrice: fee,
118
- canUseCredits: false
119
- })
120
- console.log("Scheduling transaction fee charge for ", payment.id, item)
121
- await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
122
- await STPendingInvoice.addItems(organization, [item])
123
- });
124
- }
96
+ //if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
97
+ // // Charge transaction fees
98
+ // let fee = 0
99
+ //
100
+ // if (payment.method === PaymentMethod.iDEAL) {
101
+ // fee = calculateFee(payment.price, 21, 20); // € 0,21 + 0,2%
102
+ // } else if (payment.method === PaymentMethod.Bancontact || payment.method === PaymentMethod.Payconiq) {
103
+ // fee = calculateFee(payment.price, 24, 20); // € 0,24 + 0,2%
104
+ // } else {
105
+ // fee = calculateFee(payment.price, 25, 150); // € 0,25 + 1,5%
106
+ // }
107
+ //
108
+ // const name = "Transactiekosten voor "+PaymentMethodHelper.getName(payment.method)
109
+ // const item = STInvoiceItem.create({
110
+ // name,
111
+ // description: "Via Buckaroo",
112
+ // amount: 1,
113
+ // unitPrice: fee,
114
+ // canUseCredits: false
115
+ // })
116
+ // console.log("Scheduling transaction fee charge for ", payment.id, item)
117
+ // await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
118
+ // await STPendingInvoice.addItems(organization, [item])
119
+ // });
120
+ //}
125
121
  return;
126
122
  }
127
123
 
@@ -1,6 +1,6 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
2
2
  import { SimpleError } from "@simonbackx/simple-errors"
3
- import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
3
+ import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from "@stamhoofd/models"
4
4
  import { AccessRight, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
5
5
  import { Formatter } from "@stamhoofd/utility"
6
6
 
@@ -75,6 +75,11 @@ export class AdminPermissionChecker {
75
75
  return result;
76
76
  }
77
77
 
78
+ async getOrganizationCurrentPeriod(id: string|Organization): Promise<OrganizationRegistrationPeriod> {
79
+ const organization = await this.getOrganization(id);
80
+ return await organization.getPeriod()
81
+ }
82
+
78
83
  error(message?: string): SimpleError {
79
84
  return new SimpleError({
80
85
  code: "permission_denied",
@@ -171,7 +176,8 @@ export class AdminPermissionChecker {
171
176
  }
172
177
 
173
178
  // Check parent categories
174
- const parentCategories = group.getParentCategories(organization.meta.categories)
179
+ const organizationPeriod = await this.getOrganizationCurrentPeriod(organization)
180
+ const parentCategories = group.getParentCategories(organizationPeriod.settings.categories)
175
181
  for (const category of parentCategories) {
176
182
  if (organizationPermissions.hasResourceAccess(PermissionsResourceType.GroupCategories, category.id, permissionLevel)) {
177
183
  return true
@@ -677,11 +683,22 @@ export class AdminPermissionChecker {
677
683
  return false;
678
684
  }
679
685
 
680
- if (!organizationPermissions.hasResourceAccessRight(PermissionsResourceType.GroupCategories, category.id, AccessRight.OrganizationCreateGroups)) {
681
- return false;
686
+ if (organizationPermissions.hasResourceAccessRight(PermissionsResourceType.GroupCategories, category.id, AccessRight.OrganizationCreateGroups)) {
687
+ return true;
682
688
  }
683
689
 
684
- return true;
690
+ // Check parents
691
+ const organization = await this.getOrganization(organizationId)
692
+ const organizationPeriod = await this.getOrganizationCurrentPeriod(organization)
693
+ const parentCategories = category.getParentCategories(organizationPeriod.settings.categories)
694
+
695
+ for (const parentCategory of parentCategories) {
696
+ if (organizationPermissions.hasResourceAccessRight(PermissionsResourceType.GroupCategories, parentCategory.id, AccessRight.OrganizationCreateGroups)) {
697
+ return true;
698
+ }
699
+ }
700
+
701
+ return false;
685
702
  }
686
703
 
687
704
  canUpload() {
@@ -238,11 +238,12 @@ export class AuthenticatedStructures {
238
238
  }
239
239
  }
240
240
  }
241
-
241
+ member.registrations = member.registrations.filter(r => (Context.auth.organization && Context.auth.organization.active && r.organizationId === Context.auth.organization.id) || (organizations.get(r.organizationId)?.active ?? false))
242
242
  const blob = member.getStructureWithRegistrations()
243
243
  memberBlobs.push(
244
244
  await Context.auth.filterMemberData(member, blob)
245
245
  )
246
+
246
247
  }
247
248
 
248
249
  // Load responsibilities
@@ -268,7 +269,7 @@ export class AuthenticatedStructures {
268
269
 
269
270
  return MembersBlob.create({
270
271
  members: memberBlobs,
271
- organizations: await Promise.all([...organizations.values()].map(o => this.organization(o)))
272
+ organizations: await Promise.all([...organizations.values()].filter(o => o.active).map(o => this.organization(o)))
272
273
  })
273
274
  }
274
275
 
@@ -0,0 +1,126 @@
1
+ import { SimpleError } from "@simonbackx/simple-errors";
2
+ import { BalanceItem, Member, MemberPlatformMembership, Platform } from "@stamhoofd/models";
3
+ import { SQL, SQLOrderBy, SQLWhereSign } from "@stamhoofd/sql";
4
+ import { BalanceItemRelation, BalanceItemRelationType, BalanceItemType } from "@stamhoofd/structures";
5
+ import { Formatter } from "@stamhoofd/utility";
6
+
7
+ export const MembershipCharger = {
8
+ async charge() {
9
+ console.log('Charging memberships...')
10
+
11
+ // Loop all
12
+ let lastId = "";
13
+ const platform = await Platform.getShared()
14
+ const chargeVia = platform.membershipOrganizationId
15
+
16
+ if (!chargeVia) {
17
+ throw new SimpleError({
18
+ code: 'missing_membership_organization',
19
+ message: 'Missing membershipOrganizationId',
20
+ human: 'Er is geen lokale groep verantwoordelijk voor de aanrekening van aansluitingen geconfigureerd'
21
+ })
22
+ }
23
+
24
+ function getType(id: string) {
25
+ return platform.config.membershipTypes.find(t => t.id === id)
26
+ }
27
+
28
+ let createdCount = 0;
29
+ let createdPrice = 0;
30
+
31
+ // eslint-disable-next-line no-constant-condition
32
+ while (true) {
33
+ const memberships = await MemberPlatformMembership.select()
34
+ .where('id', SQLWhereSign.Greater, lastId)
35
+ .where('balanceItemId', null)
36
+ .limit(100)
37
+ .orderBy(
38
+ new SQLOrderBy({
39
+ column: SQL.column('id'),
40
+ direction: 'ASC'
41
+ })
42
+ )
43
+ .fetch();
44
+
45
+ if (memberships.length === 0) {
46
+ break;
47
+ }
48
+
49
+ const memberIds = Formatter.uniqueArray(memberships.map(m => m.memberId))
50
+ const members = await Member.getByIDs(...memberIds)
51
+ const createdBalanceItems: BalanceItem[] = []
52
+
53
+ for (const membership of memberships) {
54
+ // charge
55
+ if (membership.balanceItemId) {
56
+ continue;
57
+ }
58
+ const type = getType(membership.membershipTypeId);
59
+ if (!type) {
60
+ console.error('Unknown membership type id ', membership.membershipTypeId)
61
+ continue;
62
+ }
63
+
64
+ if (membership.organizationId === chargeVia) {
65
+ continue;
66
+ }
67
+
68
+ const member = members.find(m => m.id === membership.memberId)
69
+
70
+ if (!member) {
71
+ console.error('Unexpected missing member id ', membership.memberId, 'for membership', membership.id)
72
+ continue;
73
+ }
74
+
75
+ const balanceItem = new BalanceItem();
76
+ balanceItem.unitPrice = membership.price
77
+ balanceItem.amount = 1
78
+ balanceItem.description = Formatter.dateNumber(membership.startDate, true) + " tot " + Formatter.dateNumber(membership.expireDate ?? membership.endDate, true)
79
+ balanceItem.relations = new Map([
80
+ [
81
+ BalanceItemRelationType.Member,
82
+ BalanceItemRelation.create({
83
+ id: member.id,
84
+ name: member.details.name
85
+ })
86
+ ],
87
+ [
88
+ BalanceItemRelationType.MembershipType,
89
+ BalanceItemRelation.create({
90
+ id: type.id,
91
+ name: type.name
92
+ })
93
+ ]
94
+ ])
95
+
96
+ balanceItem.type = BalanceItemType.PlatformMembership
97
+ balanceItem.organizationId = chargeVia
98
+ balanceItem.payingOrganizationId = membership.organizationId
99
+
100
+ await balanceItem.save();
101
+ membership.balanceItemId = balanceItem.id;
102
+ await membership.save()
103
+
104
+ createdBalanceItems.push(balanceItem)
105
+
106
+ createdCount += 1;
107
+ createdPrice += membership.price
108
+ }
109
+
110
+ await BalanceItem.updateOutstanding(createdBalanceItems)
111
+
112
+ if (memberships.length < 100) {
113
+ break;
114
+ }
115
+
116
+ const z = lastId;
117
+ lastId = memberships[memberships.length - 1].id;
118
+
119
+ if (lastId === z) {
120
+ throw new Error('Unexpected infinite loop found in MembershipCharger')
121
+ }
122
+ }
123
+
124
+ console.log('Charged ' + Formatter.integer(createdCount) +' memberships, for a total value of ' + Formatter.price(createdPrice))
125
+ }
126
+ };