@stamhoofd/backend 2.122.8 → 2.123.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 +17 -17
- package/src/endpoints/auth/GetUserEndpoint.test.ts +43 -0
- package/src/endpoints/auth/GetUserEndpoint.ts +1 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +2 -1
- package/src/excel-loaders/payments.test.ts +201 -0
- package/src/excel-loaders/payments.ts +214 -8
- package/src/helpers/AdminPermissionChecker.ts +14 -1
- package/src/helpers/Context.ts +7 -2
- package/src/helpers/FileCache.ts +7 -3
- package/src/migrate.ts +28 -25
- package/src/seeds/1782296682-create-missing-group-categories.ts +237 -0
- package/src/migrations/1750090029-document-update-year.test.ts +0 -181
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.123.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
"@simonbackx/simple-endpoints": "1.21.1",
|
|
58
58
|
"@simonbackx/simple-errors": "1.5.0",
|
|
59
59
|
"@simonbackx/simple-logging": "1.0.1",
|
|
60
|
-
"@stamhoofd/backend-env": "2.
|
|
61
|
-
"@stamhoofd/backend-i18n": "2.
|
|
62
|
-
"@stamhoofd/backend-middleware": "2.
|
|
63
|
-
"@stamhoofd/crons": "2.
|
|
64
|
-
"@stamhoofd/email": "2.
|
|
65
|
-
"@stamhoofd/excel-writer": "2.
|
|
66
|
-
"@stamhoofd/logging": "2.
|
|
67
|
-
"@stamhoofd/models": "2.
|
|
68
|
-
"@stamhoofd/object-differ": "2.
|
|
69
|
-
"@stamhoofd/queues": "2.
|
|
70
|
-
"@stamhoofd/sql": "2.
|
|
71
|
-
"@stamhoofd/structures": "2.
|
|
72
|
-
"@stamhoofd/types": "2.
|
|
73
|
-
"@stamhoofd/utility": "2.
|
|
60
|
+
"@stamhoofd/backend-env": "2.123.0",
|
|
61
|
+
"@stamhoofd/backend-i18n": "2.123.0",
|
|
62
|
+
"@stamhoofd/backend-middleware": "2.123.0",
|
|
63
|
+
"@stamhoofd/crons": "2.123.0",
|
|
64
|
+
"@stamhoofd/email": "2.123.0",
|
|
65
|
+
"@stamhoofd/excel-writer": "2.123.0",
|
|
66
|
+
"@stamhoofd/logging": "2.123.0",
|
|
67
|
+
"@stamhoofd/models": "2.123.0",
|
|
68
|
+
"@stamhoofd/object-differ": "2.123.0",
|
|
69
|
+
"@stamhoofd/queues": "2.123.0",
|
|
70
|
+
"@stamhoofd/sql": "2.123.0",
|
|
71
|
+
"@stamhoofd/structures": "2.123.0",
|
|
72
|
+
"@stamhoofd/types": "2.123.0",
|
|
73
|
+
"@stamhoofd/utility": "2.123.0",
|
|
74
74
|
"archiver": "7.0.1",
|
|
75
75
|
"axios": "1.16.0",
|
|
76
76
|
"base-x": "3.0.11",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"stripe": "16.12.0"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@stamhoofd/test-utils": "2.
|
|
94
|
+
"@stamhoofd/test-utils": "2.123.0",
|
|
95
95
|
"@types/cookie": "0.6.0",
|
|
96
96
|
"@types/luxon": "3.7.1",
|
|
97
97
|
"@types/mailparser": "3.4.6",
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
"publishConfig": {
|
|
108
108
|
"access": "public"
|
|
109
109
|
},
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "5030664a2a57bf8626611b181e7c12d86a0293f6"
|
|
111
111
|
}
|
|
@@ -3,6 +3,7 @@ import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
|
3
3
|
|
|
4
4
|
import { testServer } from '../../../tests/helpers/TestServer.js';
|
|
5
5
|
import { GetUserEndpoint } from './GetUserEndpoint.js';
|
|
6
|
+
import { STExpect } from '@stamhoofd/test-utils';
|
|
6
7
|
|
|
7
8
|
describe('Endpoint.GetUser', () => {
|
|
8
9
|
// Test endpoint
|
|
@@ -48,4 +49,46 @@ describe('Endpoint.GetUser', () => {
|
|
|
48
49
|
|
|
49
50
|
await expect(testServer.test(endpoint, r)).rejects.toThrow(/expired/i);
|
|
50
51
|
});
|
|
52
|
+
|
|
53
|
+
test('Can request details of an API user by token without knowing the organization scope', async () => {
|
|
54
|
+
const organization = await new OrganizationFactory({}).create();
|
|
55
|
+
const user = await new UserFactory({ organization, apiUser: true }).create();
|
|
56
|
+
const token = await Token.createToken(user);
|
|
57
|
+
|
|
58
|
+
const r = Request.buildJson('GET', '/v1/user');
|
|
59
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
60
|
+
|
|
61
|
+
const response = await testServer.test(endpoint, r);
|
|
62
|
+
expect(response.body).toBeDefined();
|
|
63
|
+
expect(response.body.id).toEqual(user.id);
|
|
64
|
+
expect(response.body.organizationId).toEqual(organization.id);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('Cannot request details of an normal user by token without knowing the organization scope', async () => {
|
|
68
|
+
const organization = await new OrganizationFactory({}).create();
|
|
69
|
+
const user = await new UserFactory({ organization }).create();
|
|
70
|
+
const token = await Token.createToken(user);
|
|
71
|
+
|
|
72
|
+
const r = Request.buildJson('GET', '/v1/user');
|
|
73
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
74
|
+
|
|
75
|
+
await expect(testServer.test(endpoint, r)).rejects.toThrow(STExpect.simpleError({
|
|
76
|
+
code: 'invalid_access_token'
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('Cannot request details of an API user by token with wrong organization scope', async () => {
|
|
81
|
+
const wrongOrganization = await new OrganizationFactory({}).create();
|
|
82
|
+
const organization = await new OrganizationFactory({}).create();
|
|
83
|
+
const user = await new UserFactory({ organization, apiUser: true }).create();
|
|
84
|
+
const token = await Token.createToken(user);
|
|
85
|
+
|
|
86
|
+
const r = Request.buildJson('GET', '/v1/user', wrongOrganization.getApiHost());
|
|
87
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
88
|
+
|
|
89
|
+
await expect(testServer.test(endpoint, r)).rejects.toThrow(STExpect.simpleError({
|
|
90
|
+
code: 'invalid_access_token'
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
|
|
51
94
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import type { UserWithMembers } from '@stamhoofd/structures';
|
|
4
|
-
|
|
5
4
|
import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures.js';
|
|
6
5
|
import { Context } from '../../helpers/Context.js';
|
|
7
6
|
|
|
@@ -26,7 +25,7 @@ export class GetUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
|
|
|
26
25
|
|
|
27
26
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
28
27
|
await Context.setOptionalOrganizationScope();
|
|
29
|
-
const { user } = await Context.authenticate({ allowWithoutAccount: true });
|
|
28
|
+
const { user } = await Context.authenticate({ allowWithoutAccount: true, allowUnscoped: true });
|
|
30
29
|
|
|
31
30
|
return new Response(
|
|
32
31
|
await AuthenticatedStructures.userWithMembers(user),
|
|
@@ -455,7 +455,8 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
if (shouldUpdatePeriodIds) {
|
|
458
|
-
|
|
458
|
+
const oldOrganizationPeriod = await OrganizationRegistrationPeriod.select().where('periodId', model.periodId).where('organizationId', model.organizationId).first(false);
|
|
459
|
+
if (oldOrganizationPeriod && model.settings.hasBundleDiscounts(oldOrganizationPeriod)) {
|
|
459
460
|
throw new SimpleError({
|
|
460
461
|
code: 'change_period_not_allowed',
|
|
461
462
|
message: 'This group cannot be moved to another period because it has bundle discounts.',
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { BalanceItem, BalanceItemPaymentDetailed, BalanceItemRelation, BalanceItemRelationType, BalanceItemType, Cart, CartItem, CartItemPrice, OrderData, PaymentGeneral, PaymentMethod, PaymentStatus, Product, ProductPrice, TranslatedString } from '@stamhoofd/structures';
|
|
2
|
+
import { expandPaymentBalanceItemPayments } from './payments.js';
|
|
3
|
+
|
|
4
|
+
function createOrderData(options: {
|
|
5
|
+
percentageDiscount?: number;
|
|
6
|
+
fixedDiscount?: number;
|
|
7
|
+
} = {}) {
|
|
8
|
+
const productPrice = ProductPrice.create({
|
|
9
|
+
id: 'large',
|
|
10
|
+
name: 'Groot',
|
|
11
|
+
price: 3500,
|
|
12
|
+
});
|
|
13
|
+
const otherProductPrice = ProductPrice.create({
|
|
14
|
+
id: 'small',
|
|
15
|
+
name: 'Klein',
|
|
16
|
+
price: 2500,
|
|
17
|
+
});
|
|
18
|
+
const product = Product.create({
|
|
19
|
+
id: 'coffee',
|
|
20
|
+
name: 'Koffie',
|
|
21
|
+
prices: [productPrice, otherProductPrice],
|
|
22
|
+
});
|
|
23
|
+
const cartItem = CartItem.create({
|
|
24
|
+
id: 'cart-item-1',
|
|
25
|
+
product,
|
|
26
|
+
productPrice,
|
|
27
|
+
amount: 2,
|
|
28
|
+
unitPrice: 3500,
|
|
29
|
+
calculatedPrices: [
|
|
30
|
+
CartItemPrice.create({ price: 3500 }),
|
|
31
|
+
CartItemPrice.create({ price: 3500 }),
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return OrderData.create({
|
|
36
|
+
cart: Cart.create({
|
|
37
|
+
items: [cartItem],
|
|
38
|
+
}),
|
|
39
|
+
administrationFee: 500,
|
|
40
|
+
percentageDiscount: options.percentageDiscount ?? 0,
|
|
41
|
+
fixedDiscount: options.fixedDiscount ?? 0,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createPayment(price: number): PaymentGeneral {
|
|
46
|
+
return PaymentGeneral.create({
|
|
47
|
+
id: 'payment-1',
|
|
48
|
+
method: PaymentMethod.Transfer,
|
|
49
|
+
status: PaymentStatus.Succeeded,
|
|
50
|
+
price,
|
|
51
|
+
balanceItemPayments: [
|
|
52
|
+
BalanceItemPaymentDetailed.create({
|
|
53
|
+
id: 'balance-item-payment-1',
|
|
54
|
+
price,
|
|
55
|
+
balanceItem: BalanceItem.create({
|
|
56
|
+
id: 'balance-item-1',
|
|
57
|
+
type: BalanceItemType.Order,
|
|
58
|
+
orderId: 'order-1',
|
|
59
|
+
description: 'Bestelling #1',
|
|
60
|
+
amount: 1,
|
|
61
|
+
unitPrice: price,
|
|
62
|
+
relations: new Map([
|
|
63
|
+
[BalanceItemRelationType.Webshop, BalanceItemRelation.create({
|
|
64
|
+
id: 'webshop-1',
|
|
65
|
+
name: new TranslatedString('Clubshop'),
|
|
66
|
+
})],
|
|
67
|
+
]),
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function expectRowsToMatchReplacedPayment(rows: BalanceItemPaymentDetailed[], payment: PaymentGeneral) {
|
|
75
|
+
expect(rows.reduce((sum, row) => sum + row.price, 0)).toBe(payment.balanceItemPayments[0].price);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('payments excel loader', () => {
|
|
79
|
+
describe('expandPaymentBalanceItemPayments', () => {
|
|
80
|
+
it('splits a full order payment into order item rows and fees', () => {
|
|
81
|
+
const orderData = createOrderData();
|
|
82
|
+
const payment = createPayment(orderData.totalPrice);
|
|
83
|
+
|
|
84
|
+
const rows = expandPaymentBalanceItemPayments(payment, new Map([
|
|
85
|
+
['order-1', { id: 'order-1', number: 123, data: orderData }],
|
|
86
|
+
]));
|
|
87
|
+
|
|
88
|
+
expect(rows).toHaveLength(2);
|
|
89
|
+
expect(rows[0].customTitle).toBe('Koffie');
|
|
90
|
+
expect(rows[0].balanceItem.description).toBe('Groot');
|
|
91
|
+
expect(rows[0].amount).toBe(2);
|
|
92
|
+
expect(rows[0].unitPrice).toBe(3500);
|
|
93
|
+
expect(rows[0].price).toBe(7000);
|
|
94
|
+
expect(rows[1].customTitle).toBe('Administratiekosten');
|
|
95
|
+
expect(rows[1].balanceItem.description).toBe('Administratiekosten');
|
|
96
|
+
expect(rows[1].amount).toBe(1);
|
|
97
|
+
expect(rows[1].price).toBe(500);
|
|
98
|
+
expectRowsToMatchReplacedPayment(rows, payment);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('adds order discount rows when splitting a full order payment', () => {
|
|
102
|
+
const orderData = createOrderData({
|
|
103
|
+
percentageDiscount: 1000,
|
|
104
|
+
fixedDiscount: 300,
|
|
105
|
+
});
|
|
106
|
+
const payment = createPayment(orderData.totalPrice);
|
|
107
|
+
|
|
108
|
+
const rows = expandPaymentBalanceItemPayments(payment, new Map([
|
|
109
|
+
['order-1', { id: 'order-1', number: 123, data: orderData }],
|
|
110
|
+
]));
|
|
111
|
+
|
|
112
|
+
expect(rows).toHaveLength(4);
|
|
113
|
+
expect(rows.map(row => row.customTitle)).toEqual([
|
|
114
|
+
'Koffie',
|
|
115
|
+
'Korting (10%)',
|
|
116
|
+
'Korting',
|
|
117
|
+
'Administratiekosten',
|
|
118
|
+
]);
|
|
119
|
+
expect(rows.map(row => row.balanceItem.description)).toEqual([
|
|
120
|
+
'Groot',
|
|
121
|
+
'Korting (10%)',
|
|
122
|
+
'Vaste korting',
|
|
123
|
+
'Administratiekosten',
|
|
124
|
+
]);
|
|
125
|
+
expect(rows.map(row => row.price)).toEqual([
|
|
126
|
+
7000,
|
|
127
|
+
-700,
|
|
128
|
+
-300,
|
|
129
|
+
500,
|
|
130
|
+
]);
|
|
131
|
+
expectRowsToMatchReplacedPayment(rows, payment);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('caps order discount rows to the cart subtotal', () => {
|
|
135
|
+
const orderData = createOrderData({
|
|
136
|
+
percentageDiscount: 10000,
|
|
137
|
+
fixedDiscount: 3000,
|
|
138
|
+
});
|
|
139
|
+
const payment = createPayment(orderData.totalPrice);
|
|
140
|
+
|
|
141
|
+
const rows = expandPaymentBalanceItemPayments(payment, new Map([
|
|
142
|
+
['order-1', { id: 'order-1', number: 123, data: orderData }],
|
|
143
|
+
]));
|
|
144
|
+
|
|
145
|
+
expect(rows).toHaveLength(3);
|
|
146
|
+
expect(rows.map(row => row.customTitle)).toEqual([
|
|
147
|
+
'Koffie',
|
|
148
|
+
'Korting (100%)',
|
|
149
|
+
'Administratiekosten',
|
|
150
|
+
]);
|
|
151
|
+
expect(rows.map(row => row.balanceItem.description)).toEqual([
|
|
152
|
+
'Groot',
|
|
153
|
+
'Korting (100%)',
|
|
154
|
+
'Administratiekosten',
|
|
155
|
+
]);
|
|
156
|
+
expect(rows.map(row => row.price)).toEqual([
|
|
157
|
+
7000,
|
|
158
|
+
-7000,
|
|
159
|
+
500,
|
|
160
|
+
]);
|
|
161
|
+
expectRowsToMatchReplacedPayment(rows, payment);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('keeps partial order payments and refunds as single rows', () => {
|
|
165
|
+
const orderData = createOrderData();
|
|
166
|
+
const orderMap = new Map([
|
|
167
|
+
['order-1', { id: 'order-1', number: 123, data: orderData }],
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const changedRows = expandPaymentBalanceItemPayments(createPayment(1000), orderMap);
|
|
171
|
+
expect(changedRows).toHaveLength(1);
|
|
172
|
+
expect(changedRows[0].balanceItem.description).toBe('Gedeeltelijke betaling/terugbetaling voor bestelling #123');
|
|
173
|
+
expect(changedRows[0].amount).toBe(1);
|
|
174
|
+
expect(changedRows[0].price).toBe(1000);
|
|
175
|
+
|
|
176
|
+
const refundRows = expandPaymentBalanceItemPayments(createPayment(-1000), orderMap);
|
|
177
|
+
expect(refundRows).toHaveLength(1);
|
|
178
|
+
expect(refundRows[0].balanceItem.description).toBe('Gedeeltelijke betaling/terugbetaling voor bestelling #123');
|
|
179
|
+
expect(refundRows[0].amount).toBe(1);
|
|
180
|
+
expect(refundRows[0].price).toBe(-1000);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('only splits the same full order once per export page', () => {
|
|
184
|
+
const orderData = createOrderData();
|
|
185
|
+
const orderMap = new Map([
|
|
186
|
+
['order-1', { id: 'order-1', number: 123, data: orderData }],
|
|
187
|
+
]);
|
|
188
|
+
const addedOrderIds = new Set<string>();
|
|
189
|
+
|
|
190
|
+
const firstRows = expandPaymentBalanceItemPayments(createPayment(orderData.totalPrice), orderMap, addedOrderIds);
|
|
191
|
+
const secondRows = expandPaymentBalanceItemPayments(createPayment(orderData.totalPrice), orderMap, addedOrderIds);
|
|
192
|
+
|
|
193
|
+
expect(firstRows).toHaveLength(2);
|
|
194
|
+
expect(secondRows).toHaveLength(1);
|
|
195
|
+
expect(secondRows[0].balanceItem.description).toBe('Gedeeltelijke betaling/terugbetaling voor bestelling #123');
|
|
196
|
+
expect(secondRows[0].price).toBe(orderData.totalPrice);
|
|
197
|
+
expectRowsToMatchReplacedPayment(firstRows, createPayment(orderData.totalPrice));
|
|
198
|
+
expectRowsToMatchReplacedPayment(secondRows, createPayment(orderData.totalPrice));
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import { field } from '@simonbackx/simple-encoding';
|
|
2
2
|
import type { XlsxTransformerColumn, XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
|
|
3
3
|
import { XlsxBuiltInNumberFormat } from '@stamhoofd/excel-writer';
|
|
4
|
-
import { StripeAccount } from '@stamhoofd/models';
|
|
5
|
-
import type {
|
|
6
|
-
import { BalanceItemRelationType, ExcelExportType, getBalanceItemRelationTypeName, getBalanceItemTypeName, PaginatedResponse, PaymentGeneral, PaymentMethodHelper, PaymentStatusHelper, StripeAccount as StripeAccountStruct } from '@stamhoofd/structures';
|
|
4
|
+
import { Order, StripeAccount } from '@stamhoofd/models';
|
|
5
|
+
import type { OrderData } from '@stamhoofd/structures';
|
|
6
|
+
import { BalanceItem, BalanceItemPaymentDetailed, BalanceItemRelationType, BalanceItemType, ExcelExportType, getBalanceItemRelationTypeName, getBalanceItemTypeName, PaginatedResponse, PaymentGeneral, PaymentMethodHelper, PaymentStatusHelper, StripeAccount as StripeAccountStruct } from '@stamhoofd/structures';
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
|
|
9
9
|
import { GetPaymentsEndpoint } from '../endpoints/organization/dashboard/payments/GetPaymentsEndpoint.js';
|
|
10
10
|
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
|
|
11
11
|
|
|
12
12
|
type PaymentWithItem = {
|
|
13
|
-
payment:
|
|
14
|
-
balanceItemPayment:
|
|
13
|
+
payment: PaymentGeneralWithStripeAccount;
|
|
14
|
+
balanceItemPayment: PaymentExportBalanceItemPayment;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PaymentExportOrder = {
|
|
18
|
+
id: string;
|
|
19
|
+
number: number | null;
|
|
20
|
+
data: OrderData;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type PaymentExportBalanceItemPayment = BalanceItemPaymentDetailed & {
|
|
24
|
+
customTitle: string | null;
|
|
15
25
|
};
|
|
16
26
|
|
|
17
27
|
export class PaymentGeneralWithStripeAccount extends PaymentGeneral {
|
|
18
28
|
@field({ decoder: StripeAccountStruct, nullable: true })
|
|
19
29
|
stripeAccount: StripeAccountStruct | null = null;
|
|
30
|
+
|
|
31
|
+
expandedBalanceItemPayments: PaymentExportBalanceItemPayment[] = [];
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
ExportToExcelEndpoint.loaders.set(ExcelExportType.Payments, {
|
|
@@ -31,11 +43,21 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Payments, {
|
|
|
31
43
|
accounts = (await StripeAccount.getByIDs(...stripeAccountIds)).map(s => StripeAccountStruct.create(s));
|
|
32
44
|
}
|
|
33
45
|
|
|
46
|
+
const orderIds = Formatter.uniqueArray(
|
|
47
|
+
data.results.flatMap(payment => payment.balanceItemPayments.flatMap((item) => {
|
|
48
|
+
return item.balanceItem.orderId ? [item.balanceItem.orderId] : [];
|
|
49
|
+
})),
|
|
50
|
+
);
|
|
51
|
+
const orders = orderIds.length > 0 ? await Order.getByIDs(...orderIds) : [];
|
|
52
|
+
const orderMap = new Map<string, PaymentExportOrder>(orders.map(order => [order.id, order]));
|
|
53
|
+
const addedOrderIds = new Set<string>();
|
|
54
|
+
|
|
34
55
|
return new PaginatedResponse({
|
|
35
56
|
...data,
|
|
36
57
|
results: data.results.map((p) => {
|
|
37
58
|
const payment = PaymentGeneralWithStripeAccount.create(p);
|
|
38
59
|
payment.stripeAccount = p.stripeAccountId ? (accounts.find(a => a.id === p.stripeAccountId) ?? null) : null;
|
|
60
|
+
payment.expandedBalanceItemPayments = expandPaymentBalanceItemPayments(payment, orderMap, addedOrderIds);
|
|
39
61
|
return payment;
|
|
40
62
|
}),
|
|
41
63
|
});
|
|
@@ -56,7 +78,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Payments, {
|
|
|
56
78
|
{
|
|
57
79
|
id: 'balanceItemPayments',
|
|
58
80
|
name: $t(`%Ly`),
|
|
59
|
-
transform: (data:
|
|
81
|
+
transform: (data: PaymentGeneralWithStripeAccount): PaymentWithItem[] => data.expandedBalanceItemPayments.map(p => ({
|
|
60
82
|
payment: data,
|
|
61
83
|
balanceItemPayment: p,
|
|
62
84
|
})),
|
|
@@ -100,6 +122,177 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Payments, {
|
|
|
100
122
|
],
|
|
101
123
|
});
|
|
102
124
|
|
|
125
|
+
export function expandPaymentBalanceItemPayments(
|
|
126
|
+
payment: PaymentGeneral,
|
|
127
|
+
orderMap: Map<string, PaymentExportOrder>,
|
|
128
|
+
addedOrderIds = new Set<string>(),
|
|
129
|
+
): PaymentExportBalanceItemPayment[] {
|
|
130
|
+
return payment.balanceItemPayments.flatMap((item) => {
|
|
131
|
+
const orderId = item.balanceItem.orderId;
|
|
132
|
+
if (!orderId) {
|
|
133
|
+
return [createExportBalanceItemPayment(item, null)];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const order = orderMap.get(orderId);
|
|
137
|
+
if (!order) {
|
|
138
|
+
return [createExportBalanceItemPayment(item, null)];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!addedOrderIds.has(order.id) && item.price === order.data.totalPrice) {
|
|
142
|
+
addedOrderIds.add(order.id);
|
|
143
|
+
return createOrderItemPaymentRows(item, order);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return [
|
|
147
|
+
createSyntheticBalanceItemPayment({
|
|
148
|
+
source: item,
|
|
149
|
+
description: getPartialOrderPaymentDescription(order),
|
|
150
|
+
amount: 1,
|
|
151
|
+
price: item.price,
|
|
152
|
+
}),
|
|
153
|
+
];
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createOrderItemPaymentRows(
|
|
158
|
+
item: BalanceItemPaymentDetailed,
|
|
159
|
+
order: PaymentExportOrder,
|
|
160
|
+
): PaymentExportBalanceItemPayment[] {
|
|
161
|
+
const rows: PaymentExportBalanceItemPayment[] = [];
|
|
162
|
+
|
|
163
|
+
for (const orderItem of order.data.cart.items) {
|
|
164
|
+
rows.push(
|
|
165
|
+
createSyntheticBalanceItemPayment({
|
|
166
|
+
source: item,
|
|
167
|
+
customTitle: orderItem.product.name,
|
|
168
|
+
description: orderItem.description,
|
|
169
|
+
amount: orderItem.amount,
|
|
170
|
+
price: orderItem.getPrice(),
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
addOrderDiscountRows(rows, item, order);
|
|
176
|
+
|
|
177
|
+
if (order.data.deliveryPrice) {
|
|
178
|
+
rows.push(
|
|
179
|
+
createSyntheticBalanceItemPayment({
|
|
180
|
+
source: item,
|
|
181
|
+
customTitle: $t('Leveringskosten'),
|
|
182
|
+
description: $t('Leveringskosten'),
|
|
183
|
+
amount: 1,
|
|
184
|
+
price: order.data.deliveryPrice,
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (order.data.administrationFee) {
|
|
190
|
+
rows.push(
|
|
191
|
+
createSyntheticBalanceItemPayment({
|
|
192
|
+
source: item,
|
|
193
|
+
customTitle: $t('Administratiekosten'),
|
|
194
|
+
description: $t('Administratiekosten'),
|
|
195
|
+
amount: 1,
|
|
196
|
+
price: order.data.administrationFee,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const difference = item.price - rows.reduce((sum, row) => sum + row.price, 0);
|
|
202
|
+
if (difference !== 0) {
|
|
203
|
+
rows.push(
|
|
204
|
+
createSyntheticBalanceItemPayment({
|
|
205
|
+
source: item,
|
|
206
|
+
customTitle: $t('Correctie bestelling'),
|
|
207
|
+
description: $t('Correctie bestelling'),
|
|
208
|
+
amount: 1,
|
|
209
|
+
price: difference,
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return rows;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function addOrderDiscountRows(
|
|
218
|
+
rows: PaymentExportBalanceItemPayment[],
|
|
219
|
+
item: BalanceItemPaymentDetailed,
|
|
220
|
+
order: PaymentExportOrder,
|
|
221
|
+
) {
|
|
222
|
+
let remainingDiscountablePrice = order.data.cart.price;
|
|
223
|
+
|
|
224
|
+
const appliedPercentageDiscount = Math.min(order.data.appliedPercentageDiscount, remainingDiscountablePrice);
|
|
225
|
+
if (appliedPercentageDiscount > 0) {
|
|
226
|
+
rows.push(
|
|
227
|
+
createSyntheticBalanceItemPayment({
|
|
228
|
+
source: item,
|
|
229
|
+
customTitle: $t('Korting ({percentage})', { percentage: Formatter.percentage(order.data.percentageDiscount) }),
|
|
230
|
+
description: $t('Korting ({percentage})', { percentage: Formatter.percentage(order.data.percentageDiscount) }),
|
|
231
|
+
amount: 1,
|
|
232
|
+
price: -appliedPercentageDiscount,
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
remainingDiscountablePrice -= appliedPercentageDiscount;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const fixedDiscount = Math.min(order.data.fixedDiscount, remainingDiscountablePrice);
|
|
239
|
+
if (fixedDiscount > 0) {
|
|
240
|
+
rows.push(
|
|
241
|
+
createSyntheticBalanceItemPayment({
|
|
242
|
+
source: item,
|
|
243
|
+
customTitle: $t('Korting'),
|
|
244
|
+
description: $t('Vaste korting'),
|
|
245
|
+
amount: 1,
|
|
246
|
+
price: -fixedDiscount,
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getPartialOrderPaymentDescription(order: PaymentExportOrder): string {
|
|
253
|
+
if (order.number !== null) {
|
|
254
|
+
return $t('Gedeeltelijke betaling/terugbetaling voor bestelling #{number}', { number: order.number.toString() });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return $t('Gedeeltelijke betaling/terugbetaling voor bestelling');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createSyntheticBalanceItemPayment({
|
|
261
|
+
source,
|
|
262
|
+
customTitle = null,
|
|
263
|
+
description,
|
|
264
|
+
amount,
|
|
265
|
+
price,
|
|
266
|
+
}: {
|
|
267
|
+
source: BalanceItemPaymentDetailed;
|
|
268
|
+
customTitle?: string | null;
|
|
269
|
+
description: string;
|
|
270
|
+
amount: number;
|
|
271
|
+
price: number;
|
|
272
|
+
}): PaymentExportBalanceItemPayment {
|
|
273
|
+
return createExportBalanceItemPayment(BalanceItemPaymentDetailed.create({
|
|
274
|
+
...source,
|
|
275
|
+
price,
|
|
276
|
+
balanceItem: BalanceItem.create({
|
|
277
|
+
...source.balanceItem,
|
|
278
|
+
type: BalanceItemType.Order,
|
|
279
|
+
relations: new Map(source.balanceItem.relations),
|
|
280
|
+
description,
|
|
281
|
+
amount,
|
|
282
|
+
unitPrice: amount === 0 ? 0 : price / amount,
|
|
283
|
+
}),
|
|
284
|
+
}), customTitle);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function createExportBalanceItemPayment(
|
|
288
|
+
item: BalanceItemPaymentDetailed,
|
|
289
|
+
customTitle: string | null,
|
|
290
|
+
): PaymentExportBalanceItemPayment {
|
|
291
|
+
return Object.assign(item, {
|
|
292
|
+
customTitle,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
103
296
|
function getBalanceItemColumns(): XlsxTransformerColumn<PaymentWithItem>[] {
|
|
104
297
|
return [
|
|
105
298
|
{
|
|
@@ -149,12 +342,25 @@ function getBalanceItemColumns(): XlsxTransformerColumn<PaymentWithItem>[] {
|
|
|
149
342
|
};
|
|
150
343
|
},
|
|
151
344
|
},
|
|
345
|
+
{
|
|
346
|
+
id: 'balanceItem.title',
|
|
347
|
+
name: $t('Titel'),
|
|
348
|
+
width: 40,
|
|
349
|
+
getValue: (object: PaymentWithItem) => ({
|
|
350
|
+
value: object.balanceItemPayment.customTitle || object.balanceItemPayment.balanceItem.itemTitle,
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
152
353
|
{
|
|
153
354
|
id: 'balanceItem.description',
|
|
154
355
|
name: $t(`%6o`),
|
|
155
|
-
width:
|
|
356
|
+
width: 50,
|
|
156
357
|
getValue: (object: PaymentWithItem) => ({
|
|
157
|
-
value: object.balanceItemPayment.balanceItem.description,
|
|
358
|
+
value: object.balanceItemPayment.balanceItem.itemDescription || object.balanceItemPayment.balanceItem.description,
|
|
359
|
+
style: {
|
|
360
|
+
alignment: {
|
|
361
|
+
wrapText: true,
|
|
362
|
+
},
|
|
363
|
+
},
|
|
158
364
|
}),
|
|
159
365
|
},
|
|
160
366
|
{
|
|
@@ -209,6 +209,14 @@ export class AdminPermissionChecker {
|
|
|
209
209
|
return this.user.permissions.organizationPermissions.get(typeof organizationOrId === 'string' ? organizationOrId : organizationOrId.id) ?? null;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
getUnloadedPlatformPermissions() {
|
|
213
|
+
if (!this.user.permissions) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return this.user.permissions.globalPermissions;
|
|
218
|
+
}
|
|
219
|
+
|
|
212
220
|
async canAccessPrivateOrganizationData(organization: Organization) {
|
|
213
221
|
if (!this.checkScope(organization.id)) {
|
|
214
222
|
return false;
|
|
@@ -232,7 +240,7 @@ export class AdminPermissionChecker {
|
|
|
232
240
|
return false;
|
|
233
241
|
}
|
|
234
242
|
|
|
235
|
-
if (!await this.hasSomeUnloadedAccess(organization)) {
|
|
243
|
+
if (!await this.hasSomeUnloadedAccess(organization) && !this.hasSomeUnloadedPlatformAccess()) {
|
|
236
244
|
return false;
|
|
237
245
|
}
|
|
238
246
|
return true;
|
|
@@ -1130,6 +1138,11 @@ export class AdminPermissionChecker {
|
|
|
1130
1138
|
return !!unloadedPermissions && !unloadedPermissions.isEmpty;
|
|
1131
1139
|
}
|
|
1132
1140
|
|
|
1141
|
+
hasSomeUnloadedPlatformAccess(): boolean {
|
|
1142
|
+
const unloadedPermissions = this.getUnloadedPlatformPermissions();
|
|
1143
|
+
return !!unloadedPermissions && !unloadedPermissions.isEmpty;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1133
1146
|
async canManageAdmins(organizationId: string) {
|
|
1134
1147
|
return !this.user.isApiUser && (await this.hasFullAccess(organizationId));
|
|
1135
1148
|
}
|
package/src/helpers/Context.ts
CHANGED
|
@@ -223,7 +223,7 @@ export class ContextInstance {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
async authenticate({ allowWithoutAccount = false }: { allowWithoutAccount?: boolean } = {}): Promise<{ user: User; token: Token }> {
|
|
226
|
+
async authenticate({ allowWithoutAccount = false, allowUnscoped = false }: { allowWithoutAccount?: boolean; allowUnscoped?: boolean } = {}): Promise<{ user: User; token: Token }> {
|
|
227
227
|
let header = this.request.headers.authorization;
|
|
228
228
|
|
|
229
229
|
if (!header && this.request.method === 'POST') {
|
|
@@ -255,7 +255,7 @@ export class ContextInstance {
|
|
|
255
255
|
|
|
256
256
|
const token = await Token.getByAccessToken(accessToken, true);
|
|
257
257
|
|
|
258
|
-
if (!token || (this.organization && token.user.organizationId !== null && token.user.organizationId !== this.organization.id) || (!this.organization && token.user.organizationId)) {
|
|
258
|
+
if (!token || (this.organization && token.user.organizationId !== null && token.user.organizationId !== this.organization.id) || (!this.organization && token.user.organizationId && (!allowUnscoped || !token.user.isApiUser))) {
|
|
259
259
|
if (token?.user) {
|
|
260
260
|
console.log(
|
|
261
261
|
'Failed auth: ' + token?.user.email + ' (' + token?.user.id + ')',
|
|
@@ -269,6 +269,11 @@ export class ContextInstance {
|
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
if (!this.organization && token.user.organizationId && allowUnscoped) {
|
|
273
|
+
// Automatically scope full request to the user's scope
|
|
274
|
+
await this.setManualOrganizationScope(await Organization.getByID(token.user.organizationId, true));
|
|
275
|
+
}
|
|
276
|
+
|
|
272
277
|
if (token.isAccessTokenExpired()) {
|
|
273
278
|
if (token?.user) {
|
|
274
279
|
console.log(
|
package/src/helpers/FileCache.ts
CHANGED
|
@@ -116,9 +116,13 @@ export class FileCache {
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const date =
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const date = Formatter.luxon().set({
|
|
120
|
+
day,
|
|
121
|
+
month,
|
|
122
|
+
year,
|
|
123
|
+
}).startOf('day').toJSDate();
|
|
124
|
+
|
|
125
|
+
const now = Formatter.luxon().startOf('day').toJSDate();
|
|
122
126
|
|
|
123
127
|
const diff = now.getTime() - date.getTime();
|
|
124
128
|
|
package/src/migrate.ts
CHANGED
|
@@ -8,10 +8,10 @@ Column.setJSONVersion(Version);
|
|
|
8
8
|
process.env.TZ = 'UTC';
|
|
9
9
|
|
|
10
10
|
// Polyfill require.resolve, since import.meta.resolve is not supported by vitest
|
|
11
|
-
import { createRequire } from 'node:module';
|
|
12
|
-
import { GlobalHelper } from './helpers/GlobalHelper.js';
|
|
13
11
|
import { I18n } from '@stamhoofd/backend-i18n/I18n';
|
|
14
12
|
import { QueryableModel } from '@stamhoofd/sql';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { GlobalHelper } from './helpers/GlobalHelper.js';
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
16
16
|
|
|
17
17
|
const emailPath = require.resolve('@stamhoofd/email');
|
|
@@ -50,34 +50,37 @@ const start = async () => {
|
|
|
50
50
|
database: null,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
try {
|
|
54
|
+
await globalDatabase.statement(query);
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
// External migrations
|
|
57
|
+
if (!await Migration.runAll(path.dirname(modelsPath) + '/migrations')) {
|
|
58
|
+
throw new Error('Migrations failed');
|
|
59
|
+
}
|
|
60
|
+
if (!await Migration.runAll(path.dirname(emailPath) + '/../migrations')) {
|
|
61
|
+
throw new Error('Email migrations failed');
|
|
62
|
+
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// Internal
|
|
65
|
+
await I18n.load();
|
|
66
|
+
GlobalHelper.loadGlobalTranslateFunction();
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (!await Migration.runAll(import.meta.dirname + '/migrations')) {
|
|
69
|
+
throw new Error('Internal migrations failed');
|
|
70
|
+
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if (killSignalReceived) {
|
|
73
|
+
console.error(chalk.red('Killing process due to received signal during migration'));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Reload database to prevent connection state leakage
|
|
78
|
+
await Database.reload({});
|
|
79
|
+
} finally {
|
|
80
|
+
await globalDatabase.end();
|
|
81
|
+
process.off('SIGTERM', handler);
|
|
82
|
+
process.off('SIGINT', handler);
|
|
74
83
|
}
|
|
75
|
-
|
|
76
|
-
// Reload database to prevent connection state leakage
|
|
77
|
-
await Database.reload({});
|
|
78
|
-
|
|
79
|
-
process.off('SIGTERM', handler);
|
|
80
|
-
process.off('SIGINT', handler);
|
|
81
84
|
};
|
|
82
85
|
|
|
83
86
|
export async function run() {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Group, Organization, OrganizationRegistrationPeriod, V1GroupMigrationData } from '@stamhoofd/models';
|
|
3
|
+
import { GroupCategory, GroupCategorySettings, GroupType } from '@stamhoofd/structures';
|
|
4
|
+
import { SeedTools } from '../helpers/SeedTools.js';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'test') {
|
|
8
|
+
console.log('skipped in tests');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
|
|
13
|
+
console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dryRun = false;
|
|
18
|
+
await start(dryRun);
|
|
19
|
+
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
throw new Error('Migration did not finish because of dryRun');
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// only check organizations that were checked in the 'fix' migration
|
|
26
|
+
const relevantOrganizations = new Set(['f2543d6b-523e-4b03-848b-838200b1ff41', '11a478dd-e8cf-4160-80dd-8b48d20a6d39', '6d1c32cf-2ef0-4738-bacb-d553488f97b9', '3e6f5cb9-e130-4dda-92c9-26682fa09509', '57815733-5750-492f-beb1-1efd6229803a', '7677e32d-763a-4033-bc74-30d13f27556d', '7ac75a0f-f365-4ddc-836c-66a3267f24f0', '9dced404-a92d-44c4-b301-ecd29b6ac4e0', '97928f8a-b8d1-44cc-918f-09d0ae896c3b', 'f0edb665-1ce0-442a-88e7-5a62ea78c5fc', '842ada22-82f3-4cef-a1da-ae5da20b208d', '33744473-9ad9-4d3e-a9fc-31cd1b6a2753', 'ed6e74f1-51a0-4e04-8195-e722b015eeb5', '10827cc5-5f29-46cf-b26e-5fd11a9342cc', '5f4d4a26-43c3-4f80-86ad-64d07d5a46a7', '74e2bcbf-0ff9-484b-988a-e11d147122c0', 'c69512bc-ea0c-427a-ab90-08c3dcf1c856', '16742d64-0b31-4ce7-9c1e-6194882045b9', '95d59803-c50e-4bc1-adc9-90ade32b7579', 'eb6ccffa-41c5-45df-b4e4-1eb5749bc0fe', '1a534005-2784-400c-b8fe-bde09c901259', '27e06924-49a6-468c-a16e-b5735ace1894', '7a86f3db-08d8-4752-a77d-eb85f2167942', 'f562c735-7bf4-4e2e-8c0f-6584e8e96a1d', 'e2afe517-cd35-4d9e-a561-125ad184a2ff', 'fd934e40-734c-442d-b338-c3ae288dd55d']);
|
|
27
|
+
|
|
28
|
+
// only check groups that were checked in the 'fix' migration
|
|
29
|
+
const relevantGroups = new Set([
|
|
30
|
+
'020a313c-0e97-4068-84ba-783d91e768a4', '02420f62-1a07-4e2a-ae92-13c35320fb27', '0b6d8422-857f-46e4-a632-a4af322c9193', '12ba896a-e064-4697-a5df-69664c9bd8cb', '134e33f9-3560-4eca-977b-aaef01af5ed4', '1a9bb59a-c85a-4338-94c6-f82061f1ee0e', '1bf5519d-4645-4c51-8ae5-7a202bb74e9e', '1c86c401-c32b-4c6f-a2b5-fb15649efa74', '1ddc6fde-f512-43ac-8e15-2496601e900e', '20cab6ae-fb30-4de8-948e-60caf0979ff7', '22194f9d-fba2-4497-b7b0-de8ba6e884ff', '2384f112-d50d-4fcf-b6f2-45db73a8944c', '26693e8a-b73d-4f28-a41c-d281ba91fb3e', '287ff7b8-3319-40fb-ad25-26b139db3272', '2e473bf7-36f9-4276-8770-032db776d154', '3282e30c-a3db-4f00-b827-646fcc532c43', '3490cfd8-4c0b-487f-8f0d-c96c62950160', '3d68762b-8133-42e0-a642-7649864b09b3', '4a7d205b-e996-4841-b21c-1336642e670d', '52b0285c-4935-4577-bc7b-30d0b8804fc9', '579263b6-ceb1-4be0-a31e-9a4a904f0cae', '5870423f-6faf-492e-accb-4bd110365328', '60e24adb-5bb3-48de-9008-5b80c905a01a', '6535d9b4-f2ad-4f7e-b7e2-9e50ddf5eff6', '6a508234-08aa-4b0b-9647-7aeb9d80fa5a', '722f3bdf-d9df-4a78-a4c1-783c9af3d21b', '754bb111-caad-49b3-8dde-5bd0f3ad9104', '76aef892-19b5-4deb-9276-0c6b19b652fd', '78fa525b-e346-4b78-9e25-f9b1b5134b77', '7989ea9d-354d-4eff-9350-8eb8a6c4732b', '7aba3792-0138-4302-8550-2ccb456a37ad', '7c480cd5-8db3-4c03-8f0b-a88556df0234', '8113c2a2-e788-4f11-815f-4b019d7ca966', '82958c24-bc12-45f2-9d0d-3c8b902df052', '88c28670-cf97-42e2-ae9e-d836d349c2e6', '88d87c1a-749f-453c-a60b-e54a7001a9f0', '8983c1d5-e8aa-4628-a251-17220336fad5', '89ced76d-8c98-4f83-bffa-8b467289972d', '8abed111-cda2-4bb8-8d98-21291a85fb4f', '8bb9b1c6-2d40-4a69-bbd5-63ce152b1a91', '8f98e452-e174-4946-96ec-888cc3498f56', '8fdffd17-84fa-41f7-8b74-744578d4b9d9', '92ae627b-8aa4-4197-885e-603d4a1be8aa', '9852662c-7e9f-4c26-a6d3-6335b49b9b3b', 'a06ddcb5-dc9c-4bec-a0f8-40fd4ab20016', 'a25bbeae-86b9-4162-8570-d121845f26e9', 'a690c59b-72eb-40bb-b181-d9dc904f9ec1', 'a9b0a795-3b7e-40bb-b8ec-65280d149dba', 'ab9ddbb9-21bc-4a8e-be7f-036554b70db4', 'bea51786-4d47-4bcb-bf79-fc636f047cbb', 'c806a74d-f0ae-4473-86bd-facb92085eb1', 'c9064135-322e-4848-9de0-61011d067697', 'cb7e738f-bcb5-4bc5-9962-2608ea9c4094', 'cbc21e01-bb7e-4f1d-94c8-d4aac3b7247d', 'cf766627-3e32-45a1-8d62-865b6cc1d4ed', 'd0cf99ce-24e9-4912-9dc8-660b4ff57dcc', 'd2a536af-f23d-4c3e-8275-dc2e5116ab60', 'd715451c-6292-4d58-83f8-ffce115fca21', 'd76b57b8-10c4-43e4-94d1-bb66d1053bb4', 'dcffb511-131b-401c-a4b5-13810596bc42', 'df010c46-fc79-401c-a4a4-1fddb478acff', 'dfaf11b8-f835-4952-99e1-2b0279931b05', 'e0b8e823-4213-4919-b1ed-70395ad15df2', 'e9470495-097c-47b5-8e63-cd4f7370fa2d', 'e9fe89f8-780b-417f-a4ee-b48e0b0bb8ad', 'eda36826-ee68-414d-a773-cb41e1a1e2d6', 'edaa90fa-869b-4f3c-8b90-4ea41fe9a364', 'f163d78a-9539-43a3-aeb7-443559599342', 'f24e2ca9-3036-4839-9ff4-91bd4ecb5c91', 'f665642c-93e7-4886-bd6e-f94252d54e77', 'f7e5436c-855d-4bf9-b2de-94e9d0fd3052', 'f822b26a-715a-4154-b2a2-b11e38373fad', 'fe2e4ec5-cee4-4648-ae87-2f8dee7f42c8', 'ff3a7e68-feb7-4aca-983d-a8f8315851e7',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
async function start(dryRun: boolean) {
|
|
34
|
+
await SeedTools.loop({
|
|
35
|
+
query: Organization.select(),
|
|
36
|
+
batchSize: 50,
|
|
37
|
+
useTransactionPerBatch: true,
|
|
38
|
+
action: async (organization: Organization) => {
|
|
39
|
+
if (!relevantOrganizations.has(organization.id)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await createMissingGroupCategories(organization, dryRun);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function createMissingGroupCategories(organization: Organization, dryRun: boolean) {
|
|
49
|
+
const organizationPeriods = await OrganizationRegistrationPeriod.select()
|
|
50
|
+
.where('organizationId', organization.id)
|
|
51
|
+
// exclude current period
|
|
52
|
+
.andWhereNot('periodId', organization.periodId)
|
|
53
|
+
.fetch();
|
|
54
|
+
|
|
55
|
+
if (organizationPeriods.length > 1) {
|
|
56
|
+
console.error('found multiple periods for organization: ' + organization.id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const organizationPeriod of organizationPeriods) {
|
|
60
|
+
const groups: Group[] = await Group.select()
|
|
61
|
+
.where('organizationId', organization.id)
|
|
62
|
+
// only for period being looped
|
|
63
|
+
.andWhere('periodId', organizationPeriod.periodId)
|
|
64
|
+
.andWhere('type', GroupType.Membership)
|
|
65
|
+
.andWhere('deletedAt', null)
|
|
66
|
+
.fetch();
|
|
67
|
+
|
|
68
|
+
if (groups.length === 0) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const allCategories = organizationPeriod.settings.categories;
|
|
73
|
+
|
|
74
|
+
for (const group of groups) {
|
|
75
|
+
const category = allCategories.find(c => c.groupIds.includes(group.id));
|
|
76
|
+
|
|
77
|
+
// only create missing category if group is not in any category yet
|
|
78
|
+
if (!category) {
|
|
79
|
+
const oldGroupId = await getOldGroupId(group);
|
|
80
|
+
if (!relevantGroups.has(oldGroupId)) {
|
|
81
|
+
throw new Error('not expected');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const oldGroup = await Group.getByID(oldGroupId);
|
|
85
|
+
if (!oldGroup) {
|
|
86
|
+
throw new Error('Old group not found: ' + oldGroupId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { path, organizationPeriod: currentOrganizationPeriod } = await findPath(oldGroup);
|
|
90
|
+
|
|
91
|
+
const allArchivedCategories = organizationPeriod.settings.categories;
|
|
92
|
+
|
|
93
|
+
const archivedRoot = organizationPeriod.settings.rootCategory;
|
|
94
|
+
if (!archivedRoot) {
|
|
95
|
+
throw new Error('archived root category not found');
|
|
96
|
+
}
|
|
97
|
+
let thisLayer = getChildCategories(archivedRoot, allArchivedCategories);
|
|
98
|
+
const equalArchivedPath: GroupCategory[] = [organizationPeriod.settings.rootCategory!];
|
|
99
|
+
|
|
100
|
+
for (const currentPeriodCategory of path) {
|
|
101
|
+
if (currentPeriodCategory === currentOrganizationPeriod.settings.rootCategory) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// only exact match for last category is needed
|
|
106
|
+
const exactMatch = currentPeriodCategory !== path[path.length - 1];
|
|
107
|
+
const equalArchivedCategories = thisLayer.filter(archivedCategory => isEqualCategory(currentPeriodCategory, archivedCategory, exactMatch));
|
|
108
|
+
if (equalArchivedCategories.length > 1) {
|
|
109
|
+
throw new Error(`Found multiple equal categories (${currentPeriodCategory.settings.name}): ` + equalArchivedCategories.map(c => c.settings.name).join(', '));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (equalArchivedCategories.length === 0) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const equalArchivedCategory = equalArchivedCategories[0];
|
|
117
|
+
thisLayer = getChildCategories(equalArchivedCategory, allArchivedCategories);
|
|
118
|
+
equalArchivedPath.push(equalArchivedCategory);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// logging
|
|
122
|
+
console.log('organization: ', organization.name);
|
|
123
|
+
console.log('group: ', group.settings.name.toString());
|
|
124
|
+
logPath(path, 'current');
|
|
125
|
+
logPath(equalArchivedPath, 'archived');
|
|
126
|
+
|
|
127
|
+
if (path.length === equalArchivedPath.length) {
|
|
128
|
+
// complete match -> add group to last category
|
|
129
|
+
const lastCategory = equalArchivedPath[equalArchivedPath.length - 1];
|
|
130
|
+
|
|
131
|
+
if (lastCategory.groupIds.length) {
|
|
132
|
+
throw new Error('not expected last category has group ids');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// create new category
|
|
136
|
+
const newCategory = createCategoryForOriginalGroup(oldGroup, [group.id]);
|
|
137
|
+
lastCategory.categoryIds.push(newCategory.id);
|
|
138
|
+
organizationPeriod.settings.categories.push(newCategory);
|
|
139
|
+
|
|
140
|
+
if (!dryRun) {
|
|
141
|
+
await organizationPeriod.save();
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// should not happen
|
|
145
|
+
console.error('no complete match');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getChildCategories(category: GroupCategory, allCategories: GroupCategory[]) {
|
|
153
|
+
const results: GroupCategory[] = [];
|
|
154
|
+
|
|
155
|
+
for (const childId of category.categoryIds) {
|
|
156
|
+
const child = allCategories.find(c => c.id === childId);
|
|
157
|
+
if (!child) {
|
|
158
|
+
throw new Error('child category not found');
|
|
159
|
+
}
|
|
160
|
+
results.push(child);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function getOldGroupId(group: Group) {
|
|
167
|
+
const data = await V1GroupMigrationData.select().where('newGroupId', group.id).first(true);
|
|
168
|
+
return data.oldGroupId;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find the path from the old group to the root category.
|
|
173
|
+
*/
|
|
174
|
+
async function findPath(oldGroup: Group): Promise<{ path: GroupCategory[]; organizationPeriod: OrganizationRegistrationPeriod }> {
|
|
175
|
+
/**
|
|
176
|
+
* Returns all the parent categories of the group starting with the root category (= start) or null if not found.
|
|
177
|
+
*/
|
|
178
|
+
function getPathToGroup(groupId: string, start: GroupCategory, allCategories: GroupCategory[]): GroupCategory[] | null {
|
|
179
|
+
if (start.groupIds.includes(groupId)) {
|
|
180
|
+
return [start];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const childCategoryId of start.categoryIds) {
|
|
184
|
+
const childCategory = allCategories.find(c => c.id === childCategoryId);
|
|
185
|
+
if (childCategory) {
|
|
186
|
+
const childPath = getPathToGroup(groupId, childCategory, allCategories);
|
|
187
|
+
if (childPath !== null) {
|
|
188
|
+
return [start, ...childPath];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const organizationPeriod = await OrganizationRegistrationPeriod.select()
|
|
197
|
+
.where('organizationId', oldGroup.organizationId)
|
|
198
|
+
.where('periodId', oldGroup.periodId)
|
|
199
|
+
.first(true);
|
|
200
|
+
|
|
201
|
+
const root = organizationPeriod.settings.rootCategory;
|
|
202
|
+
if (!root) {
|
|
203
|
+
throw new Error('root category not found');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const path = getPathToGroup(oldGroup.id, root, organizationPeriod.settings.categories);
|
|
207
|
+
if (path === null) {
|
|
208
|
+
throw new Error('path not found');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
organizationPeriod,
|
|
213
|
+
path,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function logPath(path: GroupCategory[], prefix: string): void {
|
|
218
|
+
console.log(`${prefix} path: ${path.map(c => c.settings.name).join(' > ')}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isEqualCategory(currentPeriodCategory: GroupCategory, archivedPeriodCategory: GroupCategory, exactMatch: boolean) {
|
|
222
|
+
if (exactMatch) {
|
|
223
|
+
return archivedPeriodCategory.settings.name === currentPeriodCategory.settings.name;
|
|
224
|
+
}
|
|
225
|
+
return archivedPeriodCategory.settings.name.startsWith(currentPeriodCategory.settings.name);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createCategoryForOriginalGroup(originalGroup: Group, groupIds: string[]) {
|
|
229
|
+
return GroupCategory.create({
|
|
230
|
+
settings: GroupCategorySettings.create({
|
|
231
|
+
name: originalGroup.settings.name.toString(),
|
|
232
|
+
public: false,
|
|
233
|
+
maximumRegistrations: null,
|
|
234
|
+
}),
|
|
235
|
+
groupIds,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import type { RegistrationPeriod } from '@stamhoofd/models';
|
|
2
|
-
import { DocumentTemplate, DocumentTemplateFactory, GroupFactory, OrganizationFactory, RegistrationPeriodFactory } from '@stamhoofd/models';
|
|
3
|
-
import { migrateDocumentYears } from './1750090029-document-update-year.js';
|
|
4
|
-
import { Formatter } from '@stamhoofd/utility';
|
|
5
|
-
|
|
6
|
-
describe('migration.document-update-year', () => {
|
|
7
|
-
describe('should use most frequent year of groups', () => {
|
|
8
|
-
test('groups with date in period', async () => {
|
|
9
|
-
const organization = await new OrganizationFactory({
|
|
10
|
-
}).create();
|
|
11
|
-
|
|
12
|
-
const period1 = await new RegistrationPeriodFactory({
|
|
13
|
-
startDate: Formatter.luxon().set({ day: 1, month: 1, year: 2021 }).toJSDate(),
|
|
14
|
-
endDate: Formatter.luxon().set({ day: 31, month: 12, year: 2021 }).toJSDate(),
|
|
15
|
-
organization,
|
|
16
|
-
}).create();
|
|
17
|
-
|
|
18
|
-
organization.periodId = period1.id;
|
|
19
|
-
await organization.save();
|
|
20
|
-
|
|
21
|
-
const period2 = await new RegistrationPeriodFactory({
|
|
22
|
-
startDate: Formatter.luxon().set({ day: 1, month: 1, year: 2019 }).toJSDate(),
|
|
23
|
-
endDate: Formatter.luxon().set({ day: 31, month: 12, year: 2019 }).toJSDate(),
|
|
24
|
-
organization,
|
|
25
|
-
}).create();
|
|
26
|
-
|
|
27
|
-
const createGroup = async (startDate: Date, endDate: Date, period: RegistrationPeriod) => {
|
|
28
|
-
const group = await new GroupFactory({ organization, period }).create();
|
|
29
|
-
group.settings.startDate = startDate;
|
|
30
|
-
group.settings.endDate = endDate;
|
|
31
|
-
await group.save();
|
|
32
|
-
return group;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// groups
|
|
36
|
-
const group1 = await createGroup(Formatter.luxon().set({ day: 1, month: 1, year: 2021 }).toJSDate(), Formatter.luxon().set({ day: 31, month: 12, year: 2021 }).toJSDate(), period1);
|
|
37
|
-
const group2 = await createGroup(Formatter.luxon().set({ day: 1, month: 1, year: 2021 }).toJSDate(), Formatter.luxon().set({ day: 31, month: 12, year: 2021 }).toJSDate(), period1);
|
|
38
|
-
const group3 = await createGroup(Formatter.luxon().set({ day: 1, month: 1, year: 2019 }).toJSDate(), Formatter.luxon().set({ day: 31, month: 12, year: 2019 }).toJSDate(), period2);
|
|
39
|
-
|
|
40
|
-
// document created in 2021
|
|
41
|
-
const document1 = await new DocumentTemplateFactory({
|
|
42
|
-
groups: [group1, group2, group3],
|
|
43
|
-
year: 0,
|
|
44
|
-
}).create();
|
|
45
|
-
|
|
46
|
-
document1.createdAt = Formatter.luxon().set({ day: 1, month: 1, year: 2022 }).toJSDate();
|
|
47
|
-
await document1.save();
|
|
48
|
-
|
|
49
|
-
// document created in 2020
|
|
50
|
-
const document2 = await new DocumentTemplateFactory({
|
|
51
|
-
groups: [group1, group2, group3],
|
|
52
|
-
year: 0,
|
|
53
|
-
}).create();
|
|
54
|
-
|
|
55
|
-
document2.createdAt = Formatter.luxon().set({ day: 1, month: 1, year: 2021 }).toJSDate();
|
|
56
|
-
await document2.save();
|
|
57
|
-
|
|
58
|
-
// document created in 2019
|
|
59
|
-
const document3 = await new DocumentTemplateFactory({
|
|
60
|
-
groups: [group1, group2, group3],
|
|
61
|
-
year: 0,
|
|
62
|
-
}).create();
|
|
63
|
-
|
|
64
|
-
document3.createdAt = Formatter.luxon().set({ day: 1, month: 1, year: 2020 }).toJSDate();
|
|
65
|
-
await document3.save();
|
|
66
|
-
|
|
67
|
-
// act
|
|
68
|
-
await migrateDocumentYears();
|
|
69
|
-
|
|
70
|
-
// assert
|
|
71
|
-
const updatedDocument1 = await DocumentTemplate.getByID(document1.id);
|
|
72
|
-
const updatedDocument2 = await DocumentTemplate.getByID(document2.id);
|
|
73
|
-
const updatedDocument3 = await DocumentTemplate.getByID(document3.id);
|
|
74
|
-
|
|
75
|
-
// take most frequent year and prefer date of document creation
|
|
76
|
-
expect(updatedDocument1?.year).toBe(2021);
|
|
77
|
-
expect(updatedDocument2?.year).toBe(2021);
|
|
78
|
-
expect(updatedDocument3?.year).toBe(2020); // maxed at creation year
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('groups overlapping period', async () => {
|
|
82
|
-
const organization = await new OrganizationFactory({
|
|
83
|
-
}).create();
|
|
84
|
-
|
|
85
|
-
const period1 = await new RegistrationPeriodFactory({
|
|
86
|
-
startDate: Formatter.luxon().set({ day: 1, month: 1, year: 2021 }).toJSDate(),
|
|
87
|
-
endDate: Formatter.luxon().set({ day: 31, month: 12, year: 2021 }).toJSDate(),
|
|
88
|
-
organization,
|
|
89
|
-
}).create();
|
|
90
|
-
|
|
91
|
-
organization.periodId = period1.id;
|
|
92
|
-
await organization.save();
|
|
93
|
-
|
|
94
|
-
const period2 = await new RegistrationPeriodFactory({
|
|
95
|
-
startDate: Formatter.luxon().set({ day: 1, month: 1, year: 2020 }).toJSDate(),
|
|
96
|
-
endDate: Formatter.luxon().set({ day: 31, month: 12, year: 2020 }).toJSDate(),
|
|
97
|
-
organization,
|
|
98
|
-
}).create();
|
|
99
|
-
|
|
100
|
-
const period3 = await new RegistrationPeriodFactory({
|
|
101
|
-
startDate: Formatter.luxon().set({ day: 1, month: 1, year: 2019 }).toJSDate(),
|
|
102
|
-
endDate: Formatter.luxon().set({ day: 31, month: 12, year: 2019 }).toJSDate(),
|
|
103
|
-
organization,
|
|
104
|
-
}).create();
|
|
105
|
-
|
|
106
|
-
const createGroup = async (startDate: Date, endDate: Date, period: RegistrationPeriod) => {
|
|
107
|
-
const group = await new GroupFactory({ organization, period }).create();
|
|
108
|
-
group.settings.startDate = startDate;
|
|
109
|
-
group.settings.endDate = endDate;
|
|
110
|
-
await group.save();
|
|
111
|
-
return group;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// groups
|
|
115
|
-
const group1 = await createGroup(Formatter.luxon().set({ day: 1, month: 7, year: 2020 }).toJSDate(), Formatter.luxon().set({ day: 30, month: 6, year: 2021 }).toJSDate(), period1);
|
|
116
|
-
const group2 = await createGroup(Formatter.luxon().set({ day: 5, month: 8, year: 2020 }).toJSDate(), Formatter.luxon().set({ day: 3, month: 5, year: 2021 }).toJSDate(), period1);
|
|
117
|
-
const group3 = await createGroup(Formatter.luxon().set({ day: 1, month: 6, year: 2018 }).toJSDate(), Formatter.luxon().set({ day: 14, month: 11, year: 2019 }).toJSDate(), period3);
|
|
118
|
-
|
|
119
|
-
// document created in 2021
|
|
120
|
-
const document1 = await new DocumentTemplateFactory({
|
|
121
|
-
groups: [group1, group2, group3],
|
|
122
|
-
year: 0,
|
|
123
|
-
}).create();
|
|
124
|
-
|
|
125
|
-
document1.createdAt = Formatter.luxon().set({ day: 1, month: 1, year: 2022 }).toJSDate();
|
|
126
|
-
await document1.save();
|
|
127
|
-
|
|
128
|
-
// document created in 2020
|
|
129
|
-
const document2 = await new DocumentTemplateFactory({
|
|
130
|
-
groups: [group1, group2, group3],
|
|
131
|
-
year: 0,
|
|
132
|
-
}).create();
|
|
133
|
-
|
|
134
|
-
document2.createdAt = Formatter.luxon().set({ day: 1, month: 1, year: 2021 }).toJSDate();
|
|
135
|
-
await document2.save();
|
|
136
|
-
|
|
137
|
-
// document created in 2019
|
|
138
|
-
const document3 = await new DocumentTemplateFactory({
|
|
139
|
-
groups: [group1, group2, group3],
|
|
140
|
-
year: 0,
|
|
141
|
-
}).create();
|
|
142
|
-
|
|
143
|
-
document3.createdAt = Formatter.luxon().set({ day: 1, month: 1, year: 2020 }).toJSDate();
|
|
144
|
-
await document3.save();
|
|
145
|
-
|
|
146
|
-
// act
|
|
147
|
-
await migrateDocumentYears();
|
|
148
|
-
|
|
149
|
-
// assert
|
|
150
|
-
const updatedDocument1 = await DocumentTemplate.getByID(document1.id);
|
|
151
|
-
const updatedDocument2 = await DocumentTemplate.getByID(document2.id);
|
|
152
|
-
const updatedDocument3 = await DocumentTemplate.getByID(document3.id);
|
|
153
|
-
|
|
154
|
-
// take most frequent year and prefer date of document creation
|
|
155
|
-
expect(updatedDocument1?.year).toBe(2020);
|
|
156
|
-
// should take 2020 because document was created in 2020
|
|
157
|
-
expect(updatedDocument2?.year).toBe(2020);
|
|
158
|
-
expect(updatedDocument3?.year).toBe(2020);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('no groups should use year before creation', async () => {
|
|
163
|
-
// create document
|
|
164
|
-
const document = await new DocumentTemplateFactory({
|
|
165
|
-
groups: [],
|
|
166
|
-
year: 0,
|
|
167
|
-
}).create();
|
|
168
|
-
|
|
169
|
-
document.createdAt = Formatter.luxon().set({ day: 1, month: 1, year: 2025 }).toJSDate();
|
|
170
|
-
await document.save();
|
|
171
|
-
|
|
172
|
-
// act
|
|
173
|
-
await migrateDocumentYears();
|
|
174
|
-
|
|
175
|
-
// assert
|
|
176
|
-
const updatedDocument = await DocumentTemplate.getByID(document.id);
|
|
177
|
-
|
|
178
|
-
// year should be year of creation
|
|
179
|
-
expect(updatedDocument?.year).toBe(2024);
|
|
180
|
-
});
|
|
181
|
-
});
|