@reactionary/commercetools 0.6.6 → 0.6.8

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 (31) hide show
  1. package/capabilities/company-registration.capability.js +137 -0
  2. package/capabilities/company.capability.js +306 -0
  3. package/capabilities/employee-invitation.capability.js +398 -0
  4. package/capabilities/employee.capability.js +346 -0
  5. package/capabilities/index.js +3 -0
  6. package/capabilities/profile.capability.js +10 -0
  7. package/core/capability-descriptors.js +65 -1
  8. package/core/client.js +2 -2
  9. package/factories/checkout/checkout.factory.js +6 -1
  10. package/factories/company/company.factory.js +80 -0
  11. package/factories/company-registration/company-registration.factory.js +36 -0
  12. package/factories/employee/employee.factory.js +61 -0
  13. package/factories/employee-invitation/employee-invitation.factory.js +61 -0
  14. package/index.js +8 -0
  15. package/package.json +2 -2
  16. package/schema/capabilities.schema.js +4 -0
  17. package/schema/commercetools.schema.js +18 -1
  18. package/src/capabilities/company-registration.capability.d.ts +21 -0
  19. package/src/capabilities/company.capability.d.ts +30 -0
  20. package/src/capabilities/employee-invitation.capability.d.ts +34 -0
  21. package/src/capabilities/employee.capability.d.ts +33 -0
  22. package/src/capabilities/index.d.ts +3 -0
  23. package/src/core/capability-descriptors.d.ts +1 -1
  24. package/src/core/initialize.types.d.ts +20 -0
  25. package/src/factories/company/company.factory.d.ts +25 -0
  26. package/src/factories/company-registration/company-registration.factory.d.ts +25 -0
  27. package/src/factories/employee/employee.factory.d.ts +22 -0
  28. package/src/factories/employee-invitation/employee-invitation.factory.d.ts +94 -0
  29. package/src/index.d.ts +8 -0
  30. package/src/schema/capabilities.schema.d.ts +24 -1
  31. package/src/schema/commercetools.schema.d.ts +57 -0
@@ -0,0 +1,398 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result)
9
+ __defProp(target, key, result);
10
+ return result;
11
+ };
12
+ import * as crypto from "crypto";
13
+ import {
14
+ error,
15
+ EmployeeInvitationSchema,
16
+ EmployeeInvitationPaginatedListSchema,
17
+ EmployeeInvitationQueryListSchema,
18
+ EmployeeIssuedInvitationSchema,
19
+ EmployeeInvitationMutationAcceptInvitationSchema,
20
+ EmployeeInvitationMutationInviteEmployeeSchema,
21
+ EmployeeInvitationMutationRevokeInvitationSchema,
22
+ EmployeeInvitationCapability,
23
+ Reactionary,
24
+ success
25
+ } from "@reactionary/core";
26
+ import {
27
+ COMMERCERTOOLS_CUSTOM_OBJECT_CONTAINER_EMPLOYEE_INVITATIONS,
28
+ COMMERCERTOOLS_CUSTOM_OBJECT_CONTAINER_EMPLOYEE_INVITATIONS_INDEX
29
+ } from "../factories/employee-invitation/employee-invitation.factory.js";
30
+ class CommercetoolsEmployeeInvitationCapability extends EmployeeInvitationCapability {
31
+ config;
32
+ commercetools;
33
+ factory;
34
+ constructor(config, cache, context, commercetools, factory) {
35
+ super(cache, context);
36
+ this.config = config;
37
+ this.commercetools = commercetools;
38
+ this.factory = factory;
39
+ }
40
+ async getClient() {
41
+ const client = await this.commercetools.getAdminClient();
42
+ if (this.context.session.identityContext.identity.type !== "Registered") {
43
+ throw new Error("Only registered users can access employees capabilities");
44
+ }
45
+ return client.withProjectKey({ projectKey: this.config.projectKey }).asAssociate().withAssociateIdValue({
46
+ associateId: this.context.session.identityContext.identity.id.userId
47
+ });
48
+ }
49
+ async getBusinessUnit(key, extraExpands = []) {
50
+ const client = await this.getClient();
51
+ const response = await client.businessUnits().withKey({ key }).get({
52
+ queryArgs: {
53
+ expand: ["associates[*].customer", ...extraExpands]
54
+ }
55
+ }).execute().catch((err) => {
56
+ if (err.statusCode === 404) {
57
+ return null;
58
+ }
59
+ throw err;
60
+ });
61
+ return response ? response.body : null;
62
+ }
63
+ findAssociateByEmail(businessUnit, email) {
64
+ return businessUnit.associates.find(
65
+ (associate) => associate.customer.obj?.email === email
66
+ );
67
+ }
68
+ async getAdminClient() {
69
+ const client = await this.commercetools.getAdminClient();
70
+ return client.withProjectKey({ projectKey: this.config.projectKey });
71
+ }
72
+ makeKeyFromCompanyIdentifier(companyIdentifier) {
73
+ return "org-" + Buffer.from(companyIdentifier.taxIdentifier).toString("base64");
74
+ }
75
+ makeKeyFromEmail(email) {
76
+ return `email-` + Buffer.from(email).toString("base64");
77
+ }
78
+ async fetchInvitationIdsByKey(adminClient, key) {
79
+ const response = await adminClient.customObjects().withContainerAndKey({
80
+ container: COMMERCERTOOLS_CUSTOM_OBJECT_CONTAINER_EMPLOYEE_INVITATIONS_INDEX,
81
+ key
82
+ }).get().execute().catch((error2) => {
83
+ if (error2?.statusCode === 404) {
84
+ return null;
85
+ }
86
+ throw error2;
87
+ });
88
+ if (!response) {
89
+ return [];
90
+ }
91
+ return response.body.value.invitationIds;
92
+ }
93
+ async addInvitationToEntityIndex(adminClient, indexKey, invitationId) {
94
+ const existingIds = await this.fetchInvitationIdsByKey(adminClient, indexKey);
95
+ const newIds = Array.from(/* @__PURE__ */ new Set([...existingIds, invitationId]));
96
+ return adminClient.customObjects().post({
97
+ body: {
98
+ container: COMMERCERTOOLS_CUSTOM_OBJECT_CONTAINER_EMPLOYEE_INVITATIONS_INDEX,
99
+ key: indexKey,
100
+ value: {
101
+ invitationIds: newIds
102
+ }
103
+ }
104
+ }).execute().then(() => void 0);
105
+ }
106
+ async addInvitationToEmailIndex(adminClient, email, invitationId) {
107
+ return this.addInvitationToEntityIndex(adminClient, this.makeKeyFromEmail(email), invitationId);
108
+ }
109
+ async addInvitationToCompanyIndex(adminClient, companyIdentifier, invitationId) {
110
+ return this.addInvitationToEntityIndex(adminClient, this.makeKeyFromCompanyIdentifier(companyIdentifier), invitationId);
111
+ }
112
+ async updateInvitationStatus(adminClient, invitationKey, invitation, newStatus) {
113
+ if (!invitation) {
114
+ throw new Error(`Invitation with key ${invitationKey} not found`);
115
+ }
116
+ if (this.context.session.identityContext.identity.type !== "Registered") {
117
+ throw new Error("Only registered users can accept invitations");
118
+ }
119
+ await adminClient.customObjects().post({
120
+ body: {
121
+ container: COMMERCERTOOLS_CUSTOM_OBJECT_CONTAINER_EMPLOYEE_INVITATIONS,
122
+ key: invitationKey,
123
+ value: {
124
+ ...invitation.value,
125
+ status: newStatus,
126
+ ...newStatus === "accepted" ? {
127
+ acceptedBy: this.context.session.identityContext.identity.id.userId,
128
+ acceptedDate: (/* @__PURE__ */ new Date()).toISOString()
129
+ } : {},
130
+ lastUpdatedBy: this.context.session.identityContext.identity.id.userId,
131
+ lastUpdatedDate: (/* @__PURE__ */ new Date()).toISOString()
132
+ }
133
+ }
134
+ }).execute();
135
+ }
136
+ async fetchInvitations(adminClient, indexKey, onlyActive, paginationOptions) {
137
+ const result = [];
138
+ let allInviteIds = await this.fetchInvitationIdsByKey(adminClient, indexKey);
139
+ if (paginationOptions) {
140
+ const start = (paginationOptions.pageNumber - 1) * paginationOptions.pageSize;
141
+ const end = start + paginationOptions.pageSize;
142
+ allInviteIds = allInviteIds.slice(start, end);
143
+ }
144
+ for (const invitationId of allInviteIds) {
145
+ const invitationResponse = await adminClient.customObjects().withContainerAndKey({ container: COMMERCERTOOLS_CUSTOM_OBJECT_CONTAINER_EMPLOYEE_INVITATIONS, key: invitationId }).get().execute().catch((error2) => {
146
+ if (error2?.statusCode === 404) {
147
+ return null;
148
+ }
149
+ throw error2;
150
+ });
151
+ const invitation = invitationResponse?.body;
152
+ if (!invitation) {
153
+ continue;
154
+ }
155
+ if (onlyActive && invitation.value.status !== "invited") {
156
+ continue;
157
+ }
158
+ if (onlyActive && new Date(invitation.value.validUntil) < /* @__PURE__ */ new Date()) {
159
+ continue;
160
+ }
161
+ result.push(invitation);
162
+ }
163
+ return result;
164
+ }
165
+ async fetchAllInvitationsForCompany(adminClient, companyIdentifier, onlyActive, paginationOptions) {
166
+ return this.fetchInvitations(adminClient, this.makeKeyFromCompanyIdentifier(companyIdentifier), onlyActive, paginationOptions);
167
+ }
168
+ async fetchAllInvitationsForUser(adminClient, userEmail, onlyActive, paginationOptions) {
169
+ return this.fetchInvitations(adminClient, this.makeKeyFromEmail(userEmail), onlyActive, paginationOptions);
170
+ }
171
+ async fetchInvitationByKey(adminClient, invitationKey) {
172
+ const inviteResponse = await adminClient.customObjects().withContainerAndKey({ container: COMMERCERTOOLS_CUSTOM_OBJECT_CONTAINER_EMPLOYEE_INVITATIONS, key: invitationKey }).get().execute().catch((error2) => {
173
+ if (error2?.statusCode === 404) {
174
+ return null;
175
+ }
176
+ throw error2;
177
+ });
178
+ if (!inviteResponse) {
179
+ return null;
180
+ }
181
+ return inviteResponse.body;
182
+ }
183
+ inviteEmployeePayload(payload, tokenHash) {
184
+ const key = payload.company.taxIdentifier;
185
+ if (this.context.session.identityContext.identity.type !== "Registered") {
186
+ throw new Error("Only registered users can invite employees");
187
+ }
188
+ const currentUser = this.context.session.identityContext.identity.id.userId;
189
+ const inviteKey = crypto.randomUUID() + "-" + (/* @__PURE__ */ new Date()).getTime();
190
+ return {
191
+ container: COMMERCERTOOLS_CUSTOM_OBJECT_CONTAINER_EMPLOYEE_INVITATIONS,
192
+ key: inviteKey,
193
+ value: {
194
+ tokenHash,
195
+ businessUnitKey: key,
196
+ status: "invited",
197
+ email: payload.email,
198
+ role: payload.role,
199
+ company: payload.company,
200
+ validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
201
+ invitedBy: "" + currentUser,
202
+ invitedDate: (/* @__PURE__ */ new Date()).toISOString(),
203
+ lastUpdatedBy: "" + currentUser,
204
+ lastUpdatedDate: (/* @__PURE__ */ new Date()).toISOString()
205
+ }
206
+ };
207
+ }
208
+ async createTokenHash(rawToken) {
209
+ const tokenHashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawToken));
210
+ const tokenHash = Array.from(new Uint8Array(tokenHashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
211
+ return tokenHash;
212
+ }
213
+ async inviteEmployee(payload) {
214
+ const key = payload.company.taxIdentifier;
215
+ const businessUnit = await this.getBusinessUnit(key);
216
+ if (!businessUnit) {
217
+ return error({
218
+ type: "NotFound",
219
+ identifier: payload.company
220
+ });
221
+ }
222
+ const customer = this.findAssociateByEmail(businessUnit, payload.email);
223
+ if (customer) {
224
+ return error({
225
+ type: "InvalidInput",
226
+ error: `Customer with email ${payload.email} is already an associate of this company.`
227
+ });
228
+ }
229
+ const rawToken = crypto.randomUUID() + (/* @__PURE__ */ new Date()).getTime() + payload.email + crypto.randomUUID();
230
+ const tokenHash = await this.createTokenHash(rawToken);
231
+ const adminClient = await this.getAdminClient();
232
+ const addInviteResponse = await adminClient.customObjects().post({
233
+ body: this.inviteEmployeePayload(payload, tokenHash)
234
+ }).execute();
235
+ await this.addInvitationToEmailIndex(adminClient, payload.email, addInviteResponse.body.key);
236
+ await this.addInvitationToCompanyIndex(adminClient, payload.company, addInviteResponse.body.key);
237
+ const invite = addInviteResponse.body;
238
+ return success(this.factory.parseEmployeeIssuedInvitation(this.context, { invite, securityToken: rawToken }, payload));
239
+ }
240
+ async acceptInvitation(payload) {
241
+ if (this.context.session.identityContext.identity.type !== "Registered") {
242
+ return error({
243
+ type: "Generic",
244
+ message: "Only registered users can accept invitations"
245
+ });
246
+ }
247
+ const adminClient = await this.getAdminClient();
248
+ const invitation = await this.fetchInvitationByKey(adminClient, payload.invitationIdentifier.key);
249
+ if (!invitation) {
250
+ return error({
251
+ type: "NotFound",
252
+ identifier: payload.invitationIdentifier
253
+ });
254
+ }
255
+ if (invitation.value.status !== "invited") {
256
+ return error({
257
+ type: "InvalidInput",
258
+ error: `Invitation with key ${payload.invitationIdentifier.key} is not in an invited status and cannot be accepted.`
259
+ });
260
+ }
261
+ if (new Date(invitation.value.validUntil) < /* @__PURE__ */ new Date()) {
262
+ return error({
263
+ type: "InvalidInput",
264
+ error: `Invitation with key ${payload.invitationIdentifier.key} has expired and cannot be accepted.`
265
+ });
266
+ }
267
+ if (invitation.value.email !== payload.currentUserEmail) {
268
+ return error({
269
+ type: "InvalidInput",
270
+ error: `Invitation with key ${payload.invitationIdentifier.key} was sent to ${invitation.value.email} but the current user email is ${payload.currentUserEmail}. Only the invited email can accept the invitation.`
271
+ });
272
+ }
273
+ const expectedHash = await this.createTokenHash(payload.securityToken);
274
+ if (invitation.value.tokenHash !== expectedHash) {
275
+ return error({
276
+ type: "InvalidInput",
277
+ error: `Invitation token is invalid.`
278
+ });
279
+ }
280
+ const businessUnit = await adminClient.businessUnits().withKey({ key: invitation.value.company.taxIdentifier }).get().execute().catch((error2) => {
281
+ if (error2?.statusCode === 404) {
282
+ return null;
283
+ }
284
+ throw error2;
285
+ });
286
+ if (!businessUnit) {
287
+ return error({
288
+ type: "NotFound",
289
+ identifier: invitation.value.company
290
+ });
291
+ }
292
+ const associateId = this.context.session.identityContext.identity.id.userId;
293
+ await adminClient.businessUnits().withKey({ key: invitation.value.company.taxIdentifier }).post({
294
+ body: {
295
+ version: businessUnit.body.version,
296
+ actions: [
297
+ {
298
+ action: "addAssociate",
299
+ associate: {
300
+ customer: {
301
+ typeId: "customer",
302
+ id: associateId
303
+ },
304
+ associateRoleAssignments: [
305
+ {
306
+ associateRole: {
307
+ typeId: "associate-role",
308
+ key: this.factory.mapRole(invitation.value.role)
309
+ }
310
+ }
311
+ ]
312
+ }
313
+ }
314
+ ]
315
+ }
316
+ }).execute();
317
+ await this.updateInvitationStatus(adminClient, payload.invitationIdentifier.key, invitation, "accepted");
318
+ const updatedInvitation = await this.fetchInvitationByKey(
319
+ adminClient,
320
+ payload.invitationIdentifier.key
321
+ );
322
+ if (!updatedInvitation) {
323
+ return error({
324
+ type: "NotFound",
325
+ identifier: payload.invitationIdentifier
326
+ });
327
+ }
328
+ return success(this.factory.parseEmployeeInvitation(this.context, updatedInvitation));
329
+ }
330
+ async revokeInvitation(payload) {
331
+ if (this.context.session.identityContext.identity.type !== "Registered") {
332
+ return error({
333
+ type: "Generic",
334
+ message: "Only registered users can accept invitations"
335
+ });
336
+ }
337
+ const adminClient = await this.getAdminClient();
338
+ const invitation = await this.fetchInvitationByKey(adminClient, payload.invitationIdentifier.key);
339
+ if (!invitation) {
340
+ return error({
341
+ type: "NotFound",
342
+ identifier: payload.invitationIdentifier
343
+ });
344
+ }
345
+ if (invitation.value.status !== "invited") {
346
+ return error({
347
+ type: "InvalidInput",
348
+ error: `Invitation with key ${payload.invitationIdentifier.key} is not in an invited status and cannot be rejected.`
349
+ });
350
+ }
351
+ await this.updateInvitationStatus(adminClient, payload.invitationIdentifier.key, invitation, "revoked");
352
+ return success(void 0);
353
+ }
354
+ async listInvitations(payload) {
355
+ if (this.context.session.identityContext.identity.type !== "Registered") {
356
+ return error({
357
+ type: "Generic",
358
+ message: "Only registered users can accept invitations"
359
+ });
360
+ }
361
+ const adminClient = await this.getAdminClient();
362
+ if (payload.search.email) {
363
+ const invitations = await this.fetchAllInvitationsForUser(adminClient, payload.search.email, false, payload.search.paginationOptions);
364
+ return success(this.factory.parseEmployeeInvitationPaginatedList(this.context, invitations, payload));
365
+ }
366
+ if (payload.search.company) {
367
+ const invitations = await this.fetchAllInvitationsForCompany(adminClient, payload.search.company, false, payload.search.paginationOptions);
368
+ return success(this.factory.parseEmployeeInvitationPaginatedList(this.context, invitations, payload));
369
+ }
370
+ return success(this.factory.parseEmployeeInvitationPaginatedList(this.context, [], payload));
371
+ }
372
+ }
373
+ __decorateClass([
374
+ Reactionary({
375
+ inputSchema: EmployeeInvitationMutationInviteEmployeeSchema,
376
+ outputSchema: EmployeeIssuedInvitationSchema
377
+ })
378
+ ], CommercetoolsEmployeeInvitationCapability.prototype, "inviteEmployee", 1);
379
+ __decorateClass([
380
+ Reactionary({
381
+ inputSchema: EmployeeInvitationMutationAcceptInvitationSchema,
382
+ outputSchema: EmployeeInvitationSchema
383
+ })
384
+ ], CommercetoolsEmployeeInvitationCapability.prototype, "acceptInvitation", 1);
385
+ __decorateClass([
386
+ Reactionary({
387
+ inputSchema: EmployeeInvitationMutationRevokeInvitationSchema
388
+ })
389
+ ], CommercetoolsEmployeeInvitationCapability.prototype, "revokeInvitation", 1);
390
+ __decorateClass([
391
+ Reactionary({
392
+ inputSchema: EmployeeInvitationQueryListSchema,
393
+ outputSchema: EmployeeInvitationPaginatedListSchema
394
+ })
395
+ ], CommercetoolsEmployeeInvitationCapability.prototype, "listInvitations", 1);
396
+ export {
397
+ CommercetoolsEmployeeInvitationCapability
398
+ };