@stamhoofd/models 2.106.0 → 2.107.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 (64) hide show
  1. package/dist/src/factories/GroupFactory.js +1 -1
  2. package/dist/src/factories/GroupFactory.js.map +1 -1
  3. package/dist/src/migrations/1762521336-did-send-peppol.sql +6 -0
  4. package/dist/src/migrations/1763216321-multiply-balance-item-payments.sql +1 -0
  5. package/dist/src/migrations/1763216322-multiply-balance-items.sql +6 -0
  6. package/dist/src/migrations/1763216323-bigint-cached-balances.sql +5 -0
  7. package/dist/src/migrations/1763216324-multiply-cached-balances.sql +6 -0
  8. package/dist/src/migrations/1763216325-multiply-member-platform-memberships.sql +4 -0
  9. package/dist/src/migrations/1763216326-multiply-payments.sql +8 -0
  10. package/dist/src/migrations/1763216327-multiply-register-codes.sql +4 -0
  11. package/dist/src/migrations/1763216328-multiply-credits.sql +3 -0
  12. package/dist/src/migrations/1763216329-multiply-orders.sql +3 -0
  13. package/dist/src/migrations/1763216330-multiply-uitpas-numbers.sql +4 -0
  14. package/dist/src/migrations/1763216331-balance-item-vat-percentages.sql +4 -0
  15. package/dist/src/migrations/1763216332-balance-item-price-total.sql +2 -0
  16. package/dist/src/migrations/1763216333-balance-item-price-total-fill.sql +3 -0
  17. package/dist/src/models/BalanceItem.d.ts +80 -2
  18. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  19. package/dist/src/models/BalanceItem.js +124 -4
  20. package/dist/src/models/BalanceItem.js.map +1 -1
  21. package/dist/src/models/EmailVerificationCode.js +1 -1
  22. package/dist/src/models/EmailVerificationCode.js.map +1 -1
  23. package/dist/src/models/Event.d.ts.map +1 -1
  24. package/dist/src/models/Event.js.map +1 -1
  25. package/dist/src/models/Order.d.ts +6 -0
  26. package/dist/src/models/Order.d.ts.map +1 -1
  27. package/dist/src/models/Order.js +6 -2
  28. package/dist/src/models/Order.js.map +1 -1
  29. package/dist/src/models/PayconiqPayment.js +2 -2
  30. package/dist/src/models/PayconiqPayment.js.map +1 -1
  31. package/dist/src/models/Payment.d.ts +3 -2
  32. package/dist/src/models/Payment.d.ts.map +1 -1
  33. package/dist/src/models/Payment.js +3 -0
  34. package/dist/src/models/Payment.js.map +1 -1
  35. package/dist/src/models/STInvoice.d.ts +1 -0
  36. package/dist/src/models/STInvoice.d.ts.map +1 -1
  37. package/dist/src/models/STInvoice.js +4 -0
  38. package/dist/src/models/STInvoice.js.map +1 -1
  39. package/dist/src/models/STPackage.js +2 -2
  40. package/dist/src/models/STPackage.js.map +1 -1
  41. package/package.json +4 -4
  42. package/src/factories/GroupFactory.ts +1 -1
  43. package/src/migrations/1762521336-did-send-peppol.sql +6 -0
  44. package/src/migrations/1763216321-multiply-balance-item-payments.sql +1 -0
  45. package/src/migrations/1763216322-multiply-balance-items.sql +6 -0
  46. package/src/migrations/1763216323-bigint-cached-balances.sql +5 -0
  47. package/src/migrations/1763216324-multiply-cached-balances.sql +6 -0
  48. package/src/migrations/1763216325-multiply-member-platform-memberships.sql +4 -0
  49. package/src/migrations/1763216326-multiply-payments.sql +8 -0
  50. package/src/migrations/1763216327-multiply-register-codes.sql +4 -0
  51. package/src/migrations/1763216328-multiply-credits.sql +3 -0
  52. package/src/migrations/1763216329-multiply-orders.sql +3 -0
  53. package/src/migrations/1763216330-multiply-uitpas-numbers.sql +4 -0
  54. package/src/migrations/1763216331-balance-item-vat-percentages.sql +4 -0
  55. package/src/migrations/1763216332-balance-item-price-total.sql +2 -0
  56. package/src/migrations/1763216333-balance-item-price-total-fill.sql +3 -0
  57. package/src/models/BalanceItem.ts +129 -5
  58. package/src/models/EmailVerificationCode.ts +1 -1
  59. package/src/models/Event.ts +2 -1
  60. package/src/models/Order.ts +6 -2
  61. package/src/models/PayconiqPayment.ts +2 -2
  62. package/src/models/Payment.ts +5 -1
  63. package/src/models/STInvoice.ts +3 -0
  64. package/src/models/STPackage.ts +2 -2
@@ -1,5 +1,5 @@
1
1
  import { column, Database } from '@simonbackx/simple-database';
2
- import { BalanceItemPaymentWithPayment, BalanceItemPaymentWithPrivatePayment, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, BalanceItemWithPayments, BalanceItemWithPrivatePayments, Payment as PaymentStruct, PrivatePayment } from '@stamhoofd/structures';
2
+ import { BalanceItemPaymentWithPayment, BalanceItemPaymentWithPrivatePayment, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, BalanceItemWithPayments, BalanceItemWithPrivatePayments, Payment as PaymentStruct, PrivatePayment, VATExcemptReason } from '@stamhoofd/structures';
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
 
@@ -72,25 +72,81 @@ export class BalanceItem extends QueryableModel {
72
72
  amount = 1;
73
73
 
74
74
  /**
75
- * Total prices
75
+ * Price per piece
76
+ *
77
+ * NOTE: We store an integer of the price up to 4 digits after the comma.
78
+ * 1 euro = 10000.
79
+ * 0,01 euro = 100
80
+ * 0,0001 euro = 1
81
+ *
82
+ * This is required for correct VAT calculations without intermediate rounding.
76
83
  */
77
84
  @column({ type: 'integer' })
78
85
  unitPrice: number;
79
86
 
87
+ @column({ type: 'integer', nullable: true })
88
+ VATPercentage: number | null = null;
89
+
90
+ @column({ type: 'boolean' })
91
+ VATIncluded = true;
92
+
93
+ /**
94
+ * Whether there is a VAT excempt reason.
95
+ * Note: keep the original VAT in these cases. On time of payment or invoicing, the VAT excemption will be revalidated.
96
+ * If that fails, we can still charge the VAT.
97
+ */
98
+ @column({ type: 'string', nullable: true })
99
+ VATExcempt: VATExcemptReason | null = null;
100
+
101
+ /**
102
+ * This is a cached value for storing in the database.
103
+ * It stores the calculated price with VAT.
104
+ *
105
+ * price should = pricePaid + pricePending + priceOpen
106
+ */
107
+ @column({
108
+ type: 'integer',
109
+ beforeSave: function () {
110
+ return this.priceWithVAT;
111
+ },
112
+ })
113
+ priceTotal = 0;
114
+
80
115
  /**
81
116
  * Cached value, for optimizations
117
+ *
118
+ * NOTE: We store an integer of the price up to 4 digits after the comma.
119
+ * 1 euro = 10000.
120
+ * 0,01 euro = 100
121
+ * 0,0001 euro = 1
122
+ *
123
+ * This is required for correct VAT calculations without intermediate rounding.
82
124
  */
83
125
  @column({ type: 'integer' })
84
126
  pricePaid = 0;
85
127
 
86
128
  /**
87
129
  * Cached value, for optimizations
130
+ *
131
+ * NOTE: We store an integer of the price up to 4 digits after the comma.
132
+ * 1 euro = 10000.
133
+ * 0,01 euro = 100
134
+ * 0,0001 euro = 1
135
+ *
136
+ * This is required for correct VAT calculations without intermediate rounding.
88
137
  */
89
138
  @column({ type: 'integer' })
90
139
  pricePending = 0;
91
140
 
92
141
  /**
93
142
  * Cached value, for optimizations
143
+ *
144
+ * NOTE: We store an integer of the price up to 4 digits after the comma.
145
+ * 1 euro = 10000.
146
+ * 0,01 euro = 100
147
+ * 0,0001 euro = 1
148
+ *
149
+ * This is required for correct VAT calculations without intermediate rounding.
94
150
  */
95
151
  @column({
96
152
  type: 'integer',
@@ -143,7 +199,67 @@ export class BalanceItem extends QueryableModel {
143
199
  })
144
200
  updatedAt: Date;
145
201
 
202
+ /**
203
+ * @deprecated: use priceWithVAT
204
+ * NOTE: This contains an integer of the price up to 4 digits after the comma.
205
+ * 1 euro = 10000.
206
+ * 0,01 euro = 100
207
+ * 0,0001 euro = 1
208
+ *
209
+ * This is required for correct VAT calculations without intermediate rounding.
210
+ */
146
211
  get price() {
212
+ return this.priceWithVAT;
213
+ }
214
+
215
+ /**
216
+ * Difference here is that when the VAT is excempt, this is still set, while VAT will be zero.
217
+ */
218
+ get calculatedVAT() {
219
+ if (!this.VATPercentage) {
220
+ // VAT percentage not set, so treat as 0%
221
+ return 0;
222
+ }
223
+
224
+ if (this.VATIncluded) {
225
+ // Calculate VAT on price incl. VAT, which is not 100% correct and causes roudning issues
226
+ return this.unitPrice * this.amount - Math.round(this.unitPrice * this.amount * 100 / (100 + this.VATPercentage));
227
+ }
228
+
229
+ // Note: the rounding is only to avoid floating point errors in software, this should not cause any actual rounding
230
+ // That is the reason why we store it up to 4 digits after comma
231
+ return Math.round(this.VATPercentage * this.unitPrice * this.amount / 100);
232
+ }
233
+
234
+ /**
235
+ * Note, this is not 100% accurate.
236
+ * Legally we most often need to calculate the VAT on invoice level and round it there.
237
+ * Technically we cannot pass infinite accurate numbers around in a system to avoid rounding. The returned number is
238
+ * therefore rounded up to 4 digits after the comma. On normal amounts, with only 2 digits after the comma, this won't lose accuracy.
239
+ * So the VAT calculation needs to happen at the end again before payment.
240
+ */
241
+ get VAT() {
242
+ if (this.VATExcempt) {
243
+ // Exempt from VAT
244
+ return 0;
245
+ }
246
+
247
+ return this.calculatedVAT;
248
+ }
249
+
250
+ get priceWithVAT() {
251
+ return this.priceWithoutVAT + this.VAT;
252
+ }
253
+
254
+ /**
255
+ * Note: when the VAT is already included, the result of this will be unreliable because of rounding issues.
256
+ * Do not use this in calculations!
257
+ */
258
+ get priceWithoutVAT() {
259
+ if (this.VATIncluded) {
260
+ return this.unitPrice * this.amount - this.calculatedVAT;
261
+ }
262
+
147
263
  return this.unitPrice * this.amount;
148
264
  }
149
265
 
@@ -168,18 +284,26 @@ export class BalanceItem extends QueryableModel {
168
284
  return this.isAfterDueDate;
169
285
  }
170
286
 
287
+ /**
288
+ * NOTE: This contains an integer of the price up to 4 digits after the comma.
289
+ * 1 euro = 10000.
290
+ * 0,01 euro = 100
291
+ * 0,0001 euro = 1
292
+ *
293
+ * This is required for correct VAT calculations without intermediate rounding.
294
+ */
171
295
  get calculatedPriceOpen() {
172
296
  if (this.status !== BalanceItemStatus.Due) {
173
297
  return -this.pricePaid - this.pricePending;
174
298
  }
175
- return this.price - this.pricePaid - this.pricePending;
299
+ return this.priceWithVAT - this.pricePaid - this.pricePending;
176
300
  }
177
301
 
178
302
  /**
179
303
  * price minus pricePaid
180
304
  */
181
305
  get priceUnpaid() {
182
- return this.price - this.pricePaid;
306
+ return this.priceWithVAT - this.pricePaid;
183
307
  }
184
308
 
185
309
  get isPaid() {
@@ -374,7 +498,7 @@ export class BalanceItem extends QueryableModel {
374
498
  SET balance_items.pricePaid = coalesce(paid.price, 0),
375
499
  balance_items.pricePending = coalesce(pending.price, 0),
376
500
  balance_items.priceOpen = (CASE
377
- WHEN balance_items.status = '${BalanceItemStatus.Due}' THEN (balance_items.unitPrice * balance_items.amount - balance_items.pricePaid - balance_items.pricePending)
501
+ WHEN balance_items.status = '${BalanceItemStatus.Due}' THEN (balance_items.priceTotal - balance_items.pricePaid - balance_items.pricePending)
378
502
  ELSE (-balance_items.pricePaid - balance_items.pricePending)
379
503
  END)
380
504
  ${secondWhere}`;
@@ -222,7 +222,7 @@ export class EmailVerificationCode extends QueryableModel {
222
222
  });
223
223
  }
224
224
 
225
- if (verificationCode.code === code || (code === '111111' && STAMHOOFD.environment === 'development')) {
225
+ if (verificationCode.code === code || (code === '111111' && (STAMHOOFD.environment === 'development' || STAMHOOFD.environment === 'test'))) {
226
226
  // Delete all remaining information!
227
227
  // To avoid leaving information about the existince of this user (tries)
228
228
  await verificationCode.delete();
@@ -118,7 +118,8 @@ export class Event extends QueryableModel {
118
118
  if (waitingList) {
119
119
  if (group.settings.allowRegistrationsByOrganization) {
120
120
  waitingList.settings.allowRegistrationsByOrganization = true;
121
- } else {
121
+ }
122
+ else {
122
123
  waitingList.settings.allowRegistrationsByOrganization = false;
123
124
  }
124
125
  await this.syncGroupRequirements(waitingList);
@@ -67,12 +67,17 @@ export class Order extends QueryableModel {
67
67
  @column({ type: 'string' })
68
68
  status = OrderStatus.Created;
69
69
 
70
- // #region generated columns for filtering
70
+ /**
71
+ * Cached value for faster in database filtering
72
+ */
71
73
  @column({ type: 'integer', nullable: true, beforeSave(this: Order) {
72
74
  return this.data.totalPrice;
73
75
  } })
74
76
  totalPrice: number = 0;
75
77
 
78
+ /**
79
+ * Cached value for faster in database filtering
80
+ */
76
81
  @column({ type: 'integer', nullable: true, beforeSave(this: Order) {
77
82
  return this.data.amount;
78
83
  } })
@@ -82,7 +87,6 @@ export class Order extends QueryableModel {
82
87
  return this.data.timeSlot?.timeIndex ?? null;
83
88
  } })
84
89
  timeSlotTime: string | null = null;
85
- // #endregion
86
90
 
87
91
  static webshop = new ManyToOneRelation(Webshop, 'webshop');
88
92
  static payment = new ManyToOneRelation(Payment, 'payment');
@@ -92,7 +92,7 @@ export class PayconiqPayment extends QueryableModel {
92
92
 
93
93
  // Check and save amount
94
94
  if (typeof response.amount === 'number' && status === PaymentStatus.Succeeded) {
95
- const amount = response.amount;
95
+ const amount = response.amount * 100; // Convert from integer with 2 decimal places to 4 places
96
96
  if (amount < payment.price) {
97
97
  // We do NOT allow lower prices
98
98
  console.error('Manual price change detected by Payconiq user. Failing the payment');
@@ -157,7 +157,7 @@ export class PayconiqPayment extends QueryableModel {
157
157
 
158
158
  const response = await this.request('POST', '/v3/payments', {
159
159
  reference: payment.id.replaceAll('-', ''), // 36 chars, max length is 35.... The actual id is also in the description
160
- amount: payment.price,
160
+ amount: Math.round(payment.price / 100), // 4 decimals to 2 decimals
161
161
  currency: 'EUR',
162
162
  callbackUrl: callbackUrl ?? 'https://' + organization.getApiHost() + '/v' + Version + '/payments/' + encodeURIComponent(payment.id) + '?exchange=true',
163
163
  returnUrl,
@@ -3,7 +3,7 @@ import { BalanceItemDetailed, BalanceItemPaymentDetailed, PaymentCustomer, Payme
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
 
6
- import { Organization } from './';
6
+ import { BalanceItem, Organization } from './';
7
7
  import { QueryableModel } from '@stamhoofd/sql';
8
8
 
9
9
  export class Payment extends QueryableModel {
@@ -165,6 +165,10 @@ export class Payment extends QueryableModel {
165
165
  this.transferDescription = settings.generateDescription(reference, organization.address.country, replacements);
166
166
  }
167
167
 
168
+ static roundPrice(price: number) {
169
+ return Math.round(price / 100) * 100;
170
+ }
171
+
168
172
  static async getGeneralStructure(payments: Payment[], includeSettlements = false): Promise<PaymentGeneral[]> {
169
173
  if (payments.length === 0) {
170
174
  return [];
@@ -81,6 +81,9 @@ export class STInvoice extends QueryableModel {
81
81
  @column({ type: 'string', nullable: true })
82
82
  reference: string | null = null;
83
83
 
84
+ @column({ type: 'boolean' })
85
+ didSendPeppol = false;
86
+
84
87
  // static organization = new ManyToOneRelation(Organization, 'organization');
85
88
  // static payment = new ManyToOneRelation(Payment, 'payment');
86
89
 
@@ -194,7 +194,7 @@ export class STPackage extends QueryableModel {
194
194
  pack.meta.serviceFeeFixed = 0;
195
195
  pack.meta.serviceFeePercentage = 2_00;
196
196
  pack.meta.serviceFeeMinimum = 0;
197
- pack.meta.serviceFeeMaximum = 20;
197
+ pack.meta.serviceFeeMaximum = 2000; // 20 cent
198
198
 
199
199
  pack.meta.unitPrice = 0;
200
200
  pack.meta.pricingType = STPricingType.Fixed;
@@ -207,7 +207,7 @@ export class STPackage extends QueryableModel {
207
207
  pack.meta.serviceFeeMinimum = 0;
208
208
  pack.meta.serviceFeeMaximum = 0;
209
209
 
210
- pack.meta.unitPrice = 100;
210
+ pack.meta.unitPrice = 1_0000; // 1 euro
211
211
  pack.meta.pricingType = STPricingType.PerMember;
212
212
  }
213
213