@stamhoofd/models 2.62.0 → 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/dist/src/migrations/1733909398-balance-items-due-at.sql +2 -0
- package/dist/src/migrations/1733909399-recalculate-at.sql +3 -0
- package/dist/src/migrations/1733994454-balance-item-price-open.sql +2 -0
- package/dist/src/migrations/1734084689-payment-type.sql +2 -0
- package/dist/src/migrations/1734084690-move-payment-column-order.sql +2 -0
- package/dist/src/migrations/1734084691-fill-payment-type-refunds.sql +1 -0
- package/dist/src/migrations/1734084692-fill-payment-type-reallocations.sql +15 -0
- package/dist/src/models/BalanceItem.d.ts +17 -4
- package/dist/src/models/BalanceItem.d.ts.map +1 -1
- package/dist/src/models/BalanceItem.js +59 -59
- package/dist/src/models/BalanceItem.js.map +1 -1
- package/dist/src/models/CachedBalance.d.ts +7 -0
- package/dist/src/models/CachedBalance.d.ts.map +1 -1
- package/dist/src/models/CachedBalance.js +70 -7
- package/dist/src/models/CachedBalance.js.map +1 -1
- package/dist/src/models/DocumentTemplate.js +1 -1
- package/dist/src/models/DocumentTemplate.js.map +1 -1
- package/dist/src/models/Order.d.ts +1 -0
- package/dist/src/models/Order.d.ts.map +1 -1
- package/dist/src/models/Order.js +6 -0
- package/dist/src/models/Order.js.map +1 -1
- package/dist/src/models/Payment.d.ts +11 -1
- package/dist/src/models/Payment.d.ts.map +1 -1
- package/dist/src/models/Payment.js +13 -0
- package/dist/src/models/Payment.js.map +1 -1
- package/package.json +2 -2
- package/src/migrations/1733909398-balance-items-due-at.sql +2 -0
- package/src/migrations/1733909399-recalculate-at.sql +3 -0
- package/src/migrations/1733994454-balance-item-price-open.sql +2 -0
- package/src/migrations/1734084689-payment-type.sql +2 -0
- package/src/migrations/1734084690-move-payment-column-order.sql +2 -0
- package/src/migrations/1734084691-fill-payment-type-refunds.sql +1 -0
- package/src/migrations/1734084692-fill-payment-type-reallocations.sql +15 -0
- package/src/models/BalanceItem.ts +61 -70
- package/src/models/CachedBalance.ts +105 -30
- package/src/models/DocumentTemplate.ts +1 -1
- package/src/models/Order.ts +7 -0
- package/src/models/Payment.ts +13 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { column, Database, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
2
|
-
import { BalanceItemPaymentWithPayment, BalanceItemPaymentWithPrivatePayment, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, BalanceItemWithPrivatePayments, OrderStatus, Payment as PaymentStruct, PrivatePayment } from '@stamhoofd/structures';
|
|
2
|
+
import { BalanceItem as BalanceItemStruct, BalanceItemPaymentWithPayment, BalanceItemPaymentWithPrivatePayment, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, BalanceItemWithPrivatePayments, OrderStatus, Payment as PaymentStruct, PrivatePayment } from '@stamhoofd/structures';
|
|
3
3
|
import { Formatter } from '@stamhoofd/utility';
|
|
4
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
5
|
|
|
@@ -90,8 +90,31 @@ export class BalanceItem extends Model {
|
|
|
90
90
|
@column({ type: 'integer' })
|
|
91
91
|
pricePending = 0;
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Cached value, for optimizations
|
|
95
|
+
*/
|
|
96
|
+
@column({
|
|
97
|
+
type: 'integer',
|
|
98
|
+
beforeSave: function () {
|
|
99
|
+
return this.calculatedPriceOpen;
|
|
100
|
+
},
|
|
101
|
+
skipUpdate: true,
|
|
102
|
+
})
|
|
103
|
+
priceOpen = 0;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* todo: deprecate ('pending' and 'paid') + 'hidden' status and replace with 'due' + 'hidden'
|
|
107
|
+
* -> maybe add 'due' (due if dueAt is null or <= now), 'hidden' (never due), 'future' (= not due until dueAt - but not possible to pay earlier)
|
|
108
|
+
*/
|
|
93
109
|
@column({ type: 'string' })
|
|
94
|
-
status = BalanceItemStatus.
|
|
110
|
+
status = BalanceItemStatus.Due;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* In case the balance item doesn't have to be paid immediately, we can set a due date.
|
|
114
|
+
* When the due date is reached, it is set to null and the cached balance is updated.
|
|
115
|
+
*/
|
|
116
|
+
@column({ type: 'datetime', nullable: true })
|
|
117
|
+
dueAt: Date | null = null;
|
|
95
118
|
|
|
96
119
|
@column({
|
|
97
120
|
type: 'datetime', beforeSave(old?: any) {
|
|
@@ -119,14 +142,13 @@ export class BalanceItem extends Model {
|
|
|
119
142
|
return this.unitPrice * this.amount;
|
|
120
143
|
}
|
|
121
144
|
|
|
122
|
-
get
|
|
145
|
+
get calculatedPriceOpen() {
|
|
146
|
+
if (this.status !== BalanceItemStatus.Due) {
|
|
147
|
+
return -this.pricePaid - this.pricePending;
|
|
148
|
+
}
|
|
123
149
|
return this.price - this.pricePaid - this.pricePending;
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
updateStatus() {
|
|
127
|
-
this.status = (this.pricePaid !== 0 || this.price === 0) && this.pricePaid >= this.price ? BalanceItemStatus.Paid : (this.pricePaid !== 0 ? BalanceItemStatus.Pending : (this.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
152
|
static async deleteItems(items: BalanceItem[]) {
|
|
131
153
|
// Find depending items
|
|
132
154
|
const dependingItemIds = Formatter.uniqueArray(items.filter(i => !!i.dependingBalanceItemId).map(i => i.dependingBalanceItemId!)).filter(id => !items.some(item => item.id === id));
|
|
@@ -137,30 +159,19 @@ export class BalanceItem extends Model {
|
|
|
137
159
|
items = [...items, ...dependingItems];
|
|
138
160
|
}
|
|
139
161
|
|
|
140
|
-
const { balanceItemPayments } = await BalanceItem.loadPayments(items);
|
|
141
|
-
|
|
142
162
|
// todo: in the future we could automatically delete payments that are not needed anymore and weren't paid yet -> to prevent leaving ghost payments
|
|
143
163
|
// for now, an admin can manually cancel those payments
|
|
144
164
|
let needsUpdate = false;
|
|
145
165
|
|
|
146
166
|
// Set other items to zero (the balance item payments keep the real price)
|
|
147
167
|
for (const item of items) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Don't change status of items that are already paid or are partially paid
|
|
151
|
-
// Not using item.paidPrice, since this is cached
|
|
152
|
-
const bip = balanceItemPayments.filter(p => p.balanceItemId === item.id);
|
|
153
|
-
|
|
154
|
-
if (bip.length === 0) {
|
|
155
|
-
// No payments associated with this item
|
|
156
|
-
item.status = BalanceItemStatus.Hidden;
|
|
157
|
-
item.amount = 0;
|
|
158
|
-
await item.save();
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
item.amount = 0;
|
|
162
|
-
await item.save();
|
|
168
|
+
if (item.status !== BalanceItemStatus.Due) {
|
|
169
|
+
continue;
|
|
163
170
|
}
|
|
171
|
+
|
|
172
|
+
needsUpdate = true;
|
|
173
|
+
item.status = BalanceItemStatus.Canceled;
|
|
174
|
+
await item.save();
|
|
164
175
|
}
|
|
165
176
|
|
|
166
177
|
if (needsUpdate) {
|
|
@@ -172,7 +183,7 @@ export class BalanceItem extends Model {
|
|
|
172
183
|
let needsUpdate = false;
|
|
173
184
|
for (const item of items) {
|
|
174
185
|
if (item.status === BalanceItemStatus.Hidden) {
|
|
175
|
-
item.status = BalanceItemStatus.
|
|
186
|
+
item.status = BalanceItemStatus.Due;
|
|
176
187
|
needsUpdate = true;
|
|
177
188
|
await item.save();
|
|
178
189
|
}
|
|
@@ -225,7 +236,6 @@ export class BalanceItem extends Model {
|
|
|
225
236
|
console.log('Update outstanding balance for', items.length, 'items');
|
|
226
237
|
|
|
227
238
|
await BalanceItem.updatePricePaid(items.map(i => i.id));
|
|
228
|
-
await BalanceItem.updatePricePending(items.map(i => i.id));
|
|
229
239
|
|
|
230
240
|
// Deprecated: the member balances have moved to CachedBalance
|
|
231
241
|
// Update outstanding amount of related members and registrations
|
|
@@ -264,7 +274,7 @@ export class BalanceItem extends Model {
|
|
|
264
274
|
* Update the outstanding balance of multiple members in one go (or all members)
|
|
265
275
|
*/
|
|
266
276
|
static async updatePricePaid(balanceItemIds: string[] | 'all') {
|
|
267
|
-
if (balanceItemIds !== 'all' && balanceItemIds.length
|
|
277
|
+
if (balanceItemIds !== 'all' && balanceItemIds.length === 0) {
|
|
268
278
|
return;
|
|
269
279
|
}
|
|
270
280
|
|
|
@@ -275,13 +285,13 @@ export class BalanceItem extends Model {
|
|
|
275
285
|
if (balanceItemIds !== 'all') {
|
|
276
286
|
firstWhere = ` AND balanceItemId IN (?)`;
|
|
277
287
|
params.push(balanceItemIds);
|
|
288
|
+
params.push(balanceItemIds);
|
|
278
289
|
|
|
279
290
|
secondWhere = `WHERE balance_items.id IN (?)`;
|
|
280
291
|
params.push(balanceItemIds);
|
|
281
292
|
}
|
|
282
293
|
|
|
283
|
-
// Note:
|
|
284
|
-
// this method will set the balance item to paid once, and only call the handlers once
|
|
294
|
+
// Note: this query only works in MySQL because of the SET assignment behaviour allowing to reference newly set values
|
|
285
295
|
const query = `
|
|
286
296
|
UPDATE
|
|
287
297
|
balance_items
|
|
@@ -296,41 +306,7 @@ export class BalanceItem extends Model {
|
|
|
296
306
|
payments.status = 'Succeeded'${firstWhere}
|
|
297
307
|
GROUP BY
|
|
298
308
|
balanceItemId
|
|
299
|
-
)
|
|
300
|
-
SET balance_items.pricePaid = coalesce(i.price, 0), balance_items.status = (CASE
|
|
301
|
-
WHEN balance_items.status = '${BalanceItemStatus.Paid}' AND balance_items.pricePaid != 0 AND balance_items.pricePaid >= (balance_items.unitPrice * balance_items.amount) THEN '${BalanceItemStatus.Paid}'
|
|
302
|
-
WHEN balance_items.pricePaid != 0 THEN '${BalanceItemStatus.Pending}'
|
|
303
|
-
WHEN balance_items.status = '${BalanceItemStatus.Hidden}' THEN '${BalanceItemStatus.Hidden}'
|
|
304
|
-
ELSE '${BalanceItemStatus.Pending}'
|
|
305
|
-
END)
|
|
306
|
-
${secondWhere}`;
|
|
307
|
-
|
|
308
|
-
await Database.update(query, params);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Call this after updating the pricePaid
|
|
313
|
-
*/
|
|
314
|
-
static async updatePricePending(balanceItemIds: string[] | 'all') {
|
|
315
|
-
if (balanceItemIds !== 'all' && balanceItemIds.length == 0) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const params: any[] = [];
|
|
320
|
-
let firstWhere = '';
|
|
321
|
-
let secondWhere = '';
|
|
322
|
-
|
|
323
|
-
if (balanceItemIds !== 'all') {
|
|
324
|
-
firstWhere = ` AND balanceItemId IN (?)`;
|
|
325
|
-
params.push(balanceItemIds);
|
|
326
|
-
|
|
327
|
-
secondWhere = `WHERE balance_items.id IN (?)`;
|
|
328
|
-
params.push(balanceItemIds);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const query = `
|
|
332
|
-
UPDATE
|
|
333
|
-
balance_items
|
|
309
|
+
) paid ON paid.balanceItemId = balance_items.id
|
|
334
310
|
LEFT JOIN (
|
|
335
311
|
SELECT
|
|
336
312
|
balanceItemId,
|
|
@@ -342,16 +318,25 @@ export class BalanceItem extends Model {
|
|
|
342
318
|
payments.status != 'Succeeded' AND payments.status != 'Failed'${firstWhere}
|
|
343
319
|
GROUP BY
|
|
344
320
|
balanceItemId
|
|
345
|
-
)
|
|
346
|
-
SET balance_items.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
321
|
+
) pending ON pending.balanceItemId = balance_items.id
|
|
322
|
+
SET balance_items.pricePaid = coalesce(paid.price, 0),
|
|
323
|
+
balance_items.pricePending = coalesce(pending.price, 0),
|
|
324
|
+
balance_items.priceOpen = (CASE
|
|
325
|
+
WHEN balance_items.status = '${BalanceItemStatus.Due}' THEN (balance_items.unitPrice * balance_items.amount - balance_items.pricePaid - balance_items.pricePending)
|
|
326
|
+
ELSE (-balance_items.pricePaid - balance_items.pricePending)
|
|
327
|
+
END)
|
|
350
328
|
${secondWhere}`;
|
|
351
329
|
|
|
352
330
|
await Database.update(query, params);
|
|
353
331
|
}
|
|
354
332
|
|
|
333
|
+
/**
|
|
334
|
+
* @deprecated
|
|
335
|
+
*/
|
|
336
|
+
static async updatePricePending(balanceItemIds: string[] | 'all') {
|
|
337
|
+
// deprecated
|
|
338
|
+
}
|
|
339
|
+
|
|
355
340
|
static async loadPayments(items: BalanceItem[]) {
|
|
356
341
|
if (items.length == 0) {
|
|
357
342
|
return { balanceItemPayments: [], payments: [] };
|
|
@@ -366,8 +351,14 @@ export class BalanceItem extends Model {
|
|
|
366
351
|
return { payments, balanceItemPayments };
|
|
367
352
|
}
|
|
368
353
|
|
|
354
|
+
getStructure() {
|
|
355
|
+
return BalanceItemStruct.create({
|
|
356
|
+
...this,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
369
360
|
static async getStructureWithPayments(items: BalanceItem[]): Promise<BalanceItemWithPayments[]> {
|
|
370
|
-
if (items.length
|
|
361
|
+
if (items.length === 0) {
|
|
371
362
|
return [];
|
|
372
363
|
}
|
|
373
364
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { column, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
2
|
-
import { SQL, SQLAlias, SQLCalculation, SQLMinusSign, SQLMultiplicationSign, SQLSelect, SQLSelectAs, SQLSum, SQLWhere, SQLWhereSign } from '@stamhoofd/sql';
|
|
3
|
-
import { BalanceItemStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
2
|
+
import { SQL, SQLAlias, SQLCalculation, SQLGreatest, SQLMin, SQLMinusSign, SQLMultiplicationSign, SQLSelect, SQLSelectAs, SQLSum, SQLWhere, SQLWhereSign } from '@stamhoofd/sql';
|
|
3
|
+
import { BalanceItem as BalanceItemStruct, BalanceItemStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
4
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
5
|
import { BalanceItem } from './BalanceItem';
|
|
6
6
|
|
|
@@ -35,9 +35,18 @@ export class CachedBalance extends Model {
|
|
|
35
35
|
@column({ type: 'integer' })
|
|
36
36
|
amount = 0;
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The sum of unconfirmed payments
|
|
40
|
+
*/
|
|
38
41
|
@column({ type: 'integer' })
|
|
39
42
|
amountPending = 0;
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* This is the minimum `dueAt` that lies in the future of all **unpaid** balance items connected to this object.
|
|
46
|
+
*/
|
|
47
|
+
@column({ type: 'datetime', nullable: true })
|
|
48
|
+
nextDueAt: Date | null = null;
|
|
49
|
+
|
|
41
50
|
@column({
|
|
42
51
|
type: 'datetime', beforeSave(old?: any) {
|
|
43
52
|
if (old !== undefined) {
|
|
@@ -102,17 +111,7 @@ export class CachedBalance extends Model {
|
|
|
102
111
|
.where(columnName, objectIds)
|
|
103
112
|
.whereNot('status', BalanceItemStatus.Hidden)
|
|
104
113
|
.where(
|
|
105
|
-
SQL.where(
|
|
106
|
-
new SQLCalculation(
|
|
107
|
-
new SQLCalculation(
|
|
108
|
-
SQL.column('unitPrice'),
|
|
109
|
-
new SQLMultiplicationSign(),
|
|
110
|
-
SQL.column('amount'),
|
|
111
|
-
),
|
|
112
|
-
new SQLMinusSign(),
|
|
113
|
-
SQL.column('pricePaid'),
|
|
114
|
-
)
|
|
115
|
-
, SQLWhereSign.NotEqual, 0)
|
|
114
|
+
SQL.where(SQL.column('priceOpen'), SQLWhereSign.NotEqual, 0)
|
|
116
115
|
.or('pricePending', SQLWhereSign.NotEqual, 0),
|
|
117
116
|
);
|
|
118
117
|
|
|
@@ -124,21 +123,12 @@ export class CachedBalance extends Model {
|
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
private static async fetchForObjects(organizationId: string, objectIds: string[], columnName: string, customWhere?: SQLWhere) {
|
|
126
|
+
const dueOffset = BalanceItemStruct.getDueOffset();
|
|
127
127
|
const query = SQL.select(
|
|
128
128
|
SQL.column(columnName),
|
|
129
129
|
new SQLSelectAs(
|
|
130
|
-
new
|
|
131
|
-
|
|
132
|
-
new SQLCalculation(
|
|
133
|
-
SQL.column('unitPrice'),
|
|
134
|
-
new SQLMultiplicationSign(),
|
|
135
|
-
SQL.column('amount'),
|
|
136
|
-
),
|
|
137
|
-
),
|
|
138
|
-
new SQLMinusSign(),
|
|
139
|
-
new SQLSum(
|
|
140
|
-
SQL.column('pricePaid'),
|
|
141
|
-
),
|
|
130
|
+
new SQLSum(
|
|
131
|
+
SQL.column('priceOpen'),
|
|
142
132
|
),
|
|
143
133
|
new SQLAlias('data__amount'),
|
|
144
134
|
),
|
|
@@ -153,6 +143,7 @@ export class CachedBalance extends Model {
|
|
|
153
143
|
.where('organizationId', organizationId)
|
|
154
144
|
.where(columnName, objectIds)
|
|
155
145
|
.whereNot('status', BalanceItemStatus.Hidden)
|
|
146
|
+
.where(SQL.where('dueAt', null).or('dueAt', SQLWhereSign.LessEqual, dueOffset))
|
|
156
147
|
.groupBy(SQL.column(columnName));
|
|
157
148
|
|
|
158
149
|
if (customWhere) {
|
|
@@ -161,7 +152,40 @@ export class CachedBalance extends Model {
|
|
|
161
152
|
|
|
162
153
|
const result = await query.fetch();
|
|
163
154
|
|
|
164
|
-
|
|
155
|
+
// Calculate future due
|
|
156
|
+
const dueQuery = SQL.select(
|
|
157
|
+
SQL.column(columnName),
|
|
158
|
+
new SQLSelectAs(
|
|
159
|
+
new SQLMin(
|
|
160
|
+
SQL.column('dueAt'),
|
|
161
|
+
),
|
|
162
|
+
new SQLAlias('data__dueAt'),
|
|
163
|
+
),
|
|
164
|
+
// If the current amount_due is negative, we can ignore that negative part if there is a future due item
|
|
165
|
+
new SQLSelectAs(
|
|
166
|
+
new SQLSum(
|
|
167
|
+
SQL.column('priceOpen'),
|
|
168
|
+
),
|
|
169
|
+
new SQLAlias('data__amount'),
|
|
170
|
+
),
|
|
171
|
+
new SQLSelectAs(
|
|
172
|
+
new SQLSum(
|
|
173
|
+
SQL.column('pricePending'),
|
|
174
|
+
),
|
|
175
|
+
new SQLAlias('data__amountPending'),
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
.from(BalanceItem.table)
|
|
179
|
+
.where('organizationId', organizationId)
|
|
180
|
+
.where(columnName, objectIds)
|
|
181
|
+
.where('status', BalanceItemStatus.Due)
|
|
182
|
+
.whereNot('dueAt', null)
|
|
183
|
+
.where('dueAt', SQLWhereSign.Greater, dueOffset)
|
|
184
|
+
.groupBy(SQL.column(columnName));
|
|
185
|
+
|
|
186
|
+
const dueResult = await dueQuery.fetch();
|
|
187
|
+
|
|
188
|
+
const results: [string, { amount: number; amountPending: number; nextDueAt: Date | null }][] = [];
|
|
165
189
|
for (const row of result) {
|
|
166
190
|
if (!row['data']) {
|
|
167
191
|
throw new Error('Invalid data namespace');
|
|
@@ -187,20 +211,68 @@ export class CachedBalance extends Model {
|
|
|
187
211
|
throw new Error('Invalid amountPending');
|
|
188
212
|
}
|
|
189
213
|
|
|
190
|
-
results.push([objectId, { amount, amountPending }]);
|
|
214
|
+
results.push([objectId, { amount, amountPending, nextDueAt: null }]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const row of dueResult) {
|
|
218
|
+
if (!row['data']) {
|
|
219
|
+
throw new Error('Invalid data namespace');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!row[BalanceItem.table]) {
|
|
223
|
+
throw new Error('Invalid ' + BalanceItem.table + ' namespace');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const objectId = row[BalanceItem.table][columnName];
|
|
227
|
+
const dueAt = row['data']['dueAt'];
|
|
228
|
+
const amount = row['data']['amount'];
|
|
229
|
+
const amountPending = row['data']['amountPending'];
|
|
230
|
+
|
|
231
|
+
if (typeof objectId !== 'string') {
|
|
232
|
+
throw new Error('Invalid objectId');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!(dueAt instanceof Date)) {
|
|
236
|
+
throw new Error('Invalid dueAt');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (typeof amount !== 'number') {
|
|
240
|
+
throw new Error('Invalid amount');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (typeof amountPending !== 'number') {
|
|
244
|
+
throw new Error('Invalid amountPending');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = results.find(r => r[0] === objectId);
|
|
248
|
+
if (result) {
|
|
249
|
+
result[1].nextDueAt = dueAt;
|
|
250
|
+
|
|
251
|
+
if (result[1].amount < 0) {
|
|
252
|
+
if (amount > 0) {
|
|
253
|
+
// Let the future due amount fill in the gap until maximum 0
|
|
254
|
+
result[1].amount = Math.min(0, result[1].amount + amount);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
result[1].amountPending += amountPending;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
results.push([objectId, { amount: 0, amountPending: amountPending, nextDueAt: dueAt }]);
|
|
262
|
+
}
|
|
191
263
|
}
|
|
192
264
|
|
|
193
265
|
// Add missing object ids (with 0 amount, otherwise we don't reset the amounts back to zero when all the balance items are hidden)
|
|
194
266
|
for (const objectId of objectIds) {
|
|
195
267
|
if (!results.find(([id]) => id === objectId)) {
|
|
196
|
-
results.push([objectId, { amount: 0, amountPending: 0 }]);
|
|
268
|
+
results.push([objectId, { amount: 0, amountPending: 0, nextDueAt: null }]);
|
|
197
269
|
}
|
|
198
270
|
}
|
|
199
271
|
|
|
200
272
|
return results;
|
|
201
273
|
}
|
|
202
274
|
|
|
203
|
-
private static async setForResults(organizationId: string, result: [string, { amount: number; amountPending: number }][], objectType: ReceivableBalanceType) {
|
|
275
|
+
private static async setForResults(organizationId: string, result: [string, { amount: number; amountPending: number; nextDueAt: null | Date }][], objectType: ReceivableBalanceType) {
|
|
204
276
|
if (result.length === 0) {
|
|
205
277
|
return;
|
|
206
278
|
}
|
|
@@ -212,10 +284,11 @@ export class CachedBalance extends Model {
|
|
|
212
284
|
'objectType',
|
|
213
285
|
'amount',
|
|
214
286
|
'amountPending',
|
|
287
|
+
'nextDueAt',
|
|
215
288
|
'createdAt',
|
|
216
289
|
'updatedAt',
|
|
217
290
|
)
|
|
218
|
-
.values(...result.map(([objectId, { amount, amountPending }]) => {
|
|
291
|
+
.values(...result.map(([objectId, { amount, amountPending, nextDueAt }]) => {
|
|
219
292
|
return [
|
|
220
293
|
uuidv4(),
|
|
221
294
|
organizationId,
|
|
@@ -223,6 +296,7 @@ export class CachedBalance extends Model {
|
|
|
223
296
|
objectType,
|
|
224
297
|
amount,
|
|
225
298
|
amountPending,
|
|
299
|
+
nextDueAt,
|
|
226
300
|
new Date(),
|
|
227
301
|
new Date(),
|
|
228
302
|
];
|
|
@@ -231,6 +305,7 @@ export class CachedBalance extends Model {
|
|
|
231
305
|
.onDuplicateKeyUpdate(
|
|
232
306
|
SQL.assignment('amount', SQL.column('v', 'amount')),
|
|
233
307
|
SQL.assignment('amountPending', SQL.column('v', 'amountPending')),
|
|
308
|
+
SQL.assignment('nextDueAt', SQL.column('v', 'nextDueAt')),
|
|
234
309
|
SQL.assignment('updatedAt', SQL.column('v', 'updatedAt')),
|
|
235
310
|
);
|
|
236
311
|
|
|
@@ -208,7 +208,7 @@ export class DocumentTemplate extends Model {
|
|
|
208
208
|
let debtor: Parent | undefined = parentsWithNRN[0] ?? registration.member.details.parents[0];
|
|
209
209
|
if (parentsWithNRN.length > 1) {
|
|
210
210
|
for (const balanceItem of balanceItems) {
|
|
211
|
-
if (balanceItem && balanceItem.userId && balanceItem.status === BalanceItemStatus.
|
|
211
|
+
if (balanceItem && balanceItem.userId && balanceItem.priceOpen === 0 && balanceItem.status === BalanceItemStatus.Due) {
|
|
212
212
|
const user = await User.getByID(balanceItem.userId);
|
|
213
213
|
if (user) {
|
|
214
214
|
const parent = parentsWithNRN.find(p => p.hasEmail(user.email));
|
package/src/models/Order.ts
CHANGED
|
@@ -157,6 +157,13 @@ export class Order extends Model {
|
|
|
157
157
|
return false;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
get isDue() {
|
|
161
|
+
if (this.status === OrderStatus.Canceled || this.status === OrderStatus.Deleted) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
160
167
|
get totalToPay() {
|
|
161
168
|
if (this.status === OrderStatus.Canceled || this.status === OrderStatus.Deleted) {
|
|
162
169
|
return 0;
|
package/src/models/Payment.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { column, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
2
|
-
import { BalanceItemDetailed, BalanceItemPaymentDetailed, PaymentCustomer, PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus, Settlement, TransferSettings, BaseOrganization } from '@stamhoofd/structures';
|
|
2
|
+
import { BalanceItemDetailed, BalanceItemPaymentDetailed, PaymentCustomer, PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus, Settlement, TransferSettings, BaseOrganization, PaymentType } from '@stamhoofd/structures';
|
|
3
3
|
import { Formatter } from '@stamhoofd/utility';
|
|
4
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
5
|
|
|
@@ -16,6 +16,18 @@ export class Payment extends Model {
|
|
|
16
16
|
})
|
|
17
17
|
id!: string;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Types of payment:
|
|
21
|
+
* - Payment (default) = positive amount or zero
|
|
22
|
+
* - Refund = negative amount
|
|
23
|
+
* - Rebooking = zero payment due to rebooking
|
|
24
|
+
*/
|
|
25
|
+
@column({ type: 'string' })
|
|
26
|
+
type = PaymentType.Payment;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* How the payment is paid out or refunded
|
|
30
|
+
*/
|
|
19
31
|
@column({ type: 'string' })
|
|
20
32
|
method: PaymentMethod;
|
|
21
33
|
|