@stamhoofd/backend 2.120.5 → 2.121.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.
Files changed (61) hide show
  1. package/package.json +12 -12
  2. package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
  3. package/src/audit-logs/init.ts +2 -0
  4. package/src/crons/index.ts +2 -0
  5. package/src/crons/invoices.ts +166 -0
  6. package/src/crons/mollie-chargebacks.ts +87 -0
  7. package/src/crons.ts +47 -10
  8. package/src/email-recipient-loaders/payments.ts +84 -41
  9. package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
  11. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
  12. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
  13. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
  14. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
  15. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
  16. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
  17. package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
  18. package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
  19. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
  20. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
  21. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
  22. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
  23. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
  24. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
  25. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
  26. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
  27. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
  28. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
  29. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
  30. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
  31. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
  32. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
  33. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
  34. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
  35. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
  36. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
  37. package/src/helpers/AdminPermissionChecker.ts +11 -3
  38. package/src/helpers/AuthenticatedStructures.ts +94 -6
  39. package/src/helpers/FinancialSupportHelper.ts +21 -0
  40. package/src/helpers/RecordAnswerHelper.test.ts +746 -0
  41. package/src/helpers/RecordAnswerHelper.ts +116 -0
  42. package/src/helpers/StripeHelper.ts +2 -3
  43. package/src/helpers/ViesHelper.ts +7 -3
  44. package/src/seeds/1750090030-records-configuration.ts +68 -3
  45. package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
  46. package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
  47. package/src/services/BalanceItemService.ts +12 -16
  48. package/src/services/InvoiceService.ts +372 -72
  49. package/src/services/MollieService.ts +537 -0
  50. package/src/services/PaymentMandateService.ts +214 -0
  51. package/src/services/PaymentService.ts +578 -222
  52. package/src/services/PlatformMembershipService.ts +1 -1
  53. package/src/services/RegistrationService.ts +66 -5
  54. package/src/services/STPackageService.ts +0 -7
  55. package/src/services/data/invoice.hbs.html +686 -0
  56. package/src/sql-filters/groups.ts +11 -1
  57. package/src/sql-filters/payments.ts +5 -0
  58. package/src/sql-filters/registration-invitations.ts +90 -0
  59. package/src/sql-sorters/registration-invitations.ts +36 -0
  60. package/vitest.config.js +1 -0
  61. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
@@ -0,0 +1,168 @@
1
+ import type { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
2
+ import { PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
3
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
4
+ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
5
+ import type { RegistrationInvitation as RegistrationInvitationStruct } from '@stamhoofd/structures';
6
+ import { GroupType, PermissionLevel, RegistrationInvitationRequest } from '@stamhoofd/structures';
7
+
8
+ import { SimpleError } from '@simonbackx/simple-errors';
9
+ import { Group, Member, RegistrationInvitation } from '@stamhoofd/models';
10
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
11
+ import { Context } from '../../../helpers/Context.js';
12
+
13
+ type Params = Record<string, never>;
14
+ type Query = undefined;
15
+ type Body = PatchableArrayAutoEncoder<RegistrationInvitationRequest>;
16
+ type ResponseBody = RegistrationInvitationStruct[];
17
+
18
+ export class PatchRegistrationInvitationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+
20
+ bodyDecoder = new PatchableArrayDecoder(
21
+ RegistrationInvitationRequest as Decoder<RegistrationInvitationRequest>,
22
+ RegistrationInvitationRequest.patchType() as Decoder<AutoEncoderPatchType<RegistrationInvitationRequest>>,
23
+ StringDecoder);
24
+
25
+ protected doesMatch(request: Request): [true, Params] | [false] {
26
+ if (request.method !== 'PATCH') {
27
+ return [false];
28
+ }
29
+
30
+ const params = Endpoint.parseParameters(request.url, '/registration-invitations', {});
31
+
32
+ if (params) {
33
+ return [true, params as Params];
34
+ }
35
+ return [false];
36
+ }
37
+
38
+ async handle(request: DecodedRequest<Params, Query, Body>) {
39
+ const organization = await Context.setOrganizationScope();
40
+ await Context.authenticate();
41
+
42
+ // Fast throw first (more in depth checking for patches later)
43
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
44
+ throw Context.auth.error();
45
+ }
46
+
47
+ const invitations: RegistrationInvitation[] = [];
48
+ let duplicateCount = 0;
49
+
50
+ const puts = request.body.getPuts();
51
+ for (const { put } of puts) {
52
+ await this.checkCanCreateRegistrationInvitation(put, organization.id);
53
+
54
+ const invitation = new RegistrationInvitation();
55
+ invitation.id = put.id;
56
+ invitation.organizationId = organization.id;
57
+ invitation.groupId = put.groupId;
58
+ invitation.memberId = put.memberId;
59
+
60
+ try {
61
+ await invitation.save();
62
+ invitations.push(invitation);
63
+ } catch (e) {
64
+ // update if duplicate
65
+ if (e.code === 'ER_DUP_ENTRY') {
66
+ const duplicate = await RegistrationInvitation.select()
67
+ .where('groupId', invitation.groupId)
68
+ .andWhere('memberId', invitation.memberId)
69
+ .first(false);
70
+
71
+ if (duplicate) {
72
+ invitations.push(duplicate);
73
+ }
74
+
75
+ duplicateCount += 1;
76
+ continue;
77
+ } else {
78
+ throw e;
79
+ }
80
+ }
81
+ }
82
+
83
+ if (request.body.getPatches().length > 0) {
84
+ throw new SimpleError({
85
+ code: 'patch_not_supported',
86
+ statusCode: 405,
87
+ message: 'Patching invitations is not supported. Only puts and deletes are supported.',
88
+ });
89
+ }
90
+
91
+ for (const id of request.body.getDeletes()) {
92
+ const invitation = await RegistrationInvitation.getByID(id);
93
+ if (!invitation) {
94
+ throw new SimpleError({
95
+ code: 'not_found',
96
+ statusCode: 404,
97
+ message: 'Registration invitation not found',
98
+ });
99
+ }
100
+
101
+ // Anyone with write access to the group can delete invitations for the group
102
+ const group = await Group.getByID(invitation.groupId);
103
+
104
+ if (!group || !await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
105
+ throw Context.auth.error($t(`%1UN`));
106
+ }
107
+
108
+ await invitation.delete();
109
+ }
110
+
111
+ // show an error if all puts were duplicates
112
+ if (puts.length > 0 && puts.length === duplicateCount) {
113
+ throw new SimpleError({
114
+ code: 'duplicate_entry',
115
+ statusCode: 409,
116
+ message: 'Duplicate entry',
117
+ human: puts.length === 1 ? $t(`%1RS`) : $t(`%1RP`),
118
+ });
119
+ }
120
+
121
+ return new Response(
122
+ await AuthenticatedStructures.registrationInvitations(invitations),
123
+ );
124
+ }
125
+
126
+ /**
127
+ * Will throw if not allowed to invite.
128
+ * @param invitation
129
+ * @param organizationId id of organization to invite for, should match the organizationId in the invitation
130
+ */
131
+ private async checkCanCreateRegistrationInvitation(invitation: RegistrationInvitationRequest, organizationId: string) {
132
+ const group = await Group.getByID(invitation.groupId);
133
+
134
+ if (!group || group.organizationId !== organizationId || !await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
135
+ throw Context.auth.error($t(`%1ST`));
136
+ }
137
+
138
+ // cannot invite for waiting list
139
+ if (group.type === GroupType.WaitingList) {
140
+ throw new SimpleError({
141
+ code: 'bad_group',
142
+ statusCode: 400,
143
+ message: 'Not allowed to invite for waiting list',
144
+ });
145
+ }
146
+
147
+ const member = await Member.getByIdWithUsersAndRegistrations(invitation.memberId);
148
+
149
+ if (!member
150
+ // in userMode 'organization' we can only invite members from the same organization
151
+ || (STAMHOOFD.userMode === 'organization' && member.organizationId !== organizationId)
152
+ // read access is suficient
153
+ || !await Context.auth.canAccessMember(member, PermissionLevel.Read)
154
+ ) {
155
+ throw Context.auth.error($t(`%1Qv`));
156
+ }
157
+
158
+ // cannot invite if already registered
159
+ if (member.registrations.some(r => r.groupId === group.id && r.registeredAt !== null && r.deactivatedAt === null)) {
160
+ throw new SimpleError({
161
+ code: 'bad_group',
162
+ statusCode: 400,
163
+ message: 'The member is already registered for this group',
164
+ human: $t('%1S2'),
165
+ })
166
+ }
167
+ }
168
+ }
@@ -188,10 +188,25 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
188
188
  model.unitPrice = patch.unitPrice ?? model.unitPrice;
189
189
  model.amount = patch.amount ?? model.amount;
190
190
  model.dueAt = patch.dueAt === undefined ? model.dueAt : patch.dueAt;
191
+
192
+ const VATPercentageBefore = model.VATPercentage;
193
+ const includedBefore = model.VATIncluded;
194
+ const excemptBefore = model.VATExcempt
195
+
191
196
  model.VATIncluded = patch.VATIncluded === undefined ? model.VATIncluded : patch.VATIncluded;
192
197
  model.VATPercentage = patch.VATPercentage === undefined ? model.VATPercentage : patch.VATPercentage;
193
198
  model.VATExcempt = patch.VATExcempt === undefined ? model.VATExcempt : patch.VATExcempt;
194
199
 
200
+ if (model.priceInvoiced !== 0) {
201
+ if (model.VATPercentage !== VATPercentageBefore || model.VATIncluded !== includedBefore || model.VATExcempt !== excemptBefore) {
202
+ throw new SimpleError({
203
+ code: 'invoiced',
204
+ message: 'You cannot change VAT settings of balance items when the balance item has been invoiced',
205
+ human: $t('%1Rt')
206
+ })
207
+ }
208
+ }
209
+
195
210
  if ((patch.dueAt !== undefined || patch.unitPrice !== undefined) && model.dueAt && model.price < 0) {
196
211
  throw new SimpleError({
197
212
  code: 'invalid_price',
@@ -1,10 +1,9 @@
1
- import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
1
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
2
2
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { STPackage } from '@stamhoofd/models';
5
4
 
6
- import { Context } from '../../../helpers/Context.js';
7
- import { STPackageService } from '../../../services/STPackageService.js';
5
+ import { Context } from '../../../../helpers/Context.js';
6
+ import { STPackageService } from '../../../../services/STPackageService.js';
8
7
  type Params = { id: string };
9
8
  type Query = undefined;
10
9
  type ResponseBody = undefined;
@@ -0,0 +1,62 @@
1
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
2
+ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { Organization } from '@stamhoofd/models';
5
+ import { Context } from '../../../../helpers/Context.js';
6
+ import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
7
+
8
+ type Params = { id: string, sellingOrganizationId: string };
9
+ type Query = undefined;
10
+ type Body = undefined;
11
+ type ResponseBody = undefined
12
+
13
+ export class DeleteOrganizationMandateEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ protected doesMatch(request: Request): [true, Params] | [false] {
15
+ if (request.method !== 'DELETE') {
16
+ return [false];
17
+ }
18
+
19
+ const params = Endpoint.parseParameters(request.url, '/billing/@sellingOrganizationId/mandates/@id', {sellingOrganizationId: String, id: String});
20
+
21
+ if (params) {
22
+ return [true, params as Params];
23
+ }
24
+ return [false];
25
+ }
26
+
27
+ async handle(request: DecodedRequest<Params, Query, Body>) {
28
+ const payingOrganization = await Context.setOrganizationScope();
29
+ const { user } = await Context.authenticate();
30
+
31
+ const id = request.params.sellingOrganizationId;
32
+ if (!id) {
33
+ throw new SimpleError({
34
+ code: 'unavailable',
35
+ message: 'This is temporarily unavailable',
36
+ human: $t('%1Rz')
37
+ })
38
+ }
39
+
40
+ const sellingOrganization = await Organization.getByID(id);
41
+ if (!sellingOrganization || !sellingOrganization.active) {
42
+ throw new SimpleError({
43
+ statusCode: 404,
44
+ code: 'not_found',
45
+ message: 'Selling organization not found',
46
+ human: $t('%1R5'),
47
+ field: 'sellingOrganization'
48
+ })
49
+ }
50
+
51
+ await PaymentMandateService.deleteMandate({
52
+ mandateId: request.params.id,
53
+ sellingOrganization,
54
+ user,
55
+ payingOrganization
56
+ })
57
+
58
+ const r = new Response(undefined);
59
+ r.status = 201;
60
+ return r;
61
+ }
62
+ }
@@ -0,0 +1,56 @@
1
+ import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
2
+ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
3
+ import type { DetailedPayableBalanceCollection} from '@stamhoofd/structures';
4
+ import { PaymentStatus } from '@stamhoofd/structures';
5
+
6
+ import { BalanceItem, Payment } from '@stamhoofd/models';
7
+ import { SQL } from '@stamhoofd/sql';
8
+ import { Context } from '../../../../helpers/Context.js';
9
+ import { GetUserDetailedPayableBalanceEndpoint } from '../../../global/registration/GetUserDetailedPayableBalanceEndpoint.js';
10
+
11
+ type Params = Record<string, never>;
12
+ type Query = undefined;
13
+ type ResponseBody = DetailedPayableBalanceCollection;
14
+ type Body = undefined;
15
+
16
+ export class GetOrganizationDetailedPayableBalanceCollectionEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
+ protected doesMatch(request: Request): [true, Params] | [false] {
18
+ if (request.method !== 'GET') {
19
+ return [false];
20
+ }
21
+
22
+ const params = request.getVersion() >= 339
23
+ ? Endpoint.parseParameters(request.url, '/organization/payable-balance/detailed', {})
24
+ : (
25
+ request.getVersion() <= 334
26
+ ? Endpoint.parseParameters(request.url, '/organization/billing/status', {})
27
+ : Endpoint.parseParameters(request.url, '/organization/billing/status/detailed', {})
28
+ );
29
+
30
+ if (params) {
31
+ return [true, params as Params];
32
+ }
33
+ return [false];
34
+ }
35
+
36
+ async handle(_: DecodedRequest<Params, Query, Body>) {
37
+ const organization = await Context.setOrganizationScope();
38
+ await Context.authenticate();
39
+
40
+ // If the user has permission, we'll also search if he has access to the organization's key
41
+ if (!await Context.auth.canManageFinances(organization.id)) {
42
+ throw Context.auth.error();
43
+ }
44
+
45
+ const balanceItemModels = await BalanceItem.balanceItemsForOrganization(organization.id);
46
+
47
+ const paymentModels = await Payment.select()
48
+ .where('payingOrganizationId', organization.id)
49
+ .andWhere(
50
+ SQL.whereNot('status', PaymentStatus.Failed),
51
+ )
52
+ .fetch();
53
+
54
+ return new Response(await GetUserDetailedPayableBalanceEndpoint.getDetailedBillingStatus(balanceItemModels, paymentModels));
55
+ }
56
+ }
@@ -1,31 +1,24 @@
1
- import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
1
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
2
2
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
3
- import type { DetailedPayableBalanceCollection} from '@stamhoofd/structures';
4
- import { PaymentStatus } from '@stamhoofd/structures';
3
+ import { DetailedPayableBalance, PaymentStatus } from '@stamhoofd/structures';
5
4
 
6
- import { BalanceItem, Payment } from '@stamhoofd/models';
5
+ import { SimpleError } from '@simonbackx/simple-errors';
6
+ import { BalanceItem, Organization, Payment } from '@stamhoofd/models';
7
7
  import { SQL } from '@stamhoofd/sql';
8
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
8
9
  import { Context } from '../../../../helpers/Context.js';
9
- import { GetUserDetailedPayableBalanceEndpoint } from '../../../global/registration/GetUserDetailedPayableBalanceEndpoint.js';
10
10
 
11
- type Params = Record<string, never>;
11
+ type Params = { sellingOrganizationId: string };
12
12
  type Query = undefined;
13
- type ResponseBody = DetailedPayableBalanceCollection;
13
+ type ResponseBody = DetailedPayableBalance;
14
14
  type Body = undefined;
15
15
 
16
- export class GetOrganizationDetailedPayableBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+ export class GetOrganizationDetailedPayableBalancendpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
17
  protected doesMatch(request: Request): [true, Params] | [false] {
18
18
  if (request.method !== 'GET') {
19
19
  return [false];
20
20
  }
21
-
22
- const params = request.getVersion() >= 339
23
- ? Endpoint.parseParameters(request.url, '/organization/payable-balance/detailed', {})
24
- : (
25
- request.getVersion() <= 334
26
- ? Endpoint.parseParameters(request.url, '/organization/billing/status', {})
27
- : Endpoint.parseParameters(request.url, '/organization/billing/status/detailed', {})
28
- );
21
+ const params = Endpoint.parseParameters(request.url, '/billing/@sellingOrganizationId/payable-balance', {sellingOrganizationId: String});
29
22
 
30
23
  if (params) {
31
24
  return [true, params as Params];
@@ -33,7 +26,7 @@ export class GetOrganizationDetailedPayableBalanceEndpoint extends Endpoint<Para
33
26
  return [false];
34
27
  }
35
28
 
36
- async handle(_: DecodedRequest<Params, Query, Body>) {
29
+ async handle(request: DecodedRequest<Params, Query, Body>) {
37
30
  const organization = await Context.setOrganizationScope();
38
31
  await Context.authenticate();
39
32
 
@@ -42,15 +35,45 @@ export class GetOrganizationDetailedPayableBalanceEndpoint extends Endpoint<Para
42
35
  throw Context.auth.error();
43
36
  }
44
37
 
45
- const balanceItemModels = await BalanceItem.balanceItemsForOrganization(organization.id);
38
+ const id = request.params.sellingOrganizationId;
39
+ if (!id) {
40
+ throw new SimpleError({
41
+ code: 'unavailable',
42
+ message: 'This is temporarily unavailable',
43
+ human: $t('%1Rz')
44
+ })
45
+ }
46
+
47
+ const sellingOrganization = await Organization.getByID(id);
48
+ if (!sellingOrganization || !sellingOrganization.active) {
49
+ throw new SimpleError({
50
+ statusCode: 404,
51
+ code: 'not_found',
52
+ message: 'Selling organization not found',
53
+ human: $t('%1R5'),
54
+ field: 'sellingOrganization'
55
+ })
56
+ }
57
+
58
+ const balanceItemModels = await BalanceItem.balanceItemsForOrganization(organization.id, request.params.sellingOrganizationId);
46
59
 
47
60
  const paymentModels = await Payment.select()
48
61
  .where('payingOrganizationId', organization.id)
62
+ .where('organizationId', request.params.sellingOrganizationId)
49
63
  .andWhere(
50
64
  SQL.whereNot('status', PaymentStatus.Failed),
51
65
  )
52
66
  .fetch();
53
67
 
54
- return new Response(await GetUserDetailedPayableBalanceEndpoint.getDetailedBillingStatus(balanceItemModels, paymentModels));
68
+ const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
69
+ const payments = await AuthenticatedStructures.paymentsGeneral(paymentModels, false);
70
+
71
+ const balance = DetailedPayableBalance.create({
72
+ organization: await AuthenticatedStructures.organization(sellingOrganization),
73
+ balanceItems,
74
+ payments,
75
+ });
76
+
77
+ return new Response(balance);
55
78
  }
56
79
  }
@@ -0,0 +1,64 @@
1
+ import type { Decoder } from '@simonbackx/simple-encoding';
2
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
3
+ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
4
+ import { SimpleError } from '@simonbackx/simple-errors';
5
+ import { Organization } from '@stamhoofd/models';
6
+ import { OrganizationCheckout } from '@stamhoofd/structures';
7
+ import type { PaymentMandate } from '@stamhoofd/structures/PaymentMandate.js';
8
+ import { Context } from '../../../../helpers/Context.js';
9
+ import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
10
+
11
+ type Params = { sellingOrganizationId: string };
12
+ type Query = undefined;
13
+ type Body = undefined;
14
+ type ResponseBody = PaymentMandate[];
15
+
16
+ export class GetOrganizationMandatesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
+ queryDecoder = OrganizationCheckout as Decoder<Query>;
18
+
19
+ protected doesMatch(request: Request): [true, Params] | [false] {
20
+ if (request.method !== 'GET') {
21
+ return [false];
22
+ }
23
+
24
+ const params = Endpoint.parseParameters(request.url, '/billing/@sellingOrganizationId/mandates', {sellingOrganizationId: String});
25
+
26
+ if (params) {
27
+ return [true, params as Params];
28
+ }
29
+ return [false];
30
+ }
31
+
32
+ async handle(request: DecodedRequest<Params, Query, Body>) {
33
+ const payingOrganization = await Context.setOrganizationScope();
34
+ const { user } = await Context.authenticate();
35
+
36
+ const id = request.params.sellingOrganizationId;
37
+ if (!id) {
38
+ throw new SimpleError({
39
+ code: 'unavailable',
40
+ message: 'This is temporarily unavailable',
41
+ human: $t('%1Rz')
42
+ })
43
+ }
44
+
45
+ const sellingOrganization = await Organization.getByID(id);
46
+ if (!sellingOrganization || !sellingOrganization.active) {
47
+ throw new SimpleError({
48
+ statusCode: 404,
49
+ code: 'not_found',
50
+ message: 'Selling organization not found',
51
+ human: $t('%1R5'),
52
+ field: 'sellingOrganization'
53
+ })
54
+ }
55
+
56
+ const mandates = await PaymentMandateService.getMandates({
57
+ sellingOrganization,
58
+ user,
59
+ payingOrganization
60
+ })
61
+
62
+ return new Response(PaymentMandateService.groupByMandate(mandates).mandates);
63
+ }
64
+ }
@@ -3,13 +3,21 @@ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
3
3
  import { OrganizationPackagesStatus, STPackage as STPackageStruct } from '@stamhoofd/structures';
4
4
  import { Context } from '../../../../helpers/Context.js';
5
5
  import { STPackageService } from '../../../../services/STPackageService.js';
6
+ import type { Decoder} from '@simonbackx/simple-encoding';
7
+ import { AutoEncoder, BooleanDecoder, field } from '@simonbackx/simple-encoding';
6
8
 
7
9
  type Params = Record<string, never>;
8
- type Query = undefined;
10
+ class Query extends AutoEncoder {
11
+ @field({ decoder: BooleanDecoder })
12
+ includeExpired = false;
13
+ }
14
+
9
15
  type ResponseBody = OrganizationPackagesStatus;
10
16
  type Body = undefined;
11
17
 
12
18
  export class GetPackagesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ queryDecoder = Query as Decoder<Query>;
20
+
13
21
  protected doesMatch(request: Request): [true, Params] | [false] {
14
22
  if (request.method !== 'GET') {
15
23
  return [false];
@@ -23,7 +31,7 @@ export class GetPackagesEndpoint extends Endpoint<Params, Query, Body, ResponseB
23
31
  return [false];
24
32
  }
25
33
 
26
- async handle(_: DecodedRequest<Params, Query, Body>) {
34
+ async handle({query}: DecodedRequest<Params, Query, Body>) {
27
35
  const organization = await Context.setOrganizationScope();
28
36
  await Context.authenticate();
29
37
 
@@ -32,7 +40,7 @@ export class GetPackagesEndpoint extends Endpoint<Params, Query, Body, ResponseB
32
40
  throw Context.auth.error();
33
41
  }
34
42
 
35
- const packages = Context.auth.hasPlatformFullAccess() ? await STPackageService.getValidPackagesWithExpired(organization.id) : await STPackageService.getActivePackages(organization.id);
43
+ const packages = query.includeExpired ? await STPackageService.getValidPackagesWithExpired(organization.id) : await STPackageService.getActivePackages(organization.id);
36
44
 
37
45
  return new Response(OrganizationPackagesStatus.create({
38
46
  packages: packages.map(p => STPackageStruct.create(p)),