@dalmore/api-contracts 1.0.0 → 1.0.2

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 (33) hide show
  1. package/common/helpers/index.ts +205 -0
  2. package/common/types/account-manager.types.ts +10 -0
  3. package/common/types/account.types.ts +59 -2
  4. package/common/types/activity.types.ts +38 -0
  5. package/common/types/asset.types.ts +3 -0
  6. package/common/types/auth.types.ts +37 -10
  7. package/common/types/batch-jobs.types.ts +0 -1
  8. package/common/types/bonus-tier.types.ts +32 -0
  9. package/common/types/common.types.ts +20 -1
  10. package/common/types/contract-helpers.ts +205 -0
  11. package/common/types/investor-account.types.ts +137 -66
  12. package/common/types/page.types.ts +1 -1
  13. package/common/types/phone.type.ts +1 -1
  14. package/common/types/queue.types.ts +0 -1
  15. package/common/types/secondary-trade.types.ts +8 -0
  16. package/common/types/trade.types.ts +83 -1
  17. package/common/types/user.types.ts +2 -0
  18. package/common/types/zip.type.ts +1 -1
  19. package/contracts/clients/accounts/index.ts +3 -0
  20. package/contracts/clients/auth/index.ts +18 -1
  21. package/contracts/clients/index.ts +2 -0
  22. package/contracts/clients/offerings/index.ts +4 -3
  23. package/contracts/clients/sites/index.ts +56 -0
  24. package/contracts/compliance/account-managers/index.ts +2 -0
  25. package/contracts/compliance/accounts/index.ts +4 -2
  26. package/contracts/compliance/bonus-tiers/index.ts +55 -0
  27. package/contracts/compliance/index.ts +2 -0
  28. package/contracts/compliance/investor-accounts/index.ts +4 -0
  29. package/contracts/issuers/accounts/index.ts +5 -0
  30. package/contracts/issuers/bonus-tiers/index.ts +18 -0
  31. package/contracts/issuers/investor-accounts/index.ts +22 -0
  32. package/package.json +12 -2
  33. package/common/types/reminder-config.types.ts +0 -40
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Contract helpers - minimal subset for API contracts package
3
+ * This file is copied directly to @dalmore/api-contracts
4
+ */
5
+ import { HttpStatus } from '@nestjs/common';
6
+ import parsePhoneNumberFromString, {
7
+ CountryCode,
8
+ PhoneNumber,
9
+ } from 'libphonenumber-js';
10
+ import { err, ok, Result } from 'neverthrow';
11
+ import { ErrorResult } from '../types/common.types';
12
+
13
+ /**
14
+ * Validates a US zip code format (12345 or 12345-6789)
15
+ */
16
+ export const validateUSZipCode = (zipCode: string): boolean => {
17
+ const regex = /^[0-9]{5}(?:-[0-9]{4})?$/;
18
+ return regex.test(zipCode);
19
+ };
20
+
21
+ /**
22
+ * Validates a Canadian postal code format (A1A 1A1)
23
+ */
24
+ export const validateCanadaZipCode = (zipCode: string): boolean => {
25
+ const regex = /^[A-Za-z]\d[A-Za-z] \d[A-Za-z]\d$/;
26
+ return regex.test(zipCode);
27
+ };
28
+
29
+ /**
30
+ * Normalizes a short date string (M/D/YY or M/D/YYYY) to MM/DD/YYYY format
31
+ */
32
+ export function normalizeShortDate(input: string): string {
33
+ const [month, day, year] = input.split('/');
34
+
35
+ if (!month || !day || !year) {
36
+ throw new Error('Invalid date format');
37
+ }
38
+
39
+ const paddedMonth = month.padStart(2, '0');
40
+ const paddedDay = day.padStart(2, '0');
41
+ const fullYear = year.length === 2 ? `20${year}` : year;
42
+
43
+ return `${paddedMonth}/${paddedDay}/${fullYear}`;
44
+ }
45
+
46
+ /**
47
+ * Converts a string into a URL-safe slug
48
+ */
49
+ export function slugify(input: string, exclude: boolean = false): string {
50
+ if (exclude) {
51
+ const excludeWords = ['llc', 'inc', 'corp', 'co'];
52
+ const negativeWordsList = [
53
+ 'www',
54
+ 'api',
55
+ 'admin',
56
+ 'dashboard',
57
+ 'auth',
58
+ 'cdn',
59
+ 'static',
60
+ 'blog',
61
+ 'support',
62
+ 'status',
63
+ 'mail',
64
+ 'smtp',
65
+ 'ftp',
66
+ 'ssh',
67
+ 'login',
68
+ 'register',
69
+ 'signup',
70
+ 'account',
71
+ 'accounts',
72
+ 'user',
73
+ 'users',
74
+ 'profile',
75
+ 'settings',
76
+ 'help',
77
+ 'docs',
78
+ 'documentation',
79
+ 'developer',
80
+ 'developers',
81
+ 'app',
82
+ 'apps',
83
+ 'test',
84
+ 'demo',
85
+ 'staging',
86
+ 'dev',
87
+ 'prod',
88
+ 'production',
89
+ 'beta',
90
+ 'alpha',
91
+ 'portal',
92
+ 'portals',
93
+ 'client',
94
+ 'clients',
95
+ 'investor',
96
+ 'investors',
97
+ 'issuer',
98
+ 'issuers',
99
+ 'compliance',
100
+ 'offering',
101
+ 'offerings',
102
+ 'trade',
103
+ 'trades',
104
+ ];
105
+
106
+ let slug = input
107
+ .toLowerCase()
108
+ .replace(/[^\w\s-]/g, '')
109
+ .replace(/[\s_-]+/g, '-')
110
+ .replace(/^-+|-+$/g, '');
111
+
112
+ excludeWords.forEach((word) => {
113
+ slug = slug.replace(new RegExp(`-${word}$`, 'i'), '');
114
+ });
115
+
116
+ if (negativeWordsList.includes(slug)) {
117
+ return 'new-account';
118
+ }
119
+
120
+ return slug;
121
+ }
122
+
123
+ return input
124
+ .toLowerCase()
125
+ .replace(/[^\w\s-]/g, '')
126
+ .replace(/[\s_-]+/g, '-')
127
+ .replace(/^-+|-+$/g, '');
128
+ }
129
+
130
+ type CurlOptions = {
131
+ query?: Record<string, string>;
132
+ headers?: Record<string, string>;
133
+ body?: Record<string, unknown>;
134
+ formData?: Record<string, string>;
135
+ };
136
+
137
+ export const authHeader: CurlOptions['headers'] = {
138
+ Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
139
+ };
140
+
141
+ const generateCurlExample = (
142
+ method: string,
143
+ path: string,
144
+ options: CurlOptions,
145
+ ) => {
146
+ const baseUrl = 'https://dalmore-client-portal-api-prod.onrender.com/api/v1';
147
+ let curl = `curl -X ${method.toUpperCase()} '${baseUrl}${path}`;
148
+ if (options.query) {
149
+ const queryString = new URLSearchParams(options.query).toString();
150
+ curl += `?${queryString}`;
151
+ }
152
+ curl += "'";
153
+ if (options.headers) {
154
+ Object.entries(options.headers).forEach(([key, value]) => {
155
+ curl += ` \\\n --header '${key}: ${value}'`;
156
+ });
157
+ }
158
+ if (options.body) {
159
+ curl += ` \\\n --header 'Content-Type: application/json'`;
160
+ curl += ` \\\n --data '${JSON.stringify(options.body)}'`;
161
+ }
162
+ if (options.formData) {
163
+ Object.entries(options.formData).forEach(([key, value]) => {
164
+ curl += ` \\\n --form '${key}=${value}'`;
165
+ });
166
+ }
167
+ return curl;
168
+ };
169
+
170
+ export const generateApiDescription = (
171
+ summary: string,
172
+ method: string,
173
+ path: string,
174
+ options: CurlOptions,
175
+ ) => {
176
+ const curlExample = generateCurlExample(method, path, options);
177
+ return `${summary}
178
+
179
+ ### Example curl request
180
+
181
+ \`\`\`bash
182
+ ${curlExample}
183
+ \`\`\``;
184
+ };
185
+
186
+ /**
187
+ * Normalizes a phone number to E.164 format
188
+ */
189
+ export function normalizePhoneNumber(
190
+ phoneNumber: string,
191
+ countryCode: CountryCode,
192
+ ): Result<PhoneNumber, ErrorResult> {
193
+ const parsedPhoneNumber = parsePhoneNumberFromString(
194
+ phoneNumber,
195
+ countryCode,
196
+ );
197
+
198
+ if (!parsedPhoneNumber || !parsedPhoneNumber.isValid()) {
199
+ return err({
200
+ status: HttpStatus.BAD_REQUEST,
201
+ message: 'Invalid phone number. Only numbers',
202
+ });
203
+ }
204
+ return ok(parsedPhoneNumber);
205
+ }
@@ -112,3 +112,13 @@ export const SetDefaultAccountManagerForAllAccountsResponse = z.object({
112
112
  export type SetDefaultAccountManagerForAllAccountsResponse = z.infer<
113
113
  typeof SetDefaultAccountManagerForAllAccountsResponse
114
114
  >;
115
+
116
+ export const AccountManagerDeleteQuery = z.object({
117
+ reassignTo: accountManagerIdSchema
118
+ .nullable()
119
+ .optional()
120
+ .openapi({ example: 'account_manager_01j5y5ghx5fg68d663j1fvy2x7' }),
121
+ });
122
+ export type AccountManagerDeleteQuery = z.infer<
123
+ typeof AccountManagerDeleteQuery
124
+ >;
@@ -47,7 +47,7 @@ export const UpdateAccountZod = z.object({
47
47
  export type UpdateAccountZod = z.infer<typeof UpdateAccountZod>;
48
48
 
49
49
  export const CreateAccountZod = z.object({
50
- name: z.string().min(1).max(100),
50
+ name: z.string().min(1).max(100).openapi({ example: 'Name' }),
51
51
  platform: z
52
52
  .nativeEnum(Platform)
53
53
  .optional()
@@ -56,7 +56,14 @@ export const CreateAccountZod = z.object({
56
56
  .nativeEnum(ManagedByType)
57
57
  .optional()
58
58
  .openapi({ example: ManagedByType.DALMORE }),
59
- approversCount: z.number().min(1).max(5).optional(),
59
+ approversCount: z.number().min(1).max(5).optional().openapi({ example: 2 }),
60
+ hubspotCompany: z
61
+ .string()
62
+ .url()
63
+ .max(2048)
64
+ .nullable()
65
+ .optional()
66
+ .openapi({ example: 'https://www.google.com' }),
60
67
  });
61
68
  export type CreateAccountZod = z.infer<typeof CreateAccountZod>;
62
69
 
@@ -135,6 +142,19 @@ export const PatchAccountZod = z.object({
135
142
  });
136
143
  export type PatchAccountZod = z.infer<typeof PatchAccountZod>;
137
144
 
145
+ export const CompliancePatchAccountZod = PatchAccountZod.extend({
146
+ hubspotCompany: z
147
+ .string()
148
+ .url()
149
+ .max(2048)
150
+ .nullable()
151
+ .optional()
152
+ .openapi({ example: 'https://www.google.com' }),
153
+ });
154
+ export type CompliancePatchAccountZod = z.infer<
155
+ typeof CompliancePatchAccountZod
156
+ >;
157
+
138
158
  export const PostAccountResponse = z.object({
139
159
  id: z.string(),
140
160
  });
@@ -166,6 +186,41 @@ export const AccountsSummaryFilterZod = z.object({
166
186
  status: z.nativeEnum(AccountStatus).optional(),
167
187
  from: dateSchema.optional().openapi({ example: 'MM/DD/YYYY' }),
168
188
  to: dateSchema.optional().openapi({ example: 'MM/DD/YYYY' }),
189
+ accountManagerIds: z
190
+ .string()
191
+ .optional()
192
+ .transform((val) => {
193
+ if (!val || typeof val !== 'string') return undefined;
194
+
195
+ const ids = val
196
+ .split(',')
197
+ .map((id: string) => id.trim())
198
+ .filter((id: string) => id.length > 0);
199
+
200
+ // Remove duplicates using Set
201
+ const uniqueIds = [...new Set(ids)];
202
+
203
+ return uniqueIds.length > 0 ? uniqueIds : undefined;
204
+ })
205
+ .pipe(
206
+ z
207
+ .array(
208
+ z.string().refine(
209
+ (value) => {
210
+ try {
211
+ const tid = TypeID.fromString(value);
212
+ return tid.getType() === 'account_manager';
213
+ } catch {
214
+ return false;
215
+ }
216
+ },
217
+ {
218
+ message: `Invalid account manager ID format. Must be a valid TypeID with "account_manager" prefix. Example: account_manager_01j5y5ghx5fg68d663j1fvy2x7`,
219
+ },
220
+ ),
221
+ )
222
+ .optional(),
223
+ ),
169
224
  });
170
225
  export type AccountsSummaryFilterZod = z.infer<typeof AccountsSummaryFilterZod>;
171
226
 
@@ -182,6 +237,8 @@ export const AccountsSummaryZod = z.object({
182
237
  offerings: z.number().int(),
183
238
  assets: z.number().int(),
184
239
  users: z.number().int(),
240
+ trades: z.number().int(),
241
+ accountManagerName: z.string(),
185
242
  createdOn: z.string(),
186
243
  status: z.string(),
187
244
  });
@@ -134,6 +134,8 @@ export enum ActivityTypeAction {
134
134
  RE_PROCESS = BaseStatus.RE_PROCESS,
135
135
  SAVE = BaseStatus.SAVE,
136
136
  ACCEPTED = BaseStatus.ACCEPTED,
137
+ JOIN = BaseStatus.JOIN,
138
+ RESTORE = BaseStatus.RESTORE,
137
139
  }
138
140
 
139
141
  export const MethodMap: { [key: string]: ActivityTypeAction } = {
@@ -215,6 +217,9 @@ export const ActivitiesFilters = z.object({
215
217
  targetId: z
216
218
  .string()
217
219
  .optional()
220
+ .openapi({
221
+ example: 'disbursement_01j6aqmtfyfwy9spjdcnh7yqk7',
222
+ })
218
223
  .refine(
219
224
  (value) => {
220
225
  try {
@@ -232,5 +237,38 @@ export const ActivitiesFilters = z.object({
232
237
  },
233
238
  ),
234
239
  search: z.string().max(50).optional(),
240
+ targetIds: z
241
+ .string()
242
+ .optional()
243
+ .openapi({
244
+ example:
245
+ 'disbursement_01j6aqmtfyfwy9spjdcnh7yqk7,disbursement_01j6aqmtfyfwy9spjdcnh7yqk8',
246
+ })
247
+ .transform((str) => {
248
+ if (!str) return [];
249
+ // Split by comma, trim whitespace, and remove duplicates using Set
250
+ const ids = str.split(',').map((id) => id.trim());
251
+ return Array.from(new Set(ids));
252
+ })
253
+ .refine(
254
+ (value) => {
255
+ if (!value || value.length === 0) return true;
256
+
257
+ // Validate each ID
258
+ return value.every((id) => {
259
+ try {
260
+ const tid = TypeID.fromString(id);
261
+ return Object.values(TargetTableConfig).some(
262
+ (config) => config.idPrefix === tid.getType(),
263
+ );
264
+ } catch {
265
+ return false;
266
+ }
267
+ });
268
+ },
269
+ {
270
+ message: `Invalid target IDs. All IDs must match the corresponding table's ID prefix. Valid prefix: ${TargetTableEnum.toString().toLowerCase()}. Example: disbursement_01j6aqmtfyfwy9spjdcnh7yqk7`,
271
+ },
272
+ ),
235
273
  });
236
274
  export type ActivitiesFilters = z.infer<typeof ActivitiesFilters>;
@@ -53,6 +53,7 @@ export const IAsset = IBaseEntity.extend({
53
53
  .nullable(), // Use z.lazy here
54
54
  template: z.nativeEnum(TemplateType),
55
55
  tiers: z.array(z.number().positive()).nullable(),
56
+ enableBonus: z.boolean(),
56
57
  });
57
58
 
58
59
  export type IAsset = z.infer<typeof IAsset>;
@@ -105,6 +106,7 @@ const PostAssetBase = z.object({
105
106
  .default(TemplateType.STANDARD)
106
107
  .openapi({ example: TemplateType.STANDARD }),
107
108
  tiers: z.array(z.number().positive()).nullable().optional(),
109
+ enableBonus: z.boolean().default(false).openapi({ example: false }),
108
110
  });
109
111
 
110
112
  const postAssetRefinement = (data: any, ctx: any) => {
@@ -237,6 +239,7 @@ export const PutAsset = z.object({
237
239
  .nullable()
238
240
  .optional(),
239
241
  tiers: z.array(z.number().positive()).nullable().optional(),
242
+ enableBonus: z.boolean().optional().openapi({ example: false }),
240
243
  });
241
244
 
242
245
  export type PutAsset = z.infer<typeof PutAsset>;
@@ -49,14 +49,14 @@ const BaseAuthBody = z.object({
49
49
  firstName: z
50
50
  .string()
51
51
  .min(2, 'firstName is less than 2 characters')
52
- .max(20, 'firstName is more than 2 characters')
52
+ .max(20, 'firstName is more than 20 characters')
53
53
  .openapi({
54
54
  example: 'Neil',
55
55
  }),
56
56
  lastName: z
57
57
  .string()
58
58
  .min(2, 'lastName is less than 2 characters')
59
- .max(20, 'lastName is more than 2 characters')
59
+ .max(20, 'lastName is more than 20 characters')
60
60
  .openapi({
61
61
  example: 'Armstrong',
62
62
  }),
@@ -201,7 +201,8 @@ export const AccessTokenResponse = z.object({
201
201
  });
202
202
  export type AccessTokenResponse = z.infer<typeof AccessTokenResponse>;
203
203
 
204
- export const IRegisterIssuerBodyZod = BaseAuthBody.extend({
204
+ // Base schema for issuer registration (shared fields without refinements)
205
+ const BaseRegisterIssuerFields = BaseAuthBody.extend({
205
206
  confirmPassword: PasswordSchema,
206
207
  accountName: z
207
208
  .string()
@@ -212,6 +213,17 @@ export const IRegisterIssuerBodyZod = BaseAuthBody.extend({
212
213
  })
213
214
  .nullable()
214
215
  .optional(),
216
+ accountType: z
217
+ .nativeEnum(ManagedByType)
218
+ .optional()
219
+ .default(ManagedByType.DALMORE)
220
+ .openapi({
221
+ example: ManagedByType.DALMORE,
222
+ }),
223
+ });
224
+
225
+ // Full issuer registration schema with code (for invite-based registration)
226
+ export const IRegisterIssuerBodyZod = BaseRegisterIssuerFields.extend({
215
227
  code: z
216
228
  .string()
217
229
  .refine(isBase64, {
@@ -221,13 +233,6 @@ export const IRegisterIssuerBodyZod = BaseAuthBody.extend({
221
233
  .openapi({
222
234
  example: 'eyJpdiI6InhhTFIyVG9ETnhnUzQ1QzUyNzRJenc9PSIsI',
223
235
  }),
224
- accountType: z
225
- .nativeEnum(ManagedByType)
226
- .optional()
227
- .default(ManagedByType.DALMORE)
228
- .openapi({
229
- example: ManagedByType.DALMORE,
230
- }),
231
236
  })
232
237
  .refine((data) => data.password === data.confirmPassword, {
233
238
  message: 'Passwords do not match',
@@ -252,6 +257,28 @@ export const IRegisterIssuerBodyZod = BaseAuthBody.extend({
252
257
  },
253
258
  );
254
259
 
260
+ // Client issuer registration schema without code (for API key registration)
261
+ export const IRegisterClientIssuerBodyZod = BaseRegisterIssuerFields.refine(
262
+ (data) => data.password === data.confirmPassword,
263
+ {
264
+ message: 'Passwords do not match',
265
+ path: ['confirmPassword'],
266
+ },
267
+ ).refine(
268
+ (data) => {
269
+ // For client registration, accountName is always required
270
+ return (
271
+ data.accountName !== null &&
272
+ data.accountName !== undefined &&
273
+ data.accountName !== ''
274
+ );
275
+ },
276
+ {
277
+ message: 'Account name is required',
278
+ path: ['accountName'],
279
+ },
280
+ );
281
+
255
282
  // Investors are a special case in which they require phone number at the time of registration
256
283
  // The phone number provided here is NOT validated and will still need to go thru the regular 2FA setup process
257
284
  export const RegisterBodyInvestors = BaseAuthBody.extend({
@@ -5,7 +5,6 @@ import { dateSchema, IPaginationMeta, UserRole } from './common.types';
5
5
  import { userIdSchema } from './user.types';
6
6
  import { accountIdSchema } from './account.types';
7
7
  import { IBaseEntity } from './entity.types';
8
- import { JobsOptions } from 'bullmq';
9
8
 
10
9
  extendZodWithOpenApi(z);
11
10
 
@@ -66,6 +66,9 @@ export const IBonusTier = IBaseEntity.extend({
66
66
  endAmount: z.number().nullable(),
67
67
  });
68
68
 
69
+ export const IBonusTierList = z.array(IBonusTier);
70
+ export type IBonusTierList = z.infer<typeof IBonusTierList>;
71
+
69
72
  export const EstimateBonusTierCalculationZod = z
70
73
  .object({
71
74
  assetId: assetIdSchema
@@ -113,3 +116,32 @@ export const EstimateBonusTierCalculationResponseZod = z.object({
113
116
  export type EstimateBonusTierCalculationResponseZod = z.infer<
114
117
  typeof EstimateBonusTierCalculationResponseZod
115
118
  >;
119
+ export type IBonusTier = z.infer<typeof IBonusTier>;
120
+ export const BonusTierListZod = z
121
+ .array(
122
+ z.object({
123
+ type: z.nativeEnum(BonusType).openapi({ example: BonusType.PERCENTAGE }),
124
+ value: z.number().positive().int().max(1000000).openapi({ example: 50 }),
125
+ startAmount: z.number().max(999999999).openapi({ example: 100 }),
126
+ endAmount: z.number().max(999999999).openapi({ example: 200 }),
127
+ }),
128
+ )
129
+ .min(1, { message: 'At least one bonus tier is required' });
130
+ export type BonusTierListZod = z.infer<typeof BonusTierListZod>;
131
+
132
+ export const PostBonusTierZod = z.object({
133
+ assetId: assetIdSchema.openapi({
134
+ example: 'asset_00041061050r3gg28a1c60t3gf',
135
+ }),
136
+ bonusTiers: BonusTierListZod,
137
+ });
138
+
139
+ export type PostBonusTierZod = z.infer<typeof PostBonusTierZod>;
140
+
141
+ export const CompliancePostBonusTierZod = PostBonusTierZod.extend({
142
+ accountId: accountIdSchema,
143
+ });
144
+
145
+ export type CompliancePostBonusTierZod = z.infer<
146
+ typeof CompliancePostBonusTierZod
147
+ >;
@@ -4,7 +4,7 @@ import { z } from 'zod';
4
4
  import { TypeID } from 'typeid-js';
5
5
  import { ErrorHttpStatusCode } from '@ts-rest/core';
6
6
  import { TwoFactorMethod } from './sms.types';
7
- import { normalizeShortDate } from '../helpers';
7
+ import { normalizeShortDate } from './contract-helpers';
8
8
  import { HttpStatus } from '@nestjs/common';
9
9
 
10
10
  extendZodWithOpenApi(z);
@@ -67,6 +67,8 @@ export enum BaseStatus {
67
67
  INACTIVE = 'INACTIVE',
68
68
  PARTIALLY_REFUNDED = 'PARTIALLY_REFUNDED',
69
69
  VOIDED = 'VOIDED',
70
+ JOIN = 'JOIN',
71
+ RESTORE = 'RESTORE',
70
72
  }
71
73
 
72
74
  export const UserWithoutPasswordAccountZod = IBaseEntity.extend({
@@ -809,6 +811,15 @@ export enum EventField {
809
811
  TRANSACTION_STATUS = 'transactionStatus',
810
812
  }
811
813
 
814
+ export enum EventName {
815
+ STATUS_CHANGED = 'status.changed',
816
+ UPDATE_FIELD = 'update.field',
817
+ ACTION_PERFORMED = 'action.performed',
818
+ PENDING_OVERDUE_TRADES_ALERT = 'pending_overdue.trades.alert',
819
+ IN_REVIEW_TRADES_ALERT = 'trade.inreview.alert',
820
+ INDIVIDUAL_AML_STATUS_CHANGED = 'individual.aml_status.changed',
821
+ }
822
+
812
823
  // pending charged settled refunded cancelled
813
824
  export enum TransactionStatus {
814
825
  PENDING = BaseStatus.PENDING,
@@ -1499,3 +1510,11 @@ export enum JobState {
1499
1510
  PRIORITIZED = 'prioritized',
1500
1511
  WAITING_CHILDREN = 'waiting-children',
1501
1512
  }
1513
+
1514
+ export const SUBJECT_TYPE_MAP: Record<BulkExportType, string> = {
1515
+ [BulkExportType.INVESTOR_ACCOUNTS]: 'Investor Accounts',
1516
+ [BulkExportType.TRADES]: 'Trades',
1517
+ [BulkExportType.COMPLIANCE_TRADES]: 'Compliance Trades',
1518
+ [BulkExportType.SECONDARY_CUSTOMERS]: 'Secondary Customers',
1519
+ [BulkExportType.SECONDARY_TRADES]: 'Secondary Trades',
1520
+ };