@stamhoofd/backend 2.18.0 → 2.19.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 +4 -4
- package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +49 -0
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +10 -0
- package/src/endpoints/organization/dashboard/billing/GetDetailedBillingStatusEndpoint.ts +2 -3
- package/src/helpers/MembershipCharger.ts +126 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"@stamhoofd/backend-i18n": "^2.17.0",
|
|
40
40
|
"@stamhoofd/backend-middleware": "^2.17.0",
|
|
41
41
|
"@stamhoofd/email": "^2.17.0",
|
|
42
|
-
"@stamhoofd/models": "^2.
|
|
42
|
+
"@stamhoofd/models": "^2.19.0",
|
|
43
43
|
"@stamhoofd/queues": "^2.17.3",
|
|
44
44
|
"@stamhoofd/sql": "^2.18.0",
|
|
45
|
-
"@stamhoofd/structures": "^2.
|
|
45
|
+
"@stamhoofd/structures": "^2.19.0",
|
|
46
46
|
"@stamhoofd/utility": "^2.17.0",
|
|
47
47
|
"archiver": "^7.0.1",
|
|
48
48
|
"aws-sdk": "^2.885.0",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"postmark": "4.0.2",
|
|
61
61
|
"stripe": "^16.6.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "876e7b976122e1c2f67d4cceebe92ec2af2bc35a"
|
|
64
64
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
3
|
+
import { sleep } from '@stamhoofd/utility';
|
|
4
|
+
import { Context } from '../../../helpers/Context';
|
|
5
|
+
import { MembershipCharger } from '../../../helpers/MembershipCharger';
|
|
6
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
type Params = Record<string, never>;
|
|
10
|
+
type Query = Record<string, never>;
|
|
11
|
+
type Body = undefined;
|
|
12
|
+
type ResponseBody = undefined;
|
|
13
|
+
|
|
14
|
+
export class ChargeMembershipsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
15
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
16
|
+
if (request.method != "POST") {
|
|
17
|
+
return [false];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const params = Endpoint.parseParameters(request.url, "/admin/charge-memberships", {});
|
|
21
|
+
|
|
22
|
+
if (params) {
|
|
23
|
+
return [true, params as Params];
|
|
24
|
+
}
|
|
25
|
+
return [false];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
29
|
+
await Context.authenticate()
|
|
30
|
+
|
|
31
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
32
|
+
throw Context.auth.error()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (QueueHandler.isRunning('charge-memberships')) {
|
|
36
|
+
throw new SimpleError({
|
|
37
|
+
code: 'charge_pending',
|
|
38
|
+
message: 'Charge already pending',
|
|
39
|
+
human: 'Er is al een aanrekening bezig, even geduld.'
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
QueueHandler.schedule('charge-memberships', async () => {
|
|
44
|
+
await MembershipCharger.charge()
|
|
45
|
+
}).catch(console.error);
|
|
46
|
+
|
|
47
|
+
return new Response(undefined);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -2,6 +2,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
2
2
|
import { SQL, SQLAlias, SQLCount, SQLDistinct, SQLSelectAs, SQLSum } from '@stamhoofd/sql';
|
|
3
3
|
import { ChargeMembershipsSummary, ChargeMembershipsTypeSummary } from '@stamhoofd/structures';
|
|
4
4
|
import { Context } from '../../../helpers/Context';
|
|
5
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
type Params = Record<string, never>;
|
|
@@ -29,6 +30,14 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
|
|
|
29
30
|
if (!Context.auth.hasPlatformFullAccess()) {
|
|
30
31
|
throw Context.auth.error()
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
if (QueueHandler.isRunning('charge-memberships')) {
|
|
35
|
+
return new Response(
|
|
36
|
+
ChargeMembershipsSummary.create({
|
|
37
|
+
running: true
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
}
|
|
32
41
|
|
|
33
42
|
const query = SQL
|
|
34
43
|
.select(
|
|
@@ -74,6 +83,7 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
|
|
|
74
83
|
|
|
75
84
|
return new Response(
|
|
76
85
|
ChargeMembershipsSummary.create({
|
|
86
|
+
running: false,
|
|
77
87
|
memberships: memberships ?? 0,
|
|
78
88
|
members: members ?? 0,
|
|
79
89
|
price: price ?? 0,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
-
import { OrganizationDetailedBillingStatus, OrganizationDetailedBillingStatusItem, PaymentMethod } from "@stamhoofd/structures";
|
|
2
|
+
import { OrganizationDetailedBillingStatus, OrganizationDetailedBillingStatusItem, PaymentMethod, PaymentStatus } from "@stamhoofd/structures";
|
|
3
3
|
|
|
4
4
|
import { BalanceItem, Organization, Payment } from "@stamhoofd/models";
|
|
5
5
|
import { SQL } from "@stamhoofd/sql";
|
|
@@ -41,8 +41,7 @@ export class GetDetailedBillingStatusEndpoint extends Endpoint<Params, Query, Bo
|
|
|
41
41
|
const paymentModels = await Payment.select()
|
|
42
42
|
.where('payingOrganizationId', organization.id)
|
|
43
43
|
.andWhere(
|
|
44
|
-
SQL.whereNot('
|
|
45
|
-
.or('method', [PaymentMethod.Transfer, PaymentMethod.DirectDebit, PaymentMethod.PointOfSale, PaymentMethod.Unknown])
|
|
44
|
+
SQL.whereNot('status', PaymentStatus.Failed)
|
|
46
45
|
)
|
|
47
46
|
.fetch()
|
|
48
47
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
2
|
+
import { BalanceItem, Member, MemberPlatformMembership, Platform } from "@stamhoofd/models";
|
|
3
|
+
import { SQL, SQLOrderBy, SQLWhereSign } from "@stamhoofd/sql";
|
|
4
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemType } from "@stamhoofd/structures";
|
|
5
|
+
import { Formatter } from "@stamhoofd/utility";
|
|
6
|
+
|
|
7
|
+
export const MembershipCharger = {
|
|
8
|
+
async charge() {
|
|
9
|
+
console.log('Charging memberships...')
|
|
10
|
+
|
|
11
|
+
// Loop all
|
|
12
|
+
let lastId = "";
|
|
13
|
+
const platform = await Platform.getShared()
|
|
14
|
+
const chargeVia = platform.membershipOrganizationId
|
|
15
|
+
|
|
16
|
+
if (!chargeVia) {
|
|
17
|
+
throw new SimpleError({
|
|
18
|
+
code: 'missing_membership_organization',
|
|
19
|
+
message: 'Missing membershipOrganizationId',
|
|
20
|
+
human: 'Er is geen lokale groep verantwoordelijk voor de aanrekening van aansluitingen geconfigureerd'
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getType(id: string) {
|
|
25
|
+
return platform.config.membershipTypes.find(t => t.id === id)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let createdCount = 0;
|
|
29
|
+
let createdPrice = 0;
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line no-constant-condition
|
|
32
|
+
while (true) {
|
|
33
|
+
const memberships = await MemberPlatformMembership.select()
|
|
34
|
+
.where('id', SQLWhereSign.Greater, lastId)
|
|
35
|
+
.where('balanceItemId', null)
|
|
36
|
+
.limit(100)
|
|
37
|
+
.orderBy(
|
|
38
|
+
new SQLOrderBy({
|
|
39
|
+
column: SQL.column('id'),
|
|
40
|
+
direction: 'ASC'
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
.fetch();
|
|
44
|
+
|
|
45
|
+
if (memberships.length === 0) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const memberIds = Formatter.uniqueArray(memberships.map(m => m.memberId))
|
|
50
|
+
const members = await Member.getByIDs(...memberIds)
|
|
51
|
+
const createdBalanceItems: BalanceItem[] = []
|
|
52
|
+
|
|
53
|
+
for (const membership of memberships) {
|
|
54
|
+
// charge
|
|
55
|
+
if (membership.balanceItemId) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const type = getType(membership.membershipTypeId);
|
|
59
|
+
if (!type) {
|
|
60
|
+
console.error('Unknown membership type id ', membership.membershipTypeId)
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (membership.organizationId === chargeVia) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const member = members.find(m => m.id === membership.memberId)
|
|
69
|
+
|
|
70
|
+
if (!member) {
|
|
71
|
+
console.error('Unexpected missing member id ', membership.memberId, 'for membership', membership.id)
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const balanceItem = new BalanceItem();
|
|
76
|
+
balanceItem.unitPrice = membership.price
|
|
77
|
+
balanceItem.amount = 1
|
|
78
|
+
balanceItem.description = Formatter.dateNumber(membership.startDate, true) + " tot " + Formatter.dateNumber(membership.expireDate ?? membership.endDate, true)
|
|
79
|
+
balanceItem.relations = new Map([
|
|
80
|
+
[
|
|
81
|
+
BalanceItemRelationType.Member,
|
|
82
|
+
BalanceItemRelation.create({
|
|
83
|
+
id: member.id,
|
|
84
|
+
name: member.details.name
|
|
85
|
+
})
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
BalanceItemRelationType.MembershipType,
|
|
89
|
+
BalanceItemRelation.create({
|
|
90
|
+
id: type.id,
|
|
91
|
+
name: type.name
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
balanceItem.type = BalanceItemType.PlatformMembership
|
|
97
|
+
balanceItem.organizationId = chargeVia
|
|
98
|
+
balanceItem.payingOrganizationId = membership.organizationId
|
|
99
|
+
|
|
100
|
+
await balanceItem.save();
|
|
101
|
+
membership.balanceItemId = balanceItem.id;
|
|
102
|
+
await membership.save()
|
|
103
|
+
|
|
104
|
+
createdBalanceItems.push(balanceItem)
|
|
105
|
+
|
|
106
|
+
createdCount += 1;
|
|
107
|
+
createdPrice += membership.price
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await BalanceItem.updateOutstanding(createdBalanceItems)
|
|
111
|
+
|
|
112
|
+
if (memberships.length < 100) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const z = lastId;
|
|
117
|
+
lastId = memberships[memberships.length - 1].id;
|
|
118
|
+
|
|
119
|
+
if (lastId === z) {
|
|
120
|
+
throw new Error('Unexpected infinite loop found in MembershipCharger')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('Charged ' + Formatter.integer(createdCount) +' memberships, for a total value of ' + Formatter.price(createdPrice))
|
|
125
|
+
}
|
|
126
|
+
};
|