@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.
- package/package.json +12 -12
- package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
- package/src/audit-logs/init.ts +2 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/invoices.ts +166 -0
- package/src/crons/mollie-chargebacks.ts +87 -0
- package/src/crons.ts +47 -10
- package/src/email-recipient-loaders/payments.ts +84 -41
- package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
- package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
- package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
- package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
- package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
- package/src/helpers/AdminPermissionChecker.ts +11 -3
- package/src/helpers/AuthenticatedStructures.ts +94 -6
- package/src/helpers/FinancialSupportHelper.ts +21 -0
- package/src/helpers/RecordAnswerHelper.test.ts +746 -0
- package/src/helpers/RecordAnswerHelper.ts +116 -0
- package/src/helpers/StripeHelper.ts +2 -3
- package/src/helpers/ViesHelper.ts +7 -3
- package/src/seeds/1750090030-records-configuration.ts +68 -3
- package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
- package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
- package/src/services/BalanceItemService.ts +12 -16
- package/src/services/InvoiceService.ts +372 -72
- package/src/services/MollieService.ts +537 -0
- package/src/services/PaymentMandateService.ts +214 -0
- package/src/services/PaymentService.ts +578 -222
- package/src/services/PlatformMembershipService.ts +1 -1
- package/src/services/RegistrationService.ts +66 -5
- package/src/services/STPackageService.ts +0 -7
- package/src/services/data/invoice.hbs.html +686 -0
- package/src/sql-filters/groups.ts +11 -1
- package/src/sql-filters/payments.ts +5 -0
- package/src/sql-filters/registration-invitations.ts +90 -0
- package/src/sql-sorters/registration-invitations.ts +36 -0
- package/vitest.config.js +1 -0
- 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',
|
package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts
RENAMED
|
@@ -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 '
|
|
7
|
-
import { STPackageService } from '
|
|
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
|
|
4
|
-
import { PaymentStatus } from '@stamhoofd/structures';
|
|
3
|
+
import { DetailedPayableBalance, PaymentStatus } from '@stamhoofd/structures';
|
|
5
4
|
|
|
6
|
-
import {
|
|
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 =
|
|
11
|
+
type Params = { sellingOrganizationId: string };
|
|
12
12
|
type Query = undefined;
|
|
13
|
-
type ResponseBody =
|
|
13
|
+
type ResponseBody = DetailedPayableBalance;
|
|
14
14
|
type Body = undefined;
|
|
15
15
|
|
|
16
|
-
export class
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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)),
|