@stamhoofd/models 2.34.1 → 2.36.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/dist/src/migrations/1726054851-balance-item-price-pending.sql +2 -0
- package/dist/src/migrations/1726054852-cached-outstanding-balances.sql +13 -0
- package/dist/src/models/BalanceItem.d.ts +10 -1
- package/dist/src/models/BalanceItem.d.ts.map +1 -1
- package/dist/src/models/BalanceItem.js +77 -9
- package/dist/src/models/BalanceItem.js.map +1 -1
- package/dist/src/models/BalanceItemPayment.d.ts.map +1 -1
- package/dist/src/models/BalanceItemPayment.js +2 -6
- package/dist/src/models/BalanceItemPayment.js.map +1 -1
- package/dist/src/models/CachedOutstandingBalance.d.ts +35 -0
- package/dist/src/models/CachedOutstandingBalance.d.ts.map +1 -0
- package/dist/src/models/CachedOutstandingBalance.js +188 -0
- package/dist/src/models/CachedOutstandingBalance.js.map +1 -0
- package/dist/src/models/Member.d.ts +4 -0
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +8 -2
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/Organization.d.ts.map +1 -1
- package/dist/src/models/Organization.js.map +1 -1
- package/dist/src/models/Registration.d.ts.map +1 -1
- package/dist/src/models/Registration.js +2 -1
- package/dist/src/models/Registration.js.map +1 -1
- package/dist/src/models/index.d.ts +1 -0
- package/dist/src/models/index.d.ts.map +1 -1
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/index.js.map +1 -1
- package/package.json +2 -2
- package/src/migrations/1726054851-balance-item-price-pending.sql +2 -0
- package/src/migrations/1726054852-cached-outstanding-balances.sql +13 -0
- package/src/models/BalanceItem.ts +95 -14
- package/src/models/BalanceItemPayment.ts +2 -9
- package/src/models/CachedOutstandingBalance.ts +239 -0
- package/src/models/Member.ts +9 -2
- package/src/models/Organization.ts +2 -2
- package/src/models/Registration.ts +2 -2
- package/src/models/index.ts +1 -0
|
@@ -59,7 +59,7 @@ export class BalanceItemPayment extends Model {
|
|
|
59
59
|
static payment = new ManyToOneRelation(Payment, "payment")
|
|
60
60
|
|
|
61
61
|
async markPaid(this: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
|
|
62
|
-
// Update cached amountPaid of the balance item
|
|
62
|
+
// Update cached amountPaid of the balance item (this will get overwritten later, but we need it to calculate the status)
|
|
63
63
|
this.balanceItem.pricePaid += this.price
|
|
64
64
|
|
|
65
65
|
// Update status
|
|
@@ -69,6 +69,7 @@ export class BalanceItemPayment extends Model {
|
|
|
69
69
|
|
|
70
70
|
// Do logic of balance item
|
|
71
71
|
if (this.balanceItem.status === BalanceItemStatus.Paid && old !== BalanceItemStatus.Paid) {
|
|
72
|
+
// Only call markPaid once (if it wasn't (partially) paid before)
|
|
72
73
|
await this.balanceItem.markPaid(this.payment, organization)
|
|
73
74
|
} else {
|
|
74
75
|
await this.balanceItem.markUpdated(this.payment, organization)
|
|
@@ -79,14 +80,6 @@ export class BalanceItemPayment extends Model {
|
|
|
79
80
|
* Call this once a earlier succeeded payment is no longer succeeded
|
|
80
81
|
*/
|
|
81
82
|
async undoPaid(this: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
|
|
82
|
-
// Update cached amountPaid of the balance item
|
|
83
|
-
this.balanceItem.pricePaid -= this.price
|
|
84
|
-
|
|
85
|
-
// Update status
|
|
86
|
-
this.balanceItem.status = this.balanceItem.pricePaid >= this.balanceItem.price ? BalanceItemStatus.Paid : BalanceItemStatus.Pending;
|
|
87
|
-
|
|
88
|
-
await this.balanceItem.save();
|
|
89
|
-
|
|
90
83
|
await this.balanceItem.undoPaid(this.payment, organization)
|
|
91
84
|
}
|
|
92
85
|
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { column, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
2
|
+
import { SQL, SQLAlias, SQLCalculation, SQLMinusSign, SQLMultiplicationSign, SQLSelect, SQLSelectAs, SQLSum, SQLWhere } from '@stamhoofd/sql';
|
|
3
|
+
import { BalanceItemStatus } from '@stamhoofd/structures';
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { BalanceItem } from './BalanceItem';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export type CachedOutstandingBalanceType = 'organization'|'member'|'user'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Keeps track of how much a member/user owes or needs to be reimbursed.
|
|
12
|
+
*/
|
|
13
|
+
export class CachedOutstandingBalance extends Model {
|
|
14
|
+
static table = "cached_outstanding_balances"
|
|
15
|
+
|
|
16
|
+
@column({
|
|
17
|
+
primary: true, type: "string", beforeSave(value) {
|
|
18
|
+
return value ?? uuidv4();
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
id!: string;
|
|
22
|
+
|
|
23
|
+
@column({ type: "string" })
|
|
24
|
+
organizationId: string
|
|
25
|
+
|
|
26
|
+
@column({ type: "string" })
|
|
27
|
+
objectId: string
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Defines which field to select
|
|
31
|
+
* organization: payingOrganizationId
|
|
32
|
+
* member: member id
|
|
33
|
+
* user: all balance items with that user id, but without a member id
|
|
34
|
+
*/
|
|
35
|
+
@column({ type: "string" })
|
|
36
|
+
objectType: CachedOutstandingBalanceType
|
|
37
|
+
|
|
38
|
+
@column({ type: "integer" })
|
|
39
|
+
amount = 0
|
|
40
|
+
|
|
41
|
+
@column({ type: "integer" })
|
|
42
|
+
amountPending = 0
|
|
43
|
+
|
|
44
|
+
@column({
|
|
45
|
+
type: "datetime", beforeSave(old?: any) {
|
|
46
|
+
if (old !== undefined) {
|
|
47
|
+
return old;
|
|
48
|
+
}
|
|
49
|
+
const date = new Date()
|
|
50
|
+
date.setMilliseconds(0)
|
|
51
|
+
return date
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
createdAt: Date
|
|
55
|
+
|
|
56
|
+
@column({
|
|
57
|
+
type: "datetime", beforeSave() {
|
|
58
|
+
const date = new Date()
|
|
59
|
+
date.setMilliseconds(0)
|
|
60
|
+
return date
|
|
61
|
+
},
|
|
62
|
+
skipUpdate: true
|
|
63
|
+
})
|
|
64
|
+
updatedAt: Date
|
|
65
|
+
|
|
66
|
+
static async getForObjects(objectIds: string[], limitOrganizationId?: string|null): Promise<CachedOutstandingBalance[]> {
|
|
67
|
+
const query = this.select()
|
|
68
|
+
.where("objectId", objectIds);
|
|
69
|
+
|
|
70
|
+
if (limitOrganizationId) {
|
|
71
|
+
query.where("organizationId", limitOrganizationId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return await query.fetch();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static async updateForObjects(organizationId: string, objectIds: string[], objectType: CachedOutstandingBalanceType) {
|
|
78
|
+
switch (objectType) {
|
|
79
|
+
case 'organization':
|
|
80
|
+
await this.updateForOrganizations(organizationId, objectIds);
|
|
81
|
+
break;
|
|
82
|
+
case 'member':
|
|
83
|
+
await this.updateForMembers(organizationId, objectIds);
|
|
84
|
+
break;
|
|
85
|
+
case 'user':
|
|
86
|
+
await this.updateForUsers(organizationId, objectIds);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static async fetchForObjects(organizationId: string, objectIds: string[], columnName: string, customWhere?: SQLWhere) {
|
|
92
|
+
const query = SQL.select(
|
|
93
|
+
SQL.column(columnName),
|
|
94
|
+
new SQLSelectAs(
|
|
95
|
+
new SQLCalculation(
|
|
96
|
+
new SQLSum(
|
|
97
|
+
new SQLCalculation(
|
|
98
|
+
SQL.column('unitPrice'),
|
|
99
|
+
new SQLMultiplicationSign(),
|
|
100
|
+
SQL.column('amount')
|
|
101
|
+
)
|
|
102
|
+
),
|
|
103
|
+
new SQLMinusSign(),
|
|
104
|
+
new SQLSum(
|
|
105
|
+
SQL.column('pricePaid')
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
new SQLAlias('data__amount')
|
|
109
|
+
),
|
|
110
|
+
new SQLSelectAs(
|
|
111
|
+
new SQLSum(
|
|
112
|
+
SQL.column('pricePending')
|
|
113
|
+
),
|
|
114
|
+
new SQLAlias('data__amountPending')
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
.from(BalanceItem.table)
|
|
118
|
+
.where("organizationId", organizationId)
|
|
119
|
+
.where(columnName, objectIds)
|
|
120
|
+
.whereNot("status", BalanceItemStatus.Hidden)
|
|
121
|
+
.groupBy(SQL.column(columnName));
|
|
122
|
+
|
|
123
|
+
if (customWhere) {
|
|
124
|
+
query.where(customWhere);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await query.fetch();
|
|
128
|
+
|
|
129
|
+
const results: [string, {amount: number, amountPending: number}][] = [];
|
|
130
|
+
for (const row of result) {
|
|
131
|
+
if (!row["data"]) {
|
|
132
|
+
throw new Error("Invalid data namespace");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!row[BalanceItem.table]) {
|
|
136
|
+
throw new Error("Invalid "+ BalanceItem.table +" namespace");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const objectId = row[BalanceItem.table][columnName];
|
|
140
|
+
const amount = row["data"]["amount"];
|
|
141
|
+
const amountPending = row["data"]["amountPending"];
|
|
142
|
+
|
|
143
|
+
if (typeof objectId !== "string") {
|
|
144
|
+
throw new Error("Invalid objectId");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof amount !== "number") {
|
|
148
|
+
throw new Error("Invalid amount");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof amountPending !== "number") {
|
|
152
|
+
throw new Error("Invalid amountPending");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
results.push([objectId, {amount, amountPending}]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private static async setForResults(organizationId: string, result: [string, {amount: number, amountPending: number}][], objectType: CachedOutstandingBalanceType) {
|
|
162
|
+
if (result.length === 0) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const query = SQL.insert(this.table)
|
|
166
|
+
.columns(
|
|
167
|
+
"id",
|
|
168
|
+
"organizationId",
|
|
169
|
+
"objectId",
|
|
170
|
+
"objectType",
|
|
171
|
+
"amount",
|
|
172
|
+
"amountPending",
|
|
173
|
+
"createdAt",
|
|
174
|
+
"updatedAt"
|
|
175
|
+
)
|
|
176
|
+
.values(...result.map(([objectId, {amount, amountPending}]) => {
|
|
177
|
+
return [
|
|
178
|
+
uuidv4(),
|
|
179
|
+
organizationId,
|
|
180
|
+
objectId,
|
|
181
|
+
objectType,
|
|
182
|
+
amount,
|
|
183
|
+
amountPending,
|
|
184
|
+
new Date(),
|
|
185
|
+
new Date()
|
|
186
|
+
]
|
|
187
|
+
}))
|
|
188
|
+
.as('v')
|
|
189
|
+
.onDuplicateKeyUpdate(
|
|
190
|
+
SQL.assignment("amount", SQL.column("v", "amount")),
|
|
191
|
+
SQL.assignment("amountPending", SQL.column("v", "amountPending")),
|
|
192
|
+
SQL.assignment("updatedAt", SQL.column("v", "updatedAt"))
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
await query.insert()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static async updateForOrganizations(organizationId: string, organizationIds: string[]) {
|
|
199
|
+
if (organizationIds.length === 0) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const results = await this.fetchForObjects(organizationId, organizationIds, "payingOrganizationId");
|
|
203
|
+
await this.setForResults(organizationId, results, "organization");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static async updateForMembers(organizationId: string, memberIds: string[]) {
|
|
207
|
+
if (memberIds.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const results = await this.fetchForObjects(organizationId, memberIds, "memberId");
|
|
211
|
+
await this.setForResults(organizationId, results, "member");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static async updateForUsers(organizationId: string, userIds: string[]) {
|
|
215
|
+
if (userIds.length === 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const results = await this.fetchForObjects(organizationId, userIds, "userId", SQL.where("memberId", null));
|
|
219
|
+
await this.setForResults(organizationId, results, "user");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Experimental: needs to move to library
|
|
224
|
+
*/
|
|
225
|
+
static select() {
|
|
226
|
+
const transformer = (row: SQLResultNamespacedRow): CachedOutstandingBalance => {
|
|
227
|
+
const d = (this as typeof CachedOutstandingBalance & typeof Model).fromRow(row[this.table] as any) as CachedOutstandingBalance|undefined
|
|
228
|
+
|
|
229
|
+
if (!d) {
|
|
230
|
+
throw new Error("EmailTemplate not found")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return d;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const select = new SQLSelect(transformer, SQL.wildcard())
|
|
237
|
+
return select.from(SQL.table(this.table))
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/models/Member.ts
CHANGED
|
@@ -353,7 +353,7 @@ export class Member extends Model {
|
|
|
353
353
|
/**
|
|
354
354
|
* Fetch all members with their corresponding (valid) registrations or waiting lists and payments
|
|
355
355
|
*/
|
|
356
|
-
|
|
356
|
+
static async getMemberIdsWithRegistrationForUser(user: User): Promise<string[]> {
|
|
357
357
|
const query = SQL
|
|
358
358
|
.select('id')
|
|
359
359
|
.from(Member.table)
|
|
@@ -370,7 +370,14 @@ export class Member extends Model {
|
|
|
370
370
|
)
|
|
371
371
|
|
|
372
372
|
const data = await query.fetch()
|
|
373
|
-
return
|
|
373
|
+
return data.map((r) => r.members.id as string)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Fetch all members with their corresponding (valid) registrations or waiting lists and payments
|
|
378
|
+
*/
|
|
379
|
+
static async getMembersWithRegistrationForUser(user: User): Promise<MemberWithRegistrations[]> {
|
|
380
|
+
return this.getBlobByIds(...(await this.getMemberIdsWithRegistrationForUser(user)));
|
|
374
381
|
}
|
|
375
382
|
|
|
376
383
|
getStructureWithRegistrations(this: MemberWithRegistrations, forOrganization: null | boolean = null) {
|
|
@@ -3,7 +3,7 @@ import { DecodedRequest } from '@simonbackx/simple-endpoints';
|
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
4
|
import { I18n } from "@stamhoofd/backend-i18n";
|
|
5
5
|
import { Email, EmailInterfaceRecipient } from "@stamhoofd/email";
|
|
6
|
-
import {
|
|
6
|
+
import { Address, Country, DNSRecordStatus, EmailTemplateType, OrganizationEmail, OrganizationMetaData, OrganizationPrivateMetaData, Organization as OrganizationStruct, PaymentMethod, PaymentProvider, PrivatePaymentConfiguration, Recipient, Replacement, STPackageType, TransferSettings } from "@stamhoofd/structures";
|
|
7
7
|
import { AWSError } from 'aws-sdk';
|
|
8
8
|
import SES from 'aws-sdk/clients/sesv2';
|
|
9
9
|
import { PromiseResult } from 'aws-sdk/lib/request';
|
|
@@ -13,7 +13,7 @@ import { QueueHandler } from "@stamhoofd/queues";
|
|
|
13
13
|
import { validateDNSRecords } from "../helpers/DNSValidator";
|
|
14
14
|
import { getEmailBuilderForTemplate } from "../helpers/EmailBuilder";
|
|
15
15
|
import { OrganizationServerMetaData } from '../structures/OrganizationServerMetaData';
|
|
16
|
-
import {
|
|
16
|
+
import { OrganizationRegistrationPeriod, StripeAccount } from "./";
|
|
17
17
|
|
|
18
18
|
export class Organization extends Model {
|
|
19
19
|
static table = "organizations";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { column, Database, ManyToOneRelation, Model } from '@simonbackx/simple-database';
|
|
2
|
-
import { Email } from '@stamhoofd/email';
|
|
3
2
|
import { EmailTemplateType, GroupPrice, PaymentMethod, PaymentMethodHelper, Recipient, RegisterItemOption, Registration as RegistrationStructure, Replacement, StockReservation } from '@stamhoofd/structures';
|
|
4
3
|
import { Formatter } from '@stamhoofd/utility';
|
|
5
4
|
import { v4 as uuidv4 } from "uuid";
|
|
@@ -396,6 +395,7 @@ export class Registration extends Model {
|
|
|
396
395
|
type: EmailTemplateType.RegistrationTransferDetails,
|
|
397
396
|
group
|
|
398
397
|
},
|
|
398
|
+
type: "transactional",
|
|
399
399
|
recipients
|
|
400
400
|
})
|
|
401
401
|
}
|
|
@@ -436,7 +436,7 @@ export class Registration extends Model {
|
|
|
436
436
|
|
|
437
437
|
if (updated.shouldIncludeStock()) {
|
|
438
438
|
const groupStockReservations: StockReservation[] = [
|
|
439
|
-
// Group level stock
|
|
439
|
+
// Group level stock reservations (stored in the group)
|
|
440
440
|
StockReservation.create({
|
|
441
441
|
objectId: updated.groupPrice.id,
|
|
442
442
|
objectType: 'GroupPrice',
|
package/src/models/index.ts
CHANGED