@stamhoofd/backend 2.130.0 → 2.132.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/crons/index.ts +1 -0
- package/src/crons/stripe-payout-reports.ts +69 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +74 -3
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +6 -0
- package/src/endpoints/global/members/throwIfDrasticMemberDetailsChange.test.ts +96 -0
- package/src/endpoints/global/members/throwIfDrasticMemberDetailsChange.ts +58 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +68 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +7 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +43 -2
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +31 -2
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/stripe/GetStripePayoutsExportStatusEndpoint.ts +32 -0
- package/src/endpoints/organization/dashboard/stripe/StripePayoutsExportEndpoint.test.ts +103 -0
- package/src/endpoints/organization/dashboard/stripe/StripePayoutsExportEndpoint.ts +125 -0
- package/src/helpers/AuthenticatedStructures.ts +2 -2
- package/src/helpers/CheckSettlements.test.ts +190 -0
- package/src/helpers/CheckSettlements.ts +73 -45
- package/src/helpers/MembershipCharger.ts +26 -1
- package/src/helpers/RegistrationPeriodRelationBackfiller.test.ts +111 -0
- package/src/helpers/RegistrationPeriodRelationBackfiller.ts +125 -0
- package/src/helpers/StripePayoutExportData.ts +195 -0
- package/src/helpers/StripePayoutExportExcel.ts +280 -0
- package/src/helpers/StripePayoutReporter.test.ts +419 -0
- package/src/helpers/StripePayoutReporter.ts +585 -0
- package/src/seeds/1783400000-backfill-registration-period-relation.ts +13 -0
- package/src/seeds/1783502528-fix-registrations-with-platform-period.ts +426 -0
- package/src/services/uitpas/UitpasService.ts +1 -1
- package/src/services/uitpas/cancelTicketSales.ts +1 -1
- package/src/services/uitpas/checkPermissionsFor.ts +1 -1
- package/src/services/uitpas/getSocialTariffForEvent.ts +1 -1
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +1 -1
- package/src/services/uitpas/registerTicketSales.ts +1 -1
- package/src/services/uitpas/searchUitpasOrganizers.ts +2 -1
- package/tests/e2e/documents.test.ts +3 -1
- package/tests/helpers/MollieMocker.ts +73 -0
- package/tests/vitest.global.setup.ts +1 -1
- package/tests/vitest.setup.ts +4 -2
- package/vitest.config.js +10 -0
- /package/src/seeds/{1780665427-schedule-stock-updates.ts → 1783515690-schedule-stock-updates.ts} +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { CellValue } from '@stamhoofd/excel-writer';
|
|
2
|
+
import { ArchiverWriterAdapter, XlsxBuiltInNumberFormat, XlsxWriter } from '@stamhoofd/excel-writer';
|
|
3
|
+
import { Sorter } from '@stamhoofd/utility';
|
|
4
|
+
import { Writable } from 'node:stream';
|
|
5
|
+
|
|
6
|
+
import type { StripePayoutBreakdownData, StripePayoutData, StripePayoutExportData, StripePayoutItemData } from './StripePayoutExportData.js';
|
|
7
|
+
import { StripePayoutItemType } from './StripePayoutExportData.js';
|
|
8
|
+
|
|
9
|
+
function currencyCell(amount: number | null, width?: number): CellValue {
|
|
10
|
+
return {
|
|
11
|
+
value: amount === null ? null : amount / 1_0000,
|
|
12
|
+
width,
|
|
13
|
+
style: {
|
|
14
|
+
numberFormat: {
|
|
15
|
+
id: XlsxBuiltInNumberFormat.Currency2DecimalWithoutRed,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function dateCell(date: Date | null, width?: number): CellValue {
|
|
22
|
+
return {
|
|
23
|
+
value: date,
|
|
24
|
+
width,
|
|
25
|
+
style: {
|
|
26
|
+
numberFormat: {
|
|
27
|
+
id: XlsxBuiltInNumberFormat.DateSlash,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function textCell(value: string, width?: number): CellValue {
|
|
34
|
+
return { value, width };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const empty = textCell('');
|
|
38
|
+
|
|
39
|
+
export class StripePayoutExportExcel {
|
|
40
|
+
/**
|
|
41
|
+
* List of all items for every payout
|
|
42
|
+
*/
|
|
43
|
+
private static async writePayouts(writer: XlsxWriter, sheet: symbol, includedPayouts: StripePayoutBreakdownData[]) {
|
|
44
|
+
await writer.addRow(sheet, [
|
|
45
|
+
textCell('Datum', 13),
|
|
46
|
+
textCell('Uitbetaald bedrag', 16),
|
|
47
|
+
textCell('Naam', 45),
|
|
48
|
+
textCell('Bedrag', 13),
|
|
49
|
+
textCell('Beschrijving', 60),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
for (const breakdown of [...includedPayouts].sort((b, a) => Sorter.byDateValue(a.payout.arrivalDate, b.payout.arrivalDate))) {
|
|
53
|
+
for (const [index, item] of [...breakdown.items].sort((a, b) => Sorter.byStringValue(a.name, b.name)).entries()) {
|
|
54
|
+
await writer.addRow(sheet, [
|
|
55
|
+
index > 0 ? empty : dateCell(breakdown.payout.arrivalDate),
|
|
56
|
+
index > 0 ? empty : currencyCell(breakdown.payout.amount),
|
|
57
|
+
textCell(item.name),
|
|
58
|
+
currencyCell(item.amount),
|
|
59
|
+
textCell(item.description),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Include total line
|
|
64
|
+
const total = breakdown.items.reduce((total, item) => total + item.amount, 0);
|
|
65
|
+
await writer.addRow(sheet, [
|
|
66
|
+
empty,
|
|
67
|
+
empty,
|
|
68
|
+
textCell('Totaal'),
|
|
69
|
+
currencyCell(total),
|
|
70
|
+
textCell(total === breakdown.payout.amount ? '✓' : 'Klopt niet!'),
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
// Empty line
|
|
74
|
+
await writer.addRow(sheet, []);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Group by account + month (= payment reference): shows the connected payments and invoices
|
|
80
|
+
*/
|
|
81
|
+
private static async writeInvoices(writer: XlsxWriter, sheet: symbol, includedPayouts: StripePayoutBreakdownData[], allPayouts: StripePayoutBreakdownData[]) {
|
|
82
|
+
await writer.addRow(sheet, [
|
|
83
|
+
textCell('Referentie', 22),
|
|
84
|
+
textCell('Factuur', 35),
|
|
85
|
+
textCell('Factuurdatum', 14),
|
|
86
|
+
textCell('Factuurbedrag (incl. BTW)', 22),
|
|
87
|
+
textCell('Aangerekend bedrag', 18),
|
|
88
|
+
textCell('Uitbetalingsdatum', 16),
|
|
89
|
+
textCell('Uitbetaald bedrag', 16),
|
|
90
|
+
textCell('Opmerking', 25),
|
|
91
|
+
textCell('Klant', 30),
|
|
92
|
+
textCell('Ondernemingsnummer', 20),
|
|
93
|
+
textCell('BTW-nummer', 16),
|
|
94
|
+
textCell('Adres', 40),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// Group all data by account + reference
|
|
98
|
+
const groups = new Map<string, {
|
|
99
|
+
item: StripePayoutItemData;
|
|
100
|
+
items: { item: StripePayoutItemData; payout: StripePayoutData }[];
|
|
101
|
+
}>();
|
|
102
|
+
|
|
103
|
+
for (const breakdown of allPayouts) {
|
|
104
|
+
for (const item of breakdown.items) {
|
|
105
|
+
if (item.type !== StripePayoutItemType.Invoice) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const key = (item.accountId ?? '') + '-' + (item.reference ?? '');
|
|
110
|
+
const group = groups.get(key) ?? {
|
|
111
|
+
item,
|
|
112
|
+
items: [],
|
|
113
|
+
};
|
|
114
|
+
group.items.push({
|
|
115
|
+
item,
|
|
116
|
+
payout: breakdown.payout,
|
|
117
|
+
});
|
|
118
|
+
groups.set(key, group);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sortedGroups = [...groups.values()].sort((a, b) => Sorter.stack(
|
|
123
|
+
Sorter.byStringValue(a.item.reference ?? '', b.item.reference ?? ''),
|
|
124
|
+
Sorter.byStringValue(a.item.description, b.item.description),
|
|
125
|
+
));
|
|
126
|
+
|
|
127
|
+
for (const group of sortedGroups) {
|
|
128
|
+
// Skip groups that don't have any payout in the requested period
|
|
129
|
+
if (!group.items.some(({ payout }) => includedPayouts.some(p => p.payout.id === payout.id))) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const item = group.item;
|
|
134
|
+
const invoices = item.invoices;
|
|
135
|
+
const customer = invoices[0]?.customer ?? item.payments[0]?.customer ?? null;
|
|
136
|
+
|
|
137
|
+
const invoiceTotal = invoices.reduce((total, invoice) => total + invoice.totalWithVAT, 0);
|
|
138
|
+
const invoiceDate = invoices[0]?.invoicedAt ?? null;
|
|
139
|
+
|
|
140
|
+
for (const [index, { item: payoutItem, payout }] of [...group.items].sort((b, a) => Sorter.byDateValue(a.payout.arrivalDate, b.payout.arrivalDate)).entries()) {
|
|
141
|
+
await writer.addRow(sheet, [
|
|
142
|
+
index > 0 ? empty : textCell(item.reference ?? ''),
|
|
143
|
+
index > 0 ? empty : textCell(item.name),
|
|
144
|
+
index > 0 ? empty : dateCell(invoiceDate),
|
|
145
|
+
index > 0 ? empty : (invoices.length === 0 ? empty : currencyCell(invoiceTotal)),
|
|
146
|
+
index > 0 ? empty : (item.payments.length === 0 ? empty : currencyCell(item.paymentsTotal)),
|
|
147
|
+
dateCell(payout.arrivalDate),
|
|
148
|
+
currencyCell(payoutItem.amount),
|
|
149
|
+
empty,
|
|
150
|
+
index > 0 ? empty : textCell(customer?.dynamicName ?? item.description),
|
|
151
|
+
index > 0 ? empty : textCell(customer?.company?.companyNumber ?? '/'),
|
|
152
|
+
index > 0 ? empty : textCell(customer?.company?.VATNumber ?? '/'),
|
|
153
|
+
index > 0 ? empty : textCell(customer?.company?.address ? customer.company.address + '' : '/'),
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Include total line
|
|
158
|
+
const total = group.items.reduce((total, { item }) => total + item.amount, 0);
|
|
159
|
+
let remark = 'Nog geen betaling aangemaakt';
|
|
160
|
+
if (item.payments.length > 0) {
|
|
161
|
+
if (total !== item.paymentsTotal) {
|
|
162
|
+
remark = 'Nog onvolledig';
|
|
163
|
+
} else if (item.hasUninvoicedPayments || invoices.length === 0) {
|
|
164
|
+
remark = 'Nog niet gefactureerd';
|
|
165
|
+
} else {
|
|
166
|
+
remark = '✓';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await writer.addRow(sheet, [
|
|
171
|
+
empty,
|
|
172
|
+
empty,
|
|
173
|
+
empty,
|
|
174
|
+
empty,
|
|
175
|
+
empty,
|
|
176
|
+
textCell('Totaal'),
|
|
177
|
+
currencyCell(total),
|
|
178
|
+
textCell(remark),
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
// Empty line
|
|
182
|
+
await writer.addRow(sheet, []);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Group Stripe fees by month
|
|
188
|
+
*/
|
|
189
|
+
private static async writeStripeInvoices(writer: XlsxWriter, sheet: symbol, includedPayouts: StripePayoutBreakdownData[], allPayouts: StripePayoutBreakdownData[]) {
|
|
190
|
+
await writer.addRow(sheet, [
|
|
191
|
+
textCell('Periode', 25),
|
|
192
|
+
textCell('Afgehouden bedrag', 18),
|
|
193
|
+
textCell('Uitbetalingsdatum', 16),
|
|
194
|
+
textCell('Beschrijving', 25),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
// Group all data by Stripe invoice period
|
|
198
|
+
const groupedInvoices = new Map<string, { item: StripePayoutItemData; payout: StripePayoutData }[]>();
|
|
199
|
+
|
|
200
|
+
for (const breakdown of allPayouts) {
|
|
201
|
+
for (const item of breakdown.items) {
|
|
202
|
+
if (item.type !== StripePayoutItemType.StripeFees) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const groupName = item.description;
|
|
207
|
+
const list = groupedInvoices.get(groupName) ?? [];
|
|
208
|
+
groupedInvoices.set(groupName, list);
|
|
209
|
+
list.push({
|
|
210
|
+
item,
|
|
211
|
+
payout: breakdown.payout,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const [period, group] of groupedInvoices) {
|
|
217
|
+
// Do not include invoices that have no payout in the period
|
|
218
|
+
if (!group.find(g => includedPayouts.find(p => p.payout.id === g.payout.id))) {
|
|
219
|
+
// Likely not completely fetched: don't include it
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const [index, item] of group.entries()) {
|
|
224
|
+
await writer.addRow(sheet, [
|
|
225
|
+
index > 0 ? empty : textCell(period),
|
|
226
|
+
currencyCell(-item.item.amount),
|
|
227
|
+
dateCell(item.payout.arrivalDate),
|
|
228
|
+
empty,
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Include total line
|
|
233
|
+
const total = -group.reduce((total, item) => total + item.item.amount, 0);
|
|
234
|
+
await writer.addRow(sheet, [
|
|
235
|
+
empty,
|
|
236
|
+
currencyCell(total),
|
|
237
|
+
empty,
|
|
238
|
+
textCell('Totaal'),
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// Empty line
|
|
242
|
+
await writer.addRow(sheet, []);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static async export(e: StripePayoutExportData): Promise<Buffer> {
|
|
247
|
+
const chunks: Buffer[] = [];
|
|
248
|
+
const output = new Writable({
|
|
249
|
+
write(chunk: Buffer, _encoding, callback) {
|
|
250
|
+
chunks.push(chunk);
|
|
251
|
+
callback();
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
const finishPromise = new Promise<void>((resolve, reject) => {
|
|
255
|
+
output.on('finish', () => resolve());
|
|
256
|
+
output.on('error', reject);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const zipWriterAdapter = new ArchiverWriterAdapter(output);
|
|
260
|
+
const writer = new XlsxWriter(zipWriterAdapter);
|
|
261
|
+
|
|
262
|
+
const payoutsSheet = await writer.addSheet('Uitbetalingen');
|
|
263
|
+
const invoicesSheet = await writer.addSheet('Facturen');
|
|
264
|
+
const stripeInvoicesSheet = await writer.addSheet('Stripe Facturen');
|
|
265
|
+
await writer.ready();
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await this.writePayouts(writer, payoutsSheet, e.includedPayouts);
|
|
269
|
+
await this.writeInvoices(writer, invoicesSheet, e.includedPayouts, e.payouts);
|
|
270
|
+
await this.writeStripeInvoices(writer, stripeInvoicesSheet, e.includedPayouts, e.payouts);
|
|
271
|
+
await writer.close();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
await writer.abort();
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await finishPromise;
|
|
278
|
+
return Buffer.concat(chunks);
|
|
279
|
+
}
|
|
280
|
+
}
|