@stamhoofd/backend 2.61.1 → 2.63.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.61.1",
3
+ "version": "2.63.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -34,17 +34,17 @@
34
34
  "@bwip-js/node": "^4.5.1",
35
35
  "@mollie/api-client": "3.7.0",
36
36
  "@simonbackx/simple-database": "1.27.0",
37
- "@simonbackx/simple-encoding": "2.18.0",
37
+ "@simonbackx/simple-encoding": "2.19.0",
38
38
  "@simonbackx/simple-endpoints": "1.15.0",
39
39
  "@simonbackx/simple-logging": "^1.0.1",
40
- "@stamhoofd/backend-i18n": "2.61.1",
41
- "@stamhoofd/backend-middleware": "2.61.1",
42
- "@stamhoofd/email": "2.61.1",
43
- "@stamhoofd/models": "2.61.1",
44
- "@stamhoofd/queues": "2.61.1",
45
- "@stamhoofd/sql": "2.61.1",
46
- "@stamhoofd/structures": "2.61.1",
47
- "@stamhoofd/utility": "2.61.1",
40
+ "@stamhoofd/backend-i18n": "2.63.0",
41
+ "@stamhoofd/backend-middleware": "2.63.0",
42
+ "@stamhoofd/email": "2.63.0",
43
+ "@stamhoofd/models": "2.63.0",
44
+ "@stamhoofd/queues": "2.63.0",
45
+ "@stamhoofd/sql": "2.63.0",
46
+ "@stamhoofd/structures": "2.63.0",
47
+ "@stamhoofd/utility": "2.63.0",
48
48
  "archiver": "^7.0.1",
49
49
  "aws-sdk": "^2.885.0",
50
50
  "axios": "1.6.8",
@@ -64,5 +64,5 @@
64
64
  "publishConfig": {
65
65
  "access": "public"
66
66
  },
67
- "gitHead": "f092f61cf3641682fb5a5a9cb50c7977b1e350ef"
67
+ "gitHead": "0dd71a1869c5931dcf844dea1ecd2167819dc5c6"
68
68
  }
@@ -26,7 +26,7 @@ export const PaymentLogger = new ModelLogger(Payment, {
26
26
  },
27
27
 
28
28
  createReplacements(model, options) {
29
- let name = `${PaymentMethodHelper.getPaymentName(model.method)}`;
29
+ let name = `${PaymentMethodHelper.getPaymentName(model.method, model.type)}`;
30
30
 
31
31
  if (model.customer?.dynamicName) {
32
32
  name += ` van ${model.customer.dynamicName}`;
@@ -18,6 +18,11 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
18
18
  email,
19
19
  replacements: [
20
20
  Replacement.create({
21
+ token: 'objectName',
22
+ value: balance.object.name,
23
+ }),
24
+ Replacement.create({
25
+ // Deprecated: for backwards compatibility
21
26
  token: 'organizationName',
22
27
  value: balance.object.name,
23
28
  }),
@@ -5,7 +5,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
6
  import { Email } from '@stamhoofd/email';
7
7
  import { BalanceItem, BalanceItemPayment, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
8
- import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, Version } from '@stamhoofd/structures';
8
+ import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BalanceItem as BalanceItemStruct, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, Version, PaymentType } from '@stamhoofd/structures';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
 
11
11
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -104,7 +104,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
104
104
 
105
105
  // Validate balance items (can only happen serverside)
106
106
  const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id);
107
- let memberBalanceItemsStructs: BalanceItemWithPayments[] = [];
107
+ let memberBalanceItemsStructs: BalanceItemStruct[] = [];
108
108
  let balanceItemsModels: BalanceItem[] = [];
109
109
  if (balanceItemIds.length > 0) {
110
110
  balanceItemsModels = await BalanceItem.where({ id: { sign: 'IN', value: balanceItemIds }, organizationId: organization.id });
@@ -114,7 +114,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
114
114
  message: 'Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw.',
115
115
  });
116
116
  }
117
- memberBalanceItemsStructs = await BalanceItem.getStructureWithPayments(balanceItemsModels);
117
+ memberBalanceItemsStructs = balanceItemsModels.map(i => i.getStructure());
118
118
  }
119
119
 
120
120
  const memberIds = Formatter.uniqueArray(
@@ -429,7 +429,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
429
429
  balanceItem2.memberId = registration.memberId;
430
430
 
431
431
  // If the paying organization hasn't paid yet, this should be hidden and move to pending as soon as the paying organization has paid
432
- balanceItem2.status = BalanceItemStatus.Hidden; // shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
432
+ balanceItem2.status = BalanceItemStatus.Hidden;
433
433
  await balanceItem2.save();
434
434
 
435
435
  // do not add to createdBalanceItems array because we don't want to add this to the payment if we create a payment
@@ -440,7 +440,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
440
440
  balanceItem.userId = user.id;
441
441
  }
442
442
 
443
- balanceItem.status = BalanceItemStatus.Hidden; // shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
443
+ balanceItem.status = BalanceItemStatus.Hidden;
444
444
  balanceItem.pricePaid = 0;
445
445
 
446
446
  // Connect the 'pay back' balance item to this balance item. As soon as this balance item is paid, we'll mark the other one as pending so the outstanding balance for the member increases
@@ -554,7 +554,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
554
554
  if (oldestMember) {
555
555
  balanceItem.memberId = oldestMember.id;
556
556
  }
557
- balanceItem.status = BalanceItemStatus.Hidden; // shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
557
+ balanceItem.status = BalanceItemStatus.Hidden;
558
558
  await balanceItem.save();
559
559
  createdBalanceItems.push(balanceItem);
560
560
  }
@@ -579,7 +579,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
579
579
  }
580
580
  }
581
581
 
582
- balanceItem.status = BalanceItemStatus.Hidden; // shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden;
582
+ balanceItem.status = BalanceItemStatus.Hidden;
583
583
  await balanceItem.save();
584
584
 
585
585
  createdBalanceItems.push(balanceItem);
@@ -674,6 +674,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
674
674
  // Calculate total price to pay
675
675
  let totalPrice = 0;
676
676
  const payMembers: MemberWithRegistrations[] = [];
677
+ let hasNegative = false;
677
678
 
678
679
  for (const [balanceItem, price] of balanceItems) {
679
680
  if (organization.id !== balanceItem.organizationId) {
@@ -694,6 +695,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
694
695
  });
695
696
  }
696
697
 
698
+ if (price < 0) {
699
+ hasNegative = true;
700
+ }
701
+
697
702
  totalPrice += price;
698
703
 
699
704
  if (price > 0 && balanceItem.memberId) {
@@ -709,27 +714,33 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
709
714
  }
710
715
 
711
716
  if (totalPrice < 0) {
712
- // No payment needed: the outstanding balance will be negative and can be used in the future
713
- return;
714
- // throw new SimpleError({
715
- // code: "empty_data",
716
- // message: "Oeps! De totaalprijs is negatief."
717
- // })
717
+ // todo: try to make it non-negative by reducing some balance items
718
+ throw new SimpleError({
719
+ code: 'empty_data',
720
+ message: 'Oeps! De totaalprijs is negatief.',
721
+ });
718
722
  }
723
+ const payment = new Payment();
724
+ payment.method = checkout.paymentMethod ?? PaymentMethod.Unknown;
719
725
 
720
726
  if (totalPrice === 0) {
721
- return;
722
- }
727
+ if (balanceItems.size === 0) {
728
+ return;
729
+ }
730
+ // Create an egalizing payment
731
+ payment.method = PaymentMethod.Unknown;
723
732
 
724
- if (!checkout.paymentMethod || checkout.paymentMethod === PaymentMethod.Unknown) {
733
+ if (hasNegative) {
734
+ payment.type = PaymentType.Reallocation;
735
+ }
736
+ }
737
+ else if (payment.method === PaymentMethod.Unknown) {
725
738
  throw new SimpleError({
726
739
  code: 'invalid_data',
727
740
  message: 'Oeps, je hebt geen betaalmethode geselecteerd. Selecteer een betaalmethode en probeer opnieuw.',
728
741
  });
729
742
  }
730
743
 
731
- const payment = new Payment();
732
-
733
744
  // Who will receive this money?
734
745
  payment.organizationId = organization.id;
735
746
 
@@ -793,11 +804,16 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
793
804
  }
794
805
  }
795
806
 
796
- payment.method = checkout.paymentMethod;
797
807
  payment.status = PaymentStatus.Created;
808
+ payment.paidAt = null;
798
809
  payment.price = totalPrice;
799
810
 
800
- if (payment.method == PaymentMethod.Transfer) {
811
+ if (totalPrice === 0) {
812
+ payment.status = PaymentStatus.Succeeded;
813
+ payment.paidAt = new Date();
814
+ }
815
+
816
+ if (payment.method === PaymentMethod.Transfer) {
801
817
  // remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
802
818
  payment.transferSettings = organization.mappedTransferSettings;
803
819
 
@@ -821,7 +837,6 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
821
837
  },
822
838
  );
823
839
  }
824
- payment.paidAt = null;
825
840
 
826
841
  // Determine the payment provider
827
842
  // Throws if invalid
@@ -861,7 +876,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
861
876
  console.error(e);
862
877
  }
863
878
  }
864
- else if (payment.method !== PaymentMethod.PointOfSale) {
879
+ else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
865
880
  if (!checkout.redirectUrl || !checkout.cancelUrl) {
866
881
  throw new Error('Should have been caught earlier');
867
882
  }
@@ -43,10 +43,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
43
43
  const returnedModels: BalanceItem[] = [];
44
44
  const updateOutstandingBalance: BalanceItem[] = [];
45
45
 
46
- // Keep track of updates
47
- const memberIds: string[] = [];
48
- const registrationIds: string[] = [];
49
-
50
46
  await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
51
47
  for (const { put } of request.body.getPuts()) {
52
48
  // Create a new balance item
@@ -58,7 +54,8 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
58
54
  model.amount = put.amount;
59
55
  model.organizationId = organization.id;
60
56
  model.createdAt = put.createdAt;
61
- model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending;
57
+ model.dueAt = put.dueAt;
58
+ model.status = put.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Due;
62
59
 
63
60
  if (put.userId) {
64
61
  model.userId = (await this.validateUserId(model, put.userId)).id;
@@ -66,10 +63,43 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
66
63
 
67
64
  if (put.memberId) {
68
65
  model.memberId = (await this.validateMemberId(put.memberId)).id;
69
- memberIds.push(model.memberId);
70
66
  }
71
67
 
72
- if (!model.userId && !model.memberId) {
68
+ if (put.payingOrganizationId) {
69
+ // Not allowed if not full admin
70
+ if (!Context.auth.hasPlatformFullAccess()) {
71
+ throw Context.auth.error('Je moet volledige platform beheerder zijn om schulden tussen verenigingen te wijzigen of toe te voegen');
72
+ }
73
+ if (put.payingOrganizationId === model.organizationId) {
74
+ throw new SimpleError({
75
+ code: 'invalid_field',
76
+ message: 'payingOrganizationId cannot be the same as organizationId',
77
+ human: 'Dit is een ongeldige situatie. Een schuld moet tussen verschillende verenigingen zijn.',
78
+ field: 'payingOrganizationId',
79
+ });
80
+ }
81
+
82
+ model.payingOrganizationId = put.payingOrganizationId;
83
+ }
84
+
85
+ if (model.dueAt && model.price < 0) {
86
+ throw new SimpleError({
87
+ code: 'invalid_price',
88
+ message: 'Cannot create negative balance in the future',
89
+ human: 'Het is niet mogelijk om een negatief openstaand bedrag toe te voegen in de toekomst',
90
+ });
91
+ }
92
+
93
+ if (model.createdAt > new Date()) {
94
+ throw new SimpleError({
95
+ code: 'invalid_field',
96
+ message: 'createdAt cannot be in the future',
97
+ human: 'De datum kan niet in de toekomst liggen',
98
+ field: 'createdAt',
99
+ });
100
+ }
101
+
102
+ if (!model.userId && !model.memberId && !model.payingOrganizationId) {
73
103
  throw new SimpleError({
74
104
  code: 'invalid_field',
75
105
  message: 'No user or member provided',
@@ -93,39 +123,53 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
93
123
  });
94
124
  }
95
125
 
96
- if (patch.unitPrice !== undefined) {
97
- // throw new SimpleError({
98
- // code: 'invalid_field',
99
- // message: 'You cannot change the unit price of a balance item',
100
- // human: 'Het is niet mogelijk om de eenheidsprijs van een openstaande schuld te wijzigen. Je kan de openstaande schuld verwijderen en opnieuw aanmaken indien noodzakelijk.',
101
- // });
102
- }
126
+ if (patch.payingOrganizationId !== undefined) {
127
+ // Not allowed if not full admin
128
+ if (!Context.auth.hasPlatformFullAccess()) {
129
+ throw Context.auth.error('Je moet volledige platform beheerder zijn om schulden tussen verenigingen te wijzigen of toe te voegen');
130
+ }
131
+ if (patch.payingOrganizationId === model.organizationId) {
132
+ throw new SimpleError({
133
+ code: 'invalid_field',
134
+ message: 'payingOrganizationId cannot be the same as organizationId',
135
+ human: 'Dit is een ongeldige situatie. Een schuld moet tussen verschillende verenigingen zijn.',
136
+ field: 'payingOrganizationId',
137
+ });
138
+ }
103
139
 
104
- // Check permissions
105
- if (model.memberId) {
106
- // Update old
107
- memberIds.push(model.memberId);
140
+ model.payingOrganizationId = patch.payingOrganizationId;
108
141
  }
109
142
 
110
143
  if (patch.memberId) {
111
144
  model.memberId = (await this.validateMemberId(patch.memberId)).id;
112
-
113
- // Update new
114
- memberIds.push(model.memberId);
115
- }
116
-
117
- if (model.registrationId) {
118
- // Update old
119
- registrationIds.push(model.registrationId);
120
145
  }
121
146
 
122
147
  if (patch.createdAt) {
123
148
  model.createdAt = patch.createdAt;
149
+
150
+ if (model.createdAt > new Date()) {
151
+ throw new SimpleError({
152
+ code: 'invalid_field',
153
+ message: 'createdAt cannot be in the future',
154
+ human: 'De datum kan niet in de toekomst liggen',
155
+ field: 'createdAt',
156
+ });
157
+ }
124
158
  }
125
159
 
126
160
  model.description = patch.description ?? model.description;
127
161
  model.unitPrice = patch.unitPrice ?? model.unitPrice;
128
162
  model.amount = patch.amount ?? model.amount;
163
+ model.dueAt = patch.dueAt === undefined ? model.dueAt : patch.dueAt;
164
+
165
+ if ((patch.dueAt !== undefined || patch.unitPrice !== undefined) && model.dueAt && model.price < 0) {
166
+ throw new SimpleError({
167
+ code: 'invalid_price',
168
+ message: 'Cannot create negative balance in the future',
169
+ human: 'Het is niet mogelijk om een negatief openstaand bedrag toe te voegen in de toekomst',
170
+ field: 'dueAt',
171
+ });
172
+ }
129
173
 
130
174
  if (model.orderId) {
131
175
  // Not allowed to change this
@@ -142,13 +186,21 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
142
186
  }
143
187
  }
144
188
  else if (patch.status) {
145
- model.status = model.pricePaid >= model.price ? BalanceItemStatus.Paid : BalanceItemStatus.Pending;
189
+ model.status = patch.status;
190
+ }
191
+
192
+ if (!model.userId && !model.memberId && !model.payingOrganizationId) {
193
+ throw new SimpleError({
194
+ code: 'invalid_field',
195
+ message: 'No user or member provided',
196
+ field: 'userId',
197
+ });
146
198
  }
147
199
 
148
200
  await model.save();
149
201
  returnedModels.push(model);
150
202
 
151
- if (patch.unitPrice || patch.amount || patch.status) {
203
+ if (patch.unitPrice || patch.amount || patch.status || patch.dueAt !== undefined) {
152
204
  updateOutstandingBalance.push(model);
153
205
  }
154
206
  }
@@ -1,15 +1,14 @@
1
1
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { BalanceItem, BalanceItemPayment, Payment, Token } from '@stamhoofd/models';
4
+ import { BalanceItem, BalanceItemPayment, Payment } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
- import { Payment as PaymentStruct, PaymentGeneral, PaymentMethod, PaymentStatus, PermissionLevel } from '@stamhoofd/structures';
6
+ import { PaymentGeneral, PaymentMethod, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel } from '@stamhoofd/structures';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
9
  import { Context } from '../../../../helpers/Context';
10
- import { ExchangePaymentEndpoint } from '../../shared/ExchangePaymentEndpoint';
11
- import { PaymentService } from '../../../../services/PaymentService';
12
10
  import { BalanceItemService } from '../../../../services/BalanceItemService';
11
+ import { PaymentService } from '../../../../services/PaymentService';
13
12
 
14
13
  type Params = Record<string, never>;
15
14
  type Query = undefined;
@@ -49,11 +48,11 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
49
48
  // Modify payments
50
49
  for (const { put } of request.body.getPuts()) {
51
50
  // Create a new payment
52
- if (put.balanceItemPayments.length == 0) {
51
+ if (put.balanceItemPayments.length === 0) {
53
52
  throw new SimpleError({
54
53
  code: 'invalid_field',
55
54
  message: 'You need to add at least one balance item payment',
56
- human: 'Een betaling moet ten minste één afrekening bevatten voor een openstaande rekening.',
55
+ human: 'Een betaling moet ten minste één item bestaan',
57
56
  field: 'balanceItemPayments',
58
57
  });
59
58
  }
@@ -72,8 +71,9 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
72
71
  payment.status = PaymentStatus.Created;
73
72
  payment.method = put.method;
74
73
  payment.customer = put.customer;
74
+ payment.type = put.type;
75
75
 
76
- if (payment.method == PaymentMethod.Transfer) {
76
+ if (payment.method === PaymentMethod.Transfer) {
77
77
  if (!put.transferSettings) {
78
78
  throw new SimpleError({
79
79
  code: 'invalid_field',
@@ -112,7 +112,11 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
112
112
  balanceItemPayment.organizationId = organization.id;
113
113
  balanceItemPayment.balanceItemId = balanceItem.id;
114
114
  balanceItemPayment.price = item.price;
115
- balanceItemPayments.push(balanceItemPayment);
115
+
116
+ if (item.price !== 0) {
117
+ // Otherwise skip
118
+ balanceItemPayments.push(balanceItemPayment);
119
+ }
116
120
  }
117
121
 
118
122
  // Check permissions
@@ -122,17 +126,55 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
122
126
 
123
127
  // Check total price
124
128
  const totalPrice = balanceItemPayments.reduce((total, item) => total + item.price, 0);
129
+ payment.price = totalPrice;
125
130
 
126
- if (totalPrice !== put.price) {
127
- throw new SimpleError({
128
- code: 'invalid_field',
129
- message: "Total price doesn't match",
130
- human: 'De totale prijs komt niet overeen met de som van de afrekeningen',
131
- field: 'price',
132
- });
133
- }
131
+ switch (payment.type) {
132
+ case PaymentType.Payment: {
133
+ if (totalPrice <= 0) {
134
+ throw new SimpleError({
135
+ code: 'invalid_field',
136
+ message: 'The price should be greater than zero',
137
+ human: 'Het totaalbedrag moet groter zijn dan 0 euro',
138
+ field: 'price',
139
+ });
140
+ }
141
+ break;
142
+ }
134
143
 
135
- payment.price = totalPrice;
144
+ case PaymentType.Chargeback:
145
+ case PaymentType.Refund: {
146
+ if (totalPrice >= 0) {
147
+ throw new SimpleError({
148
+ code: 'invalid_field',
149
+ message: 'The price should be smaller than zero',
150
+ human: 'Het totaalbedrag moet kleiner zijn dan 0 euro',
151
+ field: 'price',
152
+ });
153
+ }
154
+ break;
155
+ }
156
+
157
+ case PaymentType.Reallocation:
158
+ {
159
+ if (totalPrice !== 0) {
160
+ throw new SimpleError({
161
+ code: 'invalid_field',
162
+ message: 'Total price should be zero',
163
+ human: 'Het totaalbedrag moet 0 euro zijn',
164
+ field: 'price',
165
+ });
166
+ }
167
+
168
+ if (balanceItemPayments.length < 2) {
169
+ throw new SimpleError({
170
+ code: 'missing_items',
171
+ message: 'At least two items are required for a reallocation',
172
+ human: 'Er moeten minstens twee items in een verrekening zitten',
173
+ });
174
+ }
175
+ break;
176
+ }
177
+ }
136
178
 
137
179
  // Save payment
138
180
  await payment.save();
@@ -44,10 +44,19 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
44
44
  case ReceivableBalanceType.organization: {
45
45
  paymentModels = await Payment.select()
46
46
  .where('organizationId', organization.id)
47
- .where('payingOrganizationId', request.params.id)
48
47
  .andWhere(
49
48
  SQL.whereNot('status', PaymentStatus.Failed),
50
49
  )
50
+ .join(
51
+ SQL.join(BalanceItemPayment.table)
52
+ .where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
53
+ )
54
+ .join(
55
+ SQL.join(BalanceItem.table)
56
+ .where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
57
+ )
58
+ .where(SQL.column(BalanceItem.table, 'payingOrganizationId'), request.params.id)
59
+ .groupBy(SQL.column(Payment.table, 'id'))
51
60
  .fetch();
52
61
  break;
53
62
  }
@@ -71,6 +80,26 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
71
80
  .fetch();
72
81
  break;
73
82
  }
83
+
84
+ case ReceivableBalanceType.user: {
85
+ paymentModels = await Payment.select()
86
+ .where('organizationId', organization.id)
87
+ .join(
88
+ SQL.join(BalanceItemPayment.table)
89
+ .where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
90
+ )
91
+ .join(
92
+ SQL.join(BalanceItem.table)
93
+ .where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
94
+ )
95
+ .where(SQL.column(BalanceItem.table, 'userId'), request.params.id)
96
+ .andWhere(
97
+ SQL.whereNot('status', PaymentStatus.Failed),
98
+ )
99
+ .groupBy(SQL.column(Payment.table, 'id'))
100
+ .fetch();
101
+ break;
102
+ }
74
103
  }
75
104
 
76
105
  const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { CachedBalance } from '@stamhoofd/models';
5
5
  import { compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
6
- import { ReceivableBalance as ReceivableBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
6
+ import { ReceivableBalance as ReceivableBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter, ReceivableBalanceType } from '@stamhoofd/structures';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
9
  import { Context } from '../../../../helpers/Context';
@@ -51,9 +51,17 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
51
51
  $or: {
52
52
  amount: { $neq: 0 },
53
53
  amountPending: { $neq: 0 },
54
+ nextDueAt: { $neq: null },
54
55
  },
55
56
  };
56
57
 
58
+ if (!Context.auth.hasSomePlatformAccess()) {
59
+ // Cannot see debt between organizations
60
+ scopeFilter.objectType = {
61
+ $neq: ReceivableBalanceType.organization,
62
+ };
63
+ }
64
+
57
65
  const query = CachedBalance
58
66
  .select();
59
67
 
@@ -1,9 +1,9 @@
1
1
  import { ArrayDecoder, AutoEncoderPatchType, Data, Decoder, PatchableArray, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { BalanceItem, BalanceItemPayment, Order, Payment, Token, Webshop } from '@stamhoofd/models';
4
+ import { BalanceItem, BalanceItemPayment, Order, Payment, Webshop, WebshopCounter } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
- import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment, Webshop as WebshopStruct } from '@stamhoofd/structures';
6
+ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, Webshop as WebshopStruct } from '@stamhoofd/structures';
7
7
 
8
8
  import { Context } from '../../../../helpers/Context';
9
9
  import { AuditLogService } from '../../../../services/AuditLogService';
@@ -83,6 +83,8 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
83
83
  return new Response([]);
84
84
  }
85
85
 
86
+ let clearNumbers = false;
87
+
86
88
  // Need to happen in the queue because we are updating the webshop stock
87
89
  const orders = await QueueHandler.schedule('webshop-stock/' + request.params.id, async () => {
88
90
  const webshop = await Webshop.getByID(request.params.id);
@@ -167,7 +169,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
167
169
  balanceItem.description = webshop.meta.name;
168
170
  balanceItem.pricePaid = 0;
169
171
  balanceItem.organizationId = organization.id;
170
- balanceItem.status = BalanceItemStatus.Pending;
172
+ balanceItem.status = BalanceItemStatus.Due;
171
173
  balanceItem.relations = new Map([
172
174
  [
173
175
  BalanceItemRelationType.Webshop,
@@ -244,6 +246,11 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
244
246
 
245
247
  if (model.status === OrderStatus.Deleted) {
246
248
  model.data.removePersonalData();
249
+
250
+ if (model.number !== null) {
251
+ model.number = Math.floor(Math.random() * 1000000000000) + 1000000000000;
252
+ }
253
+ clearNumbers = true;
247
254
  }
248
255
 
249
256
  if (model.status === OrderStatus.Deleted || model.status === OrderStatus.Canceled) {
@@ -264,13 +271,23 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
264
271
  const items = await BalanceItem.where({ orderId: model.id });
265
272
  if (items.length >= 1) {
266
273
  model.markUpdated();
267
- items[0].unitPrice = model.totalToPay;
268
- items[0].description = model.generateBalanceDescription(webshop);
269
- items[0].updateStatus();
270
- await items[0].save();
274
+
275
+ const paidItem = items.find(i => i.status === BalanceItemStatus.Due && i.pricePaid !== 0) ?? items[0];
276
+
277
+ paidItem.unitPrice = model.data.totalPrice;
278
+ paidItem.amount = 1;
279
+
280
+ if (model.isDue) {
281
+ paidItem.status = BalanceItemStatus.Due;
282
+ }
283
+ else {
284
+ paidItem.status = BalanceItemStatus.Canceled;
285
+ }
286
+ paidItem.description = model.generateBalanceDescription(webshop);
287
+ await paidItem.save();
271
288
 
272
289
  // Zero out the other items
273
- const otherItems = items.slice(1);
290
+ const otherItems = items.filter(i => i.id !== paidItem.id);
274
291
  await BalanceItem.deleteItems(otherItems);
275
292
  }
276
293
  else if (items.length === 0
@@ -278,11 +295,12 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
278
295
  model.markUpdated();
279
296
  const balanceItem = new BalanceItem();
280
297
  balanceItem.orderId = model.id;
281
- balanceItem.unitPrice = model.totalToPay;
298
+ balanceItem.unitPrice = model.data.totalPrice;
299
+ balanceItem.amount = 1;
300
+ balanceItem.status = BalanceItemStatus.Due;
282
301
  balanceItem.description = model.generateBalanceDescription(webshop);
283
302
  balanceItem.pricePaid = 0;
284
303
  balanceItem.organizationId = organization.id;
285
- balanceItem.status = BalanceItemStatus.Pending;
286
304
  await balanceItem.save();
287
305
  }
288
306
  }
@@ -298,6 +316,10 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
298
316
  return mapped;
299
317
  });
300
318
 
319
+ if (clearNumbers) {
320
+ WebshopCounter.resetNumbers(request.params.id);
321
+ }
322
+
301
323
  return new Response(
302
324
  await Order.getPrivateStructures(orders),
303
325
  );
@@ -6,7 +6,7 @@ import { I18n } from '@stamhoofd/backend-i18n';
6
6
  import { Email } from '@stamhoofd/email';
7
7
  import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
8
8
  import { QueueHandler } from '@stamhoofd/queues';
9
- import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct, WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation, AuditLogSource } from '@stamhoofd/structures';
9
+ import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct, WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation, AuditLogSource, PaymentCustomer } from '@stamhoofd/structures';
10
10
  import { Formatter } from '@stamhoofd/utility';
11
11
 
12
12
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
@@ -169,6 +169,11 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
169
169
  payment.status = PaymentStatus.Created;
170
170
  payment.price = totalPrice;
171
171
  payment.paidAt = null;
172
+ payment.customer = PaymentCustomer.create({
173
+ firstName: request.body.customer.firstName,
174
+ lastName: request.body.customer.lastName,
175
+ email: request.body.customer.email,
176
+ });
172
177
 
173
178
  // Determine the payment provider
174
179
  // Throws if invalid
@@ -219,18 +224,18 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
219
224
  let paymentUrl: string | null = null;
220
225
  const description = webshop.meta.name + ' - ' + payment.id;
221
226
 
222
- if (payment.method == PaymentMethod.Transfer) {
227
+ if (payment.method === PaymentMethod.Transfer) {
223
228
  await order.markValid(payment, []);
224
229
 
225
230
  if (order.number) {
226
231
  balanceItem.description = order.generateBalanceDescription(webshop);
227
232
  }
228
233
 
229
- balanceItem.status = BalanceItemStatus.Pending;
234
+ balanceItem.status = BalanceItemStatus.Due;
230
235
  await balanceItem.save();
231
236
  await payment.save();
232
237
  }
233
- else if (payment.method == PaymentMethod.PointOfSale) {
238
+ else if (payment.method === PaymentMethod.PointOfSale) {
234
239
  // Not really paid, but needed to create the tickets if needed
235
240
  await order.markPaid(payment, organization, webshop);
236
241
 
@@ -238,7 +243,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
238
243
  balanceItem.description = order.generateBalanceDescription(webshop);
239
244
  }
240
245
 
241
- balanceItem.status = BalanceItemStatus.Pending;
246
+ balanceItem.status = BalanceItemStatus.Due;
242
247
  await balanceItem.save();
243
248
  await payment.save();
244
249
  }
@@ -211,7 +211,7 @@ export class AdminPermissionChecker {
211
211
  }
212
212
 
213
213
  async canAccessMember(member: MemberWithRegistrations, permissionLevel: PermissionLevel = PermissionLevel.Read) {
214
- if (this.isUserManager(member) && permissionLevel !== PermissionLevel.Full) {
214
+ if (permissionLevel !== PermissionLevel.Full && this.isUserManager(member)) {
215
215
  return true;
216
216
  }
217
217
 
@@ -1107,13 +1107,22 @@ export class AdminPermissionChecker {
1107
1107
  const isSetFinancialSupportTrue = data.details.shouldApplyReducedPrice;
1108
1108
  const isUserManager = this.isUserManager(member);
1109
1109
 
1110
- if (data.details.securityCode !== undefined) {
1110
+ if (data.details.securityCode !== undefined || data.details.trackingYear !== undefined) {
1111
1111
  const hasFullAccess = await this.canAccessMember(member, PermissionLevel.Full);
1112
1112
 
1113
- // can only be set to null, and only if can access member with full access
1114
- if (!hasFullAccess || data.details.securityCode !== null) {
1115
- // Unset silently
1116
- data.details.securityCode = undefined;
1113
+ if (!hasFullAccess) {
1114
+ if (data.details.securityCode !== undefined) {
1115
+ // can only be set to null, and only if can access member with full access
1116
+ if (data.details.securityCode !== null) {
1117
+ // Unset silently
1118
+ data.details.securityCode = undefined;
1119
+ }
1120
+ }
1121
+
1122
+ if (data.details.trackingYear !== undefined) {
1123
+ // Unset silently
1124
+ data.details.trackingYear = undefined;
1125
+ }
1117
1126
  }
1118
1127
  }
1119
1128
 
@@ -1301,7 +1310,7 @@ export class AdminPermissionChecker {
1301
1310
  }
1302
1311
 
1303
1312
  hasSomePlatformAccess(): boolean {
1304
- return !!this.platformPermissions;
1313
+ return !!this.platformPermissions && !this.platformPermissions.isEmpty;
1305
1314
  }
1306
1315
 
1307
1316
  canManagePlatformAdmins() {
@@ -40,8 +40,6 @@ export class AuthenticatedStructures {
40
40
 
41
41
  const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions;
42
42
 
43
- console.log('includeSettlements', includeSettlements);
44
-
45
43
  const { payingOrganizations } = await Payment.loadPayingOrganizations(payments);
46
44
 
47
45
  return Payment.getGeneralStructureFromRelations({
@@ -576,6 +574,11 @@ export class AuthenticatedStructures {
576
574
  ]);
577
575
  const members = memberIds.length > 0 ? await Member.getByIDs(...memberIds) : [];
578
576
 
577
+ const userIds = Formatter.uniqueArray([
578
+ ...balances.filter(b => b.objectType === ReceivableBalanceType.user).map(b => b.objectId),
579
+ ]);
580
+ const users = userIds.length > 0 ? await User.getByIDs(...userIds) : [];
581
+
579
582
  const result: ReceivableBalanceStruct[] = [];
580
583
  for (const balance of balances) {
581
584
  let object = ReceivableBalanceObject.create({
@@ -584,7 +587,7 @@ export class AuthenticatedStructures {
584
587
  });
585
588
 
586
589
  if (balance.objectType === ReceivableBalanceType.organization) {
587
- const organization = organizationStructs.find(o => o.id == balance.objectId) ?? null;
590
+ const organization = organizationStructs.find(o => o.id === balance.objectId) ?? null;
588
591
  if (organization) {
589
592
  const theseResponsibilities = responsibilities.filter(r => r.organizationId === organization.id);
590
593
  const thisMembers = members.flatMap((m) => {
@@ -605,6 +608,7 @@ export class AuthenticatedStructures {
605
608
  lastName: member.lastName ?? '',
606
609
  emails: member.details.getMemberEmails(),
607
610
  meta: {
611
+ type: 'organization',
608
612
  responsibilityIds: responsibilities.map(r => r.responsibilityId),
609
613
  url: organization.dashboardUrl + '/boekhouding/openstaand/' + (Context.organization?.uri ?? ''),
610
614
  },
@@ -615,14 +619,58 @@ export class AuthenticatedStructures {
615
619
  else if (balance.objectType === ReceivableBalanceType.member) {
616
620
  const member = members.find(m => m.id === balance.objectId) ?? null;
617
621
  if (member) {
622
+ const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
618
623
  object = ReceivableBalanceObject.create({
619
624
  id: balance.objectId,
620
625
  name: member.details.name,
626
+ contacts: [
627
+ ...(member.details.getMemberEmails().length
628
+ ? [
629
+ ReceivableBalanceObjectContact.create({
630
+ firstName: member.details.firstName ?? '',
631
+ lastName: member.details.lastName ?? '',
632
+ emails: member.details.getMemberEmails(),
633
+ meta: {
634
+ type: 'member',
635
+ responsibilityIds: [],
636
+ url,
637
+ },
638
+ }),
639
+ ]
640
+ : []),
641
+
642
+ ...(member.details.parentsHaveAccess
643
+ ? member.details.parents.filter(p => !!p.email).map(p => ReceivableBalanceObjectContact.create({
644
+ firstName: p.firstName ?? '',
645
+ lastName: p.lastName ?? '',
646
+ emails: [p.email!],
647
+ meta: {
648
+ type: 'parent',
649
+ responsibilityIds: [],
650
+ url,
651
+ },
652
+ }))
653
+ : []),
654
+ ],
655
+ });
656
+ }
657
+ }
658
+ else if (balance.objectType === ReceivableBalanceType.user) {
659
+ const user = users.find(m => m.id === balance.objectId) ?? null;
660
+ if (user) {
661
+ const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
662
+ object = ReceivableBalanceObject.create({
663
+ id: balance.objectId,
664
+ name: user.name || user.email,
621
665
  contacts: [
622
666
  ReceivableBalanceObjectContact.create({
623
- firstName: member.details.firstName ?? '',
624
- lastName: member.details.lastName ?? '',
625
- emails: member.details.getMemberEmails(),
667
+ firstName: user.firstName ?? '',
668
+ lastName: user.lastName ?? '',
669
+ emails: [user.email],
670
+ meta: {
671
+ responsibilityIds: [],
672
+ url,
673
+ },
626
674
  }),
627
675
  ],
628
676
  });
@@ -210,6 +210,14 @@ export class StripeHelper {
210
210
 
211
211
  const totalPrice = payment.price;
212
212
 
213
+ if (totalPrice < 50) {
214
+ throw new SimpleError({
215
+ code: 'minmum_amount',
216
+ message: 'The minimum amount for an online payment is € 0,50',
217
+ human: 'Het minimum bedrag voor een online betaling is € 0,50',
218
+ });
219
+ }
220
+
213
221
  let fee = 0;
214
222
  let directCharge = false;
215
223
  const vat = calculateVATPercentage(organization.address, organization.meta.VATNumber);
@@ -0,0 +1,109 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { logger } from '@simonbackx/simple-logging';
3
+ import { Order, WebshopCounter } from '@stamhoofd/models';
4
+ import { OrderStatus } from '@stamhoofd/structures';
5
+
6
+ export default new Migration(async () => {
7
+ if (STAMHOOFD.environment == 'test') {
8
+ console.log('skipped in tests');
9
+ return;
10
+ }
11
+
12
+ process.stdout.write('\n');
13
+ let c = 0;
14
+ let pages = 0;
15
+ let id: string = '';
16
+
17
+ // There is an issue with some deleted orders that can't be stringified anymore
18
+ let limit = 100;
19
+ let restoreLimitAt: number | null = null;
20
+
21
+ await logger.setContext({ tags: ['seed'] }, async () => {
22
+ while (true) {
23
+ try {
24
+ const orders = await Order.where({
25
+ id: {
26
+ value: id,
27
+ sign: '>',
28
+ },
29
+ status: OrderStatus.Deleted,
30
+ }, { limit, sort: ['id'] });
31
+
32
+ if (orders.length === 0) {
33
+ break;
34
+ }
35
+
36
+ pages++;
37
+ process.stdout.write('.');
38
+ if (pages % 100 === 0) {
39
+ process.stdout.write('\n');
40
+ }
41
+
42
+ if (limit === 1 && restoreLimitAt && pages > restoreLimitAt) {
43
+ limit = 100;
44
+ }
45
+
46
+ for (const order of orders) {
47
+ c++;
48
+
49
+ if (order.status === OrderStatus.Deleted) {
50
+ order.data.removePersonalData();
51
+
52
+ if (order.number !== null) {
53
+ order.number = Math.floor(Math.random() * 1000000000000) + 1000000000000;
54
+ }
55
+ await order.save();
56
+ }
57
+ }
58
+
59
+ if (orders.length < limit) {
60
+ break;
61
+ }
62
+ id = orders[orders.length - 1].id;
63
+ }
64
+ catch (e) {
65
+ if (e.toString() && e.toString().includes('RangeError')) {
66
+ console.error('Found decoding issue at ' + id);
67
+ console.error(e);
68
+ if (limit === 1) {
69
+ // We found the causing order.
70
+
71
+ const _orders = await Order.where({
72
+ id: {
73
+ value: id,
74
+ sign: '>',
75
+ },
76
+ status: OrderStatus.Deleted,
77
+ }, { limit: 1, sort: ['id'], select: 'id' });
78
+
79
+ if (_orders.length === 1) {
80
+ console.log('Found broken order: ' + _orders[0].id);
81
+ console.log('Deleting order');
82
+ // Delete
83
+ await _orders[0].delete();
84
+ }
85
+ else {
86
+ console.error('Could not find causing order');
87
+ throw e;
88
+ }
89
+ }
90
+ // Something wrong with an order
91
+ // continue
92
+ limit = 1;
93
+ restoreLimitAt = pages + 100;
94
+ continue;
95
+ }
96
+ console.error('Error at ' + id);
97
+ throw e;
98
+ }
99
+ }
100
+ });
101
+
102
+ console.log('Cleared ' + c + ' deleted orders');
103
+
104
+ // Clear all cached order numbers
105
+ WebshopCounter.clearAll();
106
+
107
+ // Do something here
108
+ return Promise.resolve();
109
+ });
@@ -0,0 +1,30 @@
1
+ import { Database, Migration } from '@simonbackx/simple-database';
2
+ import { BalanceItemStatus } from '@stamhoofd/structures';
3
+
4
+ export default new Migration(async () => {
5
+ if (STAMHOOFD.environment === 'test') {
6
+ console.log('skipped in tests');
7
+ return;
8
+ }
9
+
10
+ const query = `
11
+ UPDATE
12
+ balance_items
13
+ SET status = ?
14
+ WHERE status IN (?)`;
15
+ await Database.update(query, [
16
+ BalanceItemStatus.Due,
17
+ ['Paid', 'Pending'],
18
+ ]);
19
+
20
+ const q2 = `
21
+ UPDATE
22
+ balance_items
23
+ SET status = ?,
24
+ amount = coalesce(nullif(ROUND(coalesce(pricePaid / nullif(unitPrice, 0), 0)), 0), 1)
25
+ WHERE amount = 0 AND status = ?`;
26
+ await Database.update(q2, [
27
+ BalanceItemStatus.Canceled,
28
+ BalanceItemStatus.Due,
29
+ ]);
30
+ });
@@ -0,0 +1,40 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { logger } from '@simonbackx/simple-logging';
3
+ import { BalanceItem } from '@stamhoofd/models';
4
+
5
+ export default new Migration(async () => {
6
+ if (STAMHOOFD.environment == 'test') {
7
+ console.log('skipped in tests');
8
+ return;
9
+ }
10
+
11
+ process.stdout.write('\n');
12
+ let c = 0;
13
+ let id: string = '';
14
+
15
+ await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
16
+ while (true) {
17
+ const items = await BalanceItem.where({
18
+ id: {
19
+ value: id,
20
+ sign: '>',
21
+ },
22
+ }, { limit: 1000, sort: ['id'] });
23
+
24
+ await BalanceItem.updateOutstanding(items);
25
+
26
+ c += items.length;
27
+ process.stdout.write('.');
28
+
29
+ if (items.length < 1000) {
30
+ break;
31
+ }
32
+ id = items[items.length - 1].id;
33
+ }
34
+ });
35
+
36
+ console.log('Updated outstanding balance for ' + c + ' items');
37
+
38
+ // Do something here
39
+ return Promise.resolve();
40
+ });
@@ -7,16 +7,20 @@ type Loaded<T> = (T) extends ManyToOneRelation<infer Key, infer Model> ? Record<
7
7
 
8
8
  export const BalanceItemPaymentService = {
9
9
  async markPaid(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
10
+ const wasPaid = balanceItemPayment.balanceItem.priceOpen === 0;
11
+
10
12
  // Update cached amountPaid of the balance item (balanceItemPayment will get overwritten later, but we need it to calculate the status)
11
13
  balanceItemPayment.balanceItem.pricePaid += balanceItemPayment.price;
12
14
 
13
- // Update status
14
- const old = balanceItemPayment.balanceItem.status;
15
- balanceItemPayment.balanceItem.updateStatus();
15
+ if (balanceItemPayment.balanceItem.status === BalanceItemStatus.Hidden && balanceItemPayment.balanceItem.pricePaid !== 0) {
16
+ balanceItemPayment.balanceItem.status = BalanceItemStatus.Due;
17
+ }
18
+
16
19
  await balanceItemPayment.balanceItem.save();
20
+ const isPaid = balanceItemPayment.balanceItem.priceOpen === 0;
17
21
 
18
22
  // Do logic of balance item
19
- if (balanceItemPayment.balanceItem.status === BalanceItemStatus.Paid && old !== BalanceItemStatus.Paid && balanceItemPayment.price >= 0) {
23
+ if (isPaid && !wasPaid && balanceItemPayment.price >= 0 && balanceItemPayment.balanceItem.status === BalanceItemStatus.Due) {
20
24
  // Only call markPaid once (if it wasn't (partially) paid before)
21
25
  await BalanceItemService.markPaid(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
22
26
  }
@@ -10,4 +10,5 @@ export const receivableBalanceFilterCompilers: SQLFilterDefinitions = {
10
10
  objectType: createSQLColumnFilterCompiler('objectType'),
11
11
  amount: createSQLColumnFilterCompiler('amount'),
12
12
  amountPending: createSQLColumnFilterCompiler('amountPending'),
13
+ nextDueAt: createSQLColumnFilterCompiler('nextDueAt'),
13
14
  };