@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.18.0",
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.18.0",
42
+ "@stamhoofd/models": "^2.19.0",
43
43
  "@stamhoofd/queues": "^2.17.3",
44
44
  "@stamhoofd/sql": "^2.18.0",
45
- "@stamhoofd/structures": "^2.18.0",
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": "a47aa5bb6ad71d1e6a2f88bc203caa5acf4f497f"
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('paidAt', null)
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
+ };