@stamhoofd/backend 2.57.1 → 2.59.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/index.ts +6 -1
- package/package.json +13 -13
- package/src/audit-logs/EventLogger.ts +30 -0
- package/src/audit-logs/GroupLogger.ts +95 -0
- package/src/audit-logs/MemberLogger.ts +24 -0
- package/src/audit-logs/MemberPlatformMembershipLogger.ts +57 -0
- package/src/audit-logs/MemberResponsibilityRecordLogger.ts +69 -0
- package/src/audit-logs/ModelLogger.ts +218 -0
- package/src/audit-logs/OrderLogger.ts +57 -0
- package/src/audit-logs/OrganizationLogger.ts +26 -0
- package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +77 -0
- package/src/audit-logs/PaymentLogger.ts +43 -0
- package/src/audit-logs/PlatformLogger.ts +13 -0
- package/src/audit-logs/RegistrationLogger.ts +53 -0
- package/src/audit-logs/RegistrationPeriodLogger.ts +21 -0
- package/src/audit-logs/StripeAccountLogger.ts +47 -0
- package/src/audit-logs/WebshopLogger.ts +35 -0
- package/src/crons.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +12 -24
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +4 -18
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -3
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +3 -18
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +0 -15
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +5 -2
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +5 -15
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +18 -28
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +6 -3
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -2
- package/src/helpers/AuthenticatedStructures.ts +16 -1
- package/src/helpers/Context.ts +8 -2
- package/src/helpers/MemberUserSyncer.ts +45 -40
- package/src/helpers/PeriodHelper.ts +31 -27
- package/src/helpers/TagHelper.ts +23 -20
- package/src/seeds/1722344162-update-membership.ts +2 -2
- package/src/seeds/1726572303-schedule-stock-updates.ts +2 -1
- package/src/services/AuditLogService.ts +89 -216
- package/src/services/BalanceItemPaymentService.ts +1 -1
- package/src/services/BalanceItemService.ts +14 -5
- package/src/services/MemberNumberService.ts +120 -0
- package/src/services/PaymentService.ts +199 -193
- package/src/services/PlatformMembershipService.ts +284 -0
- package/src/services/RegistrationService.ts +76 -27
- package/src/services/explainPatch.ts +528 -316
- package/src/helpers/MembershipHelper.ts +0 -54
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Model } from '@simonbackx/simple-database';
|
|
2
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
3
|
+
import { Group, Member, MemberPlatformMembership, Organization, Platform, Registration } from '@stamhoofd/models';
|
|
4
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
5
|
+
import { SQL } from '@stamhoofd/sql';
|
|
6
|
+
import { AuditLogSource } from '@stamhoofd/structures';
|
|
7
|
+
import { Formatter, Sorter } from '@stamhoofd/utility';
|
|
8
|
+
import { AuditLogService } from './AuditLogService';
|
|
9
|
+
import { MemberNumberService } from './MemberNumberService';
|
|
10
|
+
|
|
11
|
+
export class PlatformMembershipService {
|
|
12
|
+
static listen() {
|
|
13
|
+
// Listen for group changes
|
|
14
|
+
Model.modelEventBus.addListener(this, (event) => {
|
|
15
|
+
if (!(event.model instanceof Group)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if group has been deleted
|
|
20
|
+
if (event.type === 'deleted' || (event.type === 'updated' && (event.changedFields['deletedAt'] !== undefined || event.changedFields['defaultAgeGroupId'] !== undefined))) {
|
|
21
|
+
PlatformMembershipService.updateMembershipsForGroupId(event.model.id);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static async updateAll() {
|
|
27
|
+
console.log('Scheduling updateAllMemberships');
|
|
28
|
+
|
|
29
|
+
let c = 0;
|
|
30
|
+
let id: string = '';
|
|
31
|
+
const tag = 'updateAllMemberships';
|
|
32
|
+
const batch = 100;
|
|
33
|
+
|
|
34
|
+
QueueHandler.cancel(tag);
|
|
35
|
+
|
|
36
|
+
await QueueHandler.schedule(tag, async () => {
|
|
37
|
+
console.log('Starting updateAllMemberships');
|
|
38
|
+
await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
|
|
39
|
+
while (true) {
|
|
40
|
+
const rawMembers = await Member.where({
|
|
41
|
+
id: {
|
|
42
|
+
value: id,
|
|
43
|
+
sign: '>',
|
|
44
|
+
},
|
|
45
|
+
}, { limit: batch, sort: ['id'] });
|
|
46
|
+
|
|
47
|
+
if (rawMembers.length === 0) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const promises: Promise<any>[] = [];
|
|
52
|
+
|
|
53
|
+
for (const member of rawMembers) {
|
|
54
|
+
promises.push((async () => {
|
|
55
|
+
await PlatformMembershipService.updateMembershipsForId(member.id, true);
|
|
56
|
+
c++;
|
|
57
|
+
|
|
58
|
+
if (c % 10000 === 0) {
|
|
59
|
+
process.stdout.write(c + ' members updated\n');
|
|
60
|
+
}
|
|
61
|
+
})());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await Promise.all(promises);
|
|
65
|
+
id = rawMembers[rawMembers.length - 1].id;
|
|
66
|
+
|
|
67
|
+
if (rawMembers.length < batch) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static updateMembershipsForGroupId(id: string) {
|
|
76
|
+
QueueHandler.schedule('bulk-update-memberships', async () => {
|
|
77
|
+
console.log('Bulk updating memberships for group id ', id);
|
|
78
|
+
|
|
79
|
+
// Get all members that are registered in this group
|
|
80
|
+
const memberIds = (await SQL.select(
|
|
81
|
+
SQL.column('members', 'id'),
|
|
82
|
+
)
|
|
83
|
+
.from(SQL.table(Member.table))
|
|
84
|
+
.join(
|
|
85
|
+
SQL.leftJoin(
|
|
86
|
+
SQL.table(Registration.table),
|
|
87
|
+
).where(
|
|
88
|
+
SQL.column(Registration.table, 'memberId'),
|
|
89
|
+
SQL.column(Member.table, 'id'),
|
|
90
|
+
),
|
|
91
|
+
).where(
|
|
92
|
+
SQL.column(Registration.table, 'groupId'),
|
|
93
|
+
id,
|
|
94
|
+
).fetch()).flatMap(r => (r.members && (typeof r.members.id) === 'string') ? [r.members.id as string] : []);
|
|
95
|
+
|
|
96
|
+
for (const id of memberIds) {
|
|
97
|
+
await PlatformMembershipService.updateMembershipsForId(id);
|
|
98
|
+
}
|
|
99
|
+
}).catch((e) => {
|
|
100
|
+
console.error('Failed to update memberships for group id ', id, e);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static async updateMembershipsForId(id: string, silent = false) {
|
|
105
|
+
return await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
|
|
106
|
+
await QueueHandler.schedule('updateMemberships-' + id, async function (this: undefined) {
|
|
107
|
+
if (!silent) {
|
|
108
|
+
console.log('update memberships for id ', id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const me = await Member.getWithRegistrations(id);
|
|
112
|
+
if (!me) {
|
|
113
|
+
if (!silent) {
|
|
114
|
+
console.log('Skipping automatic membership for: ' + id, ' - member not found');
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const platform = await Platform.getShared();
|
|
119
|
+
const registrations = me.registrations.filter(r => r.group.periodId === platform.periodId && r.registeredAt && !r.deactivatedAt);
|
|
120
|
+
const now = new Date();
|
|
121
|
+
|
|
122
|
+
const defaultMemberships = registrations.flatMap((r) => {
|
|
123
|
+
if (!r.group.defaultAgeGroupId) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const defaultAgeGroup = platform.config.defaultAgeGroups.find(g => g.id === r.group.defaultAgeGroupId);
|
|
127
|
+
if (!defaultAgeGroup || !defaultAgeGroup.defaultMembershipTypeId) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const defaultMembership = platform.config.membershipTypes.find(m => m.id === defaultAgeGroup.defaultMembershipTypeId);
|
|
132
|
+
if (!defaultMembership) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const periodConfig = defaultMembership.periods.get(platform.periodId);
|
|
136
|
+
|
|
137
|
+
if (periodConfig === undefined) {
|
|
138
|
+
console.warn('Found default membership without period configuration', defaultMembership.id, platform.periodId);
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!(periodConfig.startDate <= now && periodConfig.endDate >= now)) {
|
|
143
|
+
// Do not add this membership automatically because it won't match as an active membership
|
|
144
|
+
// later on (edge case but otherwise we'll get duplicate memberships and add a new one every time)
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [{
|
|
149
|
+
registration: r,
|
|
150
|
+
membership: defaultMembership,
|
|
151
|
+
}];
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Get active memberships for this member that
|
|
155
|
+
const memberships = await MemberPlatformMembership.where({ memberId: me.id, periodId: platform.periodId });
|
|
156
|
+
const activeMemberships = memberships.filter(m => m.startDate <= now && m.endDate >= now && m.deletedAt === null);
|
|
157
|
+
const activeMembershipsUndeletable = activeMemberships.filter(m => !m.canDelete() || !m.generated);
|
|
158
|
+
|
|
159
|
+
if (defaultMemberships.length === 0) {
|
|
160
|
+
// Stop all active memberships that were added automatically
|
|
161
|
+
for (const membership of activeMemberships) {
|
|
162
|
+
if (membership.canDelete() && membership.generated) {
|
|
163
|
+
if (!silent) {
|
|
164
|
+
console.log('Removing membership because no longer registered member and not yet invoiced for: ' + me.id + ' - membership ' + membership.id);
|
|
165
|
+
}
|
|
166
|
+
membership.deletedAt = new Date();
|
|
167
|
+
await membership.save();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!silent) {
|
|
172
|
+
console.log('Skipping automatic membership for: ' + me.id, ' - no default memberships found');
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (activeMembershipsUndeletable.length) {
|
|
178
|
+
// Skip automatic additions
|
|
179
|
+
for (const m of activeMembershipsUndeletable) {
|
|
180
|
+
try {
|
|
181
|
+
await m.calculatePrice(me);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
// Ignore error: membership might not be available anymore
|
|
185
|
+
if (!silent) {
|
|
186
|
+
console.error('Failed to calculate price for undeletable membership', m.id, e);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
await m.save();
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Add the cheapest available membership
|
|
195
|
+
const organizations = await Organization.getByIDs(...Formatter.uniqueArray(defaultMemberships.map(m => m.registration.organizationId)));
|
|
196
|
+
|
|
197
|
+
const defaultMembershipsWithOrganization = defaultMemberships.map(({ membership, registration }) => {
|
|
198
|
+
const organizationId = registration.organizationId;
|
|
199
|
+
const organization = organizations.find(o => o.id === organizationId);
|
|
200
|
+
return { membership, registration, organization };
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const shouldApplyReducedPrice = me.details.shouldApplyReducedPrice;
|
|
204
|
+
|
|
205
|
+
const cheapestMembership = defaultMembershipsWithOrganization.sort(({ membership: a, registration: ar, organization: ao }, { membership: b, registration: br, organization: bo }) => {
|
|
206
|
+
const tagIdsA = ao?.meta.tags ?? [];
|
|
207
|
+
const tagIdsB = bo?.meta.tags ?? [];
|
|
208
|
+
const diff = a.getPrice(platform.periodId, now, tagIdsA, shouldApplyReducedPrice)! - b.getPrice(platform.periodId, now, tagIdsB, shouldApplyReducedPrice)!;
|
|
209
|
+
if (diff == 0) {
|
|
210
|
+
return Sorter.byDateValue(br.createdAt, ar.createdAt);
|
|
211
|
+
}
|
|
212
|
+
return diff;
|
|
213
|
+
})[0];
|
|
214
|
+
if (!cheapestMembership) {
|
|
215
|
+
throw new Error('No membership found');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check if already have the same membership
|
|
219
|
+
for (const m of activeMemberships) {
|
|
220
|
+
if (m.membershipTypeId === cheapestMembership.membership.id) {
|
|
221
|
+
// Update the price of this active membership (could have changed)
|
|
222
|
+
try {
|
|
223
|
+
await m.calculatePrice(me);
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
// Ignore error: membership might not be available anymore
|
|
227
|
+
if (!silent) {
|
|
228
|
+
console.error('Failed to calculate price for active membership', m.id, e);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await m.save();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const periodConfig = cheapestMembership.membership.periods.get(platform.periodId);
|
|
237
|
+
if (!periodConfig) {
|
|
238
|
+
console.error('Missing membership prices for membership type ' + cheapestMembership.membership.id + ' and period ' + platform.periodId);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Can we revive an earlier deleted membership?
|
|
243
|
+
if (!silent) {
|
|
244
|
+
console.log('Creating automatic membership for: ' + me.id + ' - membership type ' + cheapestMembership.membership.id);
|
|
245
|
+
}
|
|
246
|
+
const membership = new MemberPlatformMembership();
|
|
247
|
+
membership.memberId = me.id;
|
|
248
|
+
membership.membershipTypeId = cheapestMembership.membership.id;
|
|
249
|
+
membership.organizationId = cheapestMembership.registration.organizationId;
|
|
250
|
+
membership.periodId = platform.periodId;
|
|
251
|
+
|
|
252
|
+
membership.startDate = periodConfig.startDate;
|
|
253
|
+
membership.endDate = periodConfig.endDate;
|
|
254
|
+
membership.expireDate = periodConfig.expireDate;
|
|
255
|
+
membership.generated = true;
|
|
256
|
+
|
|
257
|
+
if (me.details.memberNumber === null) {
|
|
258
|
+
try {
|
|
259
|
+
await MemberNumberService.assignMemberNumber(me, membership);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
console.error(`Failed to assign member number for id ${me.id}: ${error.message}`);
|
|
263
|
+
// If the assignment of the member number fails the membership is not created but the member is registered
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await membership.calculatePrice(me);
|
|
269
|
+
await membership.save();
|
|
270
|
+
|
|
271
|
+
// This reasoning allows us to replace an existing membership with a cheaper one (not date based ones, but type based ones)
|
|
272
|
+
for (const toDelete of activeMemberships) {
|
|
273
|
+
if (toDelete.canDelete() && toDelete.generated) {
|
|
274
|
+
if (!silent) {
|
|
275
|
+
console.log('Removing membership because cheaper membership found for: ' + me.id + ' - membership ' + toDelete.id);
|
|
276
|
+
}
|
|
277
|
+
toDelete.deletedAt = new Date();
|
|
278
|
+
await toDelete.save();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ManyToOneRelation } from '@simonbackx/simple-database';
|
|
2
2
|
import { Document, Group, Member, Registration } from '@stamhoofd/models';
|
|
3
|
-
import {
|
|
4
|
-
import { GroupService } from './GroupService';
|
|
3
|
+
import { AuditLogSource, EmailTemplateType, StockReservation } from '@stamhoofd/structures';
|
|
5
4
|
import { AuditLogService } from './AuditLogService';
|
|
5
|
+
import { GroupService } from './GroupService';
|
|
6
|
+
import { PlatformMembershipService } from './PlatformMembershipService';
|
|
7
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
6
9
|
|
|
7
10
|
export const RegistrationService = {
|
|
8
11
|
async markValid(registrationId: string) {
|
|
@@ -20,9 +23,9 @@ export const RegistrationService = {
|
|
|
20
23
|
registration.deactivatedAt = null;
|
|
21
24
|
registration.canRegister = false;
|
|
22
25
|
await registration.save();
|
|
23
|
-
|
|
26
|
+
RegistrationService.scheduleStockUpdate(registration.id);
|
|
24
27
|
|
|
25
|
-
await
|
|
28
|
+
await PlatformMembershipService.updateMembershipsForId(registration.memberId);
|
|
26
29
|
|
|
27
30
|
await registration.sendEmailTemplate({
|
|
28
31
|
type: EmailTemplateType.RegistrationConfirmation,
|
|
@@ -36,22 +39,12 @@ export const RegistrationService = {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// Update group occupancy
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Create a log
|
|
42
|
-
if (member && group) {
|
|
43
|
-
await AuditLogService.log({
|
|
44
|
-
type: AuditLogType.MemberRegistered,
|
|
45
|
-
member,
|
|
46
|
-
group,
|
|
47
|
-
registration,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
42
|
+
await GroupService.updateOccupancy(registration.groupId);
|
|
50
43
|
|
|
51
44
|
return true;
|
|
52
45
|
},
|
|
53
46
|
|
|
54
|
-
async deactivate(registration: Registration,
|
|
47
|
+
async deactivate(registration: Registration, _group?: Group, _member?: Member) {
|
|
55
48
|
if (registration.deactivatedAt !== null) {
|
|
56
49
|
return;
|
|
57
50
|
}
|
|
@@ -59,20 +52,76 @@ export const RegistrationService = {
|
|
|
59
52
|
// Clear the registration
|
|
60
53
|
registration.deactivatedAt = new Date();
|
|
61
54
|
await registration.save();
|
|
62
|
-
|
|
55
|
+
RegistrationService.scheduleStockUpdate(registration.id);
|
|
56
|
+
|
|
57
|
+
await PlatformMembershipService.updateMembershipsForId(registration.memberId);
|
|
58
|
+
},
|
|
63
59
|
|
|
64
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Adds or removes the order to the stock of the webshop (if it wasn't already included). If amounts were changed, only those
|
|
62
|
+
* changes will get added
|
|
63
|
+
* Should always happen in the webshop-stock queue to prevent multiple webshop writes at the same time
|
|
64
|
+
* + in combination with validation and reading the webshop
|
|
65
|
+
*/
|
|
66
|
+
scheduleStockUpdate(id: string) {
|
|
67
|
+
QueueHandler.cancel('registration-stock-update-' + id);
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
|
|
70
|
+
await QueueHandler.schedule('registration-stock-update-' + id, async function (this: undefined) {
|
|
71
|
+
const updated = await Registration.getByID(id);
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
if (!updated) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Start with clearing all the stock reservations we've already made
|
|
78
|
+
if (updated.stockReservations) {
|
|
79
|
+
const groupIds = Formatter.uniqueArray(updated.stockReservations.flatMap(r => r.objectType === 'Group' ? [r.objectId] : []));
|
|
80
|
+
for (const groupId of groupIds) {
|
|
81
|
+
const stocks = StockReservation.filter('Group', groupId, updated.stockReservations);
|
|
82
|
+
|
|
83
|
+
// Technically we don't need to await this, but okay...
|
|
84
|
+
await Group.freeStockReservations(groupId, stocks);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (updated.shouldIncludeStock()) {
|
|
89
|
+
const groupStockReservations: StockReservation[] = [
|
|
90
|
+
// Group level stock reservations (stored in the group)
|
|
91
|
+
StockReservation.create({
|
|
92
|
+
objectId: updated.groupPrice.id,
|
|
93
|
+
objectType: 'GroupPrice',
|
|
94
|
+
amount: 1,
|
|
95
|
+
}),
|
|
96
|
+
...updated.options.map((o) => {
|
|
97
|
+
return StockReservation.create({
|
|
98
|
+
objectId: o.option.id,
|
|
99
|
+
objectType: 'GroupOption',
|
|
100
|
+
amount: o.amount,
|
|
101
|
+
});
|
|
102
|
+
}),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
await Group.applyStockReservations(updated.groupId, groupStockReservations);
|
|
106
|
+
|
|
107
|
+
updated.stockReservations = [
|
|
108
|
+
// Global level stock reservations (stored in each group)
|
|
109
|
+
StockReservation.create({
|
|
110
|
+
objectId: updated.groupId,
|
|
111
|
+
objectType: 'Group',
|
|
112
|
+
amount: 1,
|
|
113
|
+
children: groupStockReservations,
|
|
114
|
+
}),
|
|
115
|
+
];
|
|
116
|
+
await updated.save();
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (updated.stockReservations.length) {
|
|
120
|
+
updated.stockReservations = [];
|
|
121
|
+
await updated.save();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
75
124
|
});
|
|
76
|
-
}
|
|
125
|
+
}).catch(console.error);
|
|
77
126
|
},
|
|
78
127
|
};
|